Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Implement a basic client for Apple Store Receipt verification.

  • Loading branch information...
commit cf8344b757a8270e93f7a0c918fba81774030e5a 0 parents
Manfred Stienstra Manfred authored
9 Gemfile
@@ -0,0 +1,9 @@
+source 'http://rubygems.org'
+
+gem 'rake'
+gem 'json'
+gem 'nap'
+
+group :test do
+ gem 'peck', :git => 'git@github.com:Fingertips/Peck.git'
+end
9 Rakefile
@@ -0,0 +1,9 @@
+
+task :default => :specs
+
+desc "Run all specs"
+task :specs do
+ FileList['spec/*_spec.rb'].sort.each do |spec|
+ sh "ruby -I lib #{spec} -e ''"
+ end
+end
10 lib/oja.rb
@@ -0,0 +1,10 @@
+require 'oja/receipt'
+require 'oja/request'
+require 'oja/response'
+
+module Oja
+ def self.verify_filename(receipt_filename)
+ receipt = Oja::Receipt.new(:filename => receipt_filename)
+ receipt.verify
+ end
+end
46 lib/oja/mock.rb
@@ -0,0 +1,46 @@
+require 'oja/request'
+require 'oja/response'
+
+module Oja
+ class Mock
+ class Response
+ attr_reader :status_code, :payload
+
+ def initialize(status_code, payload)
+ @status_code = status_code
+ @payload = payload
+ end
+
+ def ok?
+ 200 == status_code.to_i
+ end
+
+ def body
+ JSON.dump(payload)
+ end
+ end
+
+ class << self
+ attr_accessor :responses
+ end
+ @responses = []
+
+ def self.next_response_arguments
+ if responses.empty?
+ [200, { status: 0, receipt: '' }]
+ else
+ responses.pop
+ end
+ end
+
+ def self.next_response
+ Oja::Mock::Response.new(*next_response_arguments)
+ end
+ end
+
+ class Request
+ def response
+ @response ||= Oja::Mock.next_response
+ end
+ end
+end
54 lib/oja/receipt.rb
@@ -0,0 +1,54 @@
+require 'base64'
+
+module Oja
+ class Receipt
+ attr_accessor :data, :filename
+
+ def initialize(attributes)
+ attributes.each do |attribute, value|
+ send("#{attribute}=", value)
+ end
+ end
+
+ def request(environment=:production)
+ request = Request.new(
+ :receipt => self,
+ :environment => environment
+ )
+ request.run
+ end
+
+ def read
+ File.read(filename)
+ end
+
+ def json_data
+ data ? JSON.dump(data) : read
+ end
+
+ def receipt_data
+ Base64.encode64(json_data)
+ end
+
+ def to_json
+ JSON.dump(
+ 'receipt-data' => receipt_data
+ )
+ end
+
+ def verify
+ response = request(:production)
+ if response.sandbox_receipt_in_production?
+ request(:sandbox)
+ else
+ response
+ end
+ end
+
+ private
+
+ def data_or_read
+ data || File.read(filename)
+ end
+ end
+end
37 lib/oja/request.rb
@@ -0,0 +1,37 @@
+require 'rest'
+require 'json'
+
+module Oja
+ class Request
+ ENDPOINT = {
+ :production => 'https://buy.itunes.apple.com/verifyReceipt',
+ :sandbox => 'https://sandbox.itunes.apple.com/verifyReceipt'
+ }
+
+ attr_accessor :environment, :receipt
+
+ def initialize(attributes)
+ attributes.each do |attribute, value|
+ send("#{attribute}=", value)
+ end
+ end
+
+ def endpoint
+ ENDPOINT[environment]
+ end
+
+ def response
+ @response ||= REST.post(endpoint, receipt.to_json)
+ end
+
+ def response_data
+ JSON.parse(response.body)
+ end
+
+ def run
+ if response.ok?
+ Oja::Response.new(response_data)
+ end
+ end
+ end
+end
59 lib/oja/response.rb
@@ -0,0 +1,59 @@
+module Oja
+ class Response
+ STATUS = {
+ 0 => :active,
+ 21000 => :bad_json,
+ 21002 => :malformed,
+ 21003 => :authentication_error,
+ 21004 => :authentication_failed,
+ 21005 => :service_unavailable,
+ 21006 => :inactive,
+ 21007 => :sandbox_receipt_in_production,
+ 21008 => :production_receipt_in_sandbox
+ }
+
+ HUMANIZED_STATUS = {
+ 0 => 'Active',
+ 21000 => 'Bad JSON',
+ 21002 => 'Malformed',
+ 21003 => 'Authentication Error',
+ 21004 => 'Authentication Failed',
+ 21005 => 'Service Unavailable',
+ 21006 => 'Inactive',
+ 21007 => 'Sandbox Receipt in Production',
+ 21008 => 'Production Receipt in Sandbox'
+ }
+
+ attr_reader :status_code, :receipt_data
+
+ def initialize(data)
+ self.data = data
+ end
+
+ def data=(data)
+ @status_code = data['status'].to_i
+ @receipt_data = data['receipt']
+ end
+
+ def status
+ STATUS[status_code]
+ end
+
+ def humanized_status
+ HUMANIZED_STATUS[status_code]
+ end
+
+ STATUS.each do |code, method|
+ define_method("#{method}?") do
+ status_code == code
+ end
+ end
+
+ def self.status_code(needle)
+ needle = needle.to_sym
+ STATUS.each do |status_code, status|
+ return status_code if needle == status
+ end; nil
+ end
+ end
+end
14 spec/fixtures/receipts/auto_renewable.json
@@ -0,0 +1,14 @@
+{
+ "original-purchase-date":"2012-02-14 21:06:28 Etc\/GMT",
+ "purchase-date":"2012-02-14 21:21:26 Etc\/GMT",
+ "expires-date-formatted":"2012-03-14 21:21:26 Etc\/GMT",
+ "expires-date":"1329254786000",
+ "quantity":"1",
+ "bvrs":"1,0",
+ "item-id":"774979675",
+ "original-transaction-id":"1000000026852552",
+ "transaction-id":"1000000026854199",
+ "version-external-identifier":"9362012",
+ "bid":"com.corp.AcmeApp",
+ "product-id":"com.corp.AcmeApp.Monthly"
+}
16 spec/oja_spec.rb
@@ -0,0 +1,16 @@
+require File.expand_path('../preamble', __FILE__)
+require 'oja/mock'
+
+describe Oja do
+ it "verifies an active receipt from disk" do
+ response = Oja.verify_filename(receipt_filename('auto_renewable'))
+ # The default for the mock response is success
+ response.should.be.active
+ end
+
+ it "verifies an inactive receipt from disk" do
+ Oja::Mock.responses << [200, { status: Oja::Response.status_code(:inactive) }]
+ response = Oja.verify_filename(receipt_filename('auto_renewable'))
+ response.should.be.inactive
+ end
+end
24 spec/preamble.rb
@@ -0,0 +1,24 @@
+require 'bundler/setup'
+require 'peck/flavors/vanilla'
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+require 'oja'
+
+class Peck::Context
+ def receipt_filename(name)
+ filename = File.expand_path("../fixtures/receipts/#{name}.json", __FILE__)
+ if File.exist?(filename)
+ filename
+ else
+ raise ArgumentError, "There is no receipt fixture with the name `#{name}'"
+ end
+ end
+
+ def receipt_data(name)
+ JSON.parse(File.read(receipt_filename(name)))
+ end
+
+ def receipt_as_json(name)
+ JSON.dump(receipt_data(name))
+ end
+end
13 spec/receipt_spec.rb
@@ -0,0 +1,13 @@
+require File.expand_path('../preamble', __FILE__)
+
+describe Oja::Receipt do
+ it "loads a receipt from disk" do
+ receipt = Oja::Receipt.new(:filename => receipt_filename('auto_renewable'))
+ JSON.parse(receipt.json_data).should == receipt_data('auto_renewable')
+ end
+
+ it "accepts data for a receipt" do
+ receipt = Oja::Receipt.new(:data => receipt_data('auto_renewable'))
+ JSON.parse(receipt.json_data).should == receipt_data('auto_renewable')
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.