├── VERSION ├── spec ├── fake_json │ ├── offline.json │ ├── invalid.json │ ├── valid.json │ ├── sandboxed.json │ ├── autorenew_subscription.json │ ├── autorenew_subscription_expired.json │ ├── valid_application.json │ └── array_of_latest_receipt_info.json ├── spec_helper.rb ├── helpers │ └── fake_json_helper.rb ├── itunes_spec.rb └── itunes │ └── receipt_spec.rb ├── .rspec ├── .travis.yml ├── Gemfile ├── .gitignore ├── Rakefile ├── lib ├── itunes.rb └── itunes │ └── receipt.rb ├── LICENSE ├── itunes-receipt.gemspec └── README.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /spec/fake_json/offline.json: -------------------------------------------------------------------------------- 1 | {"status":21005} -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=documentation 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec -------------------------------------------------------------------------------- /spec/fake_json/invalid.json: -------------------------------------------------------------------------------- 1 | {"status":21002, "exception":"java.lang.IllegalArgumentException: propertyListFromString parsed an object, but there's still more text in the string. A plist should contain only one top-level object. Line number: 6, column: 1."} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | Gemfile.lock 21 | 22 | ## PROJECT::SPECIFIC 23 | coverage* 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | 3 | SimpleCov.start do 4 | add_filter 'spec' 5 | end 6 | 7 | require 'rspec' 8 | require 'itunes/receipt' 9 | require 'helpers/fake_json_helper' 10 | 11 | def sandbox_mode(&block) 12 | Itunes.sandbox! 13 | yield 14 | ensure 15 | Itunes.sandbox = false 16 | end 17 | -------------------------------------------------------------------------------- /spec/fake_json/valid.json: -------------------------------------------------------------------------------- 1 | {"receipt":{"item_id":"420862234", "original_transaction_id":"1000000001479608", "bvrs":"1.0", "product_id":"com.cerego.iknow.30d", "purchase_date":"2011-02-17 06:20:57 Etc/GMT", "quantity":"1", "bid":"com.cerego.iknow", "original_purchase_date":"2011-02-17 06:20:57 Etc/GMT", "transaction_id":"1000000001479608"}, "status":0} -------------------------------------------------------------------------------- /spec/fake_json/sandboxed.json: -------------------------------------------------------------------------------- 1 | {"receipt":{"item_id":"420862234", "original_transaction_id":"1000000001479608", "bvrs":"1.0", "product_id":"com.cerego.iknow.30d", "purchase_date":"2011-02-17 06:20:57 Etc/GMT", "quantity":"1", "bid":"com.cerego.iknow", "original_purchase_date":"2011-02-17 06:20:57 Etc/GMT", "transaction_id":"1000000001500000"}, "status":21007} 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | namespace :coverage do 8 | desc "Open coverage report" 9 | task :report do 10 | require 'simplecov' 11 | `open "#{File.join SimpleCov.coverage_path, 'index.html'}"` 12 | end 13 | end 14 | 15 | task :spec do 16 | Rake::Task[:'coverage:report'].invoke unless ENV['TRAVIS_RUBY_VERSION'] 17 | end 18 | 19 | task :default => :spec -------------------------------------------------------------------------------- /spec/helpers/fake_json_helper.rb: -------------------------------------------------------------------------------- 1 | require 'fakeweb' 2 | 3 | module FakeJsonHelper 4 | 5 | def fake_json(expected, options = {}) 6 | FakeWeb.register_uri( 7 | :post, 8 | Itunes.endpoint, 9 | options.merge( 10 | :body => File.read(File.join(File.dirname(__FILE__), '../fake_json', "#{expected}.json")) 11 | ) 12 | ) 13 | end 14 | 15 | def post_to(endpoint) 16 | raise_error( 17 | FakeWeb::NetConnectNotAllowedError, 18 | "Real HTTP connections are disabled. Unregistered request: POST #{endpoint}" 19 | ) 20 | end 21 | 22 | end 23 | 24 | FakeWeb.allow_net_connect = false 25 | include FakeJsonHelper -------------------------------------------------------------------------------- /spec/itunes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Itunes do 4 | 5 | describe 'the module' do 6 | before do 7 | Itunes.shared_secret = nil 8 | end 9 | 10 | context 'sandbox' do 11 | it 'should be production by default' do 12 | Itunes.sandbox?.should == false 13 | end 14 | 15 | it 'should be settable' do 16 | Itunes.sandbox! 17 | Itunes.sandbox?.should == true 18 | end 19 | end 20 | 21 | context 'shared_secret' do 22 | it 'should allow setting' do 23 | Itunes.shared_secret.should be_nil 24 | Itunes.shared_secret = 'hey' 25 | Itunes.shared_secret.should == 'hey' 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/itunes.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'active_support/all' 3 | require 'restclient_with_cert' 4 | 5 | module Itunes 6 | 7 | ENDPOINT = { 8 | :production => 'https://buy.itunes.apple.com/verifyReceipt', 9 | :sandbox => 'https://sandbox.itunes.apple.com/verifyReceipt' 10 | } 11 | 12 | def self.endpoint 13 | ENDPOINT[itunes_env] 14 | end 15 | 16 | def self.itunes_env 17 | sandbox? ? :sandbox : :production 18 | end 19 | 20 | def self.sandbox? 21 | @@sandbox 22 | end 23 | def self.sandbox! 24 | self.sandbox = true 25 | end 26 | def self.sandbox=(boolean) 27 | @@sandbox = boolean 28 | end 29 | self.sandbox = false 30 | 31 | def self.shared_secret 32 | @@shared_secret 33 | end 34 | def self.shared_secret=(shared_secret) 35 | @@shared_secret = shared_secret 36 | end 37 | self.shared_secret = nil 38 | 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 nov matake 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /itunes-receipt.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'itunes-receipt' 6 | s.description = %q{Handle iTunes In App Purchase Receipt Verification} 7 | s.summary = %q{Handle iTunes In App Purchase Receipt Verification} 8 | s.version = File.read(File.join(File.dirname(__FILE__), 'VERSION')) 9 | s.authors = ['nov matake'] 10 | s.email = 'nov@matake.jp' 11 | s.homepage = 'http://github.com/nov/itunes-receipt' 12 | s.require_paths = ['lib'] 13 | s.files = `git ls-files`.split("\n") 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 16 | s.add_dependency 'json', '>= 1.4.3' 17 | s.add_dependency 'restclient_with_cert' 18 | s.add_dependency 'activesupport', '>= 2.3' 19 | s.add_dependency 'i18n' 20 | s.add_development_dependency 'rake', '>= 0.8' 21 | s.add_development_dependency 'rspec', '>= 2' 22 | s.add_development_dependency 'fakeweb', '>= 1.3.0' 23 | s.add_development_dependency 'simplecov' 24 | end 25 | -------------------------------------------------------------------------------- /spec/fake_json/autorenew_subscription.json: -------------------------------------------------------------------------------- 1 | {"receipt":{"original_purchase_date_pst":"2012-10-11 07:45:40 America/Los_Angeles", "unique_identifier":"4d69cc362c5c58ce62da68ee43721ea670907bad", "original_transaction_id":"1000000057005439", "expires_date":"1350157508197", "transaction_id":"1000000055076747", "quantity":"1", "product_id":"com.notkeepingitreal.fizzbuzz.subscription.autorenew1m", "original_purchase_date_ms":"1349966740000", "bid":"com.notkeepingitreal.fizzbuzz", "web_order_line_item_id":"1000000026553289", "bvrs":"1.0", "expires_date_formatted":"2012-10-13 19:45:08 Etc/GMT", "purchase_date":"2012-10-13 19:40:08 Etc/GMT", "purchase_date_ms":"1350157208197", "expires_date_formatted_pst":"2012-10-13 12:45:08 America/Los_Angeles", "purchase_date_pst":"2012-10-13 12:40:08 America/Los_Angeles", "original_purchase_date":"2012-10-11 14:45:40 Etc/GMT", "item_id":"570504929"}, "latest_receipt_info":{"original_purchase_date_pst":"2012-10-11 07:45:40 America/Los_Angeles", "unique_identifier":"4d79cc562c5c58ce62da68ee43721ea670907bad", "original_transaction_id":"1000000057005439", "expires_date":"1350157808000", "transaction_id":"1000000052076747", "quantity":"1", "product_id":"com.notkeepingitreal.fizzbuzz.subscription.autorenew1m", "original_purchase_date_ms":"1349966740000", "bid":"com.notkeepingitreal.fizzbuzz", "web_order_line_item_id":"1000000027293289", "bvrs":"1.0", "expires_date_formatted":"2012-10-13 19:50:08 Etc/GMT", "purchase_date":"2012-10-13 19:40:08 Etc/GMT", "purchase_date_ms":"1350157208000", "expires_date_formatted_pst":"2012-10-13 12:50:08 America/Los_Angeles", "purchase_date_pst":"2012-10-13 12:40:08 America/Los_Angeles", "original_purchase_date":"2012-10-11 14:45:40 Etc/GMT", "item_id":"570504929"}, "status":0, "latest_receipt":"junk="} 2 | 3 | -------------------------------------------------------------------------------- /spec/fake_json/autorenew_subscription_expired.json: -------------------------------------------------------------------------------- 1 | {"receipt":{"original_purchase_date_pst":"2012-10-11 07:45:40 America/Los_Angeles", "unique_identifier":"4d69cc362c5c58ce62da68ee43721ea670907bad", "original_transaction_id":"1000000057005439", "expires_date":"1350157508197", "transaction_id":"1000000055076747", "quantity":"1", "product_id":"com.notkeepingitreal.fizzbuzz.subscription.autorenew1m", "original_purchase_date_ms":"1349966740000", "bid":"com.notkeepingitreal.fizzbuzz", "web_order_line_item_id":"1000000026553289", "bvrs":"1.0", "expires_date_formatted":"2012-10-13 19:45:08 Etc/GMT", "purchase_date":"2012-10-13 19:40:08 Etc/GMT", "purchase_date_ms":"1350157208197", "expires_date_formatted_pst":"2012-10-13 12:45:08 America/Los_Angeles", "purchase_date_pst":"2012-10-13 12:40:08 America/Los_Angeles", "original_purchase_date":"2012-10-11 14:45:40 Etc/GMT", "item_id":"570504929"}, "latest_receipt_info":{"original_purchase_date_pst":"2012-10-11 07:45:40 America/Los_Angeles", "unique_identifier":"4d79cc562c5c58ce62da68ee43721ea670907bad", "original_transaction_id":"1000000057005439", "expires_date":"1350157808000", "transaction_id":"1000000052076747", "quantity":"1", "product_id":"com.notkeepingitreal.fizzbuzz.subscription.autorenew1m", "original_purchase_date_ms":"1349966740000", "bid":"com.notkeepingitreal.fizzbuzz", "web_order_line_item_id":"1000000027293289", "bvrs":"1.0", "expires_date_formatted":"2012-10-13 19:50:08 Etc/GMT", "purchase_date":"2012-10-13 19:40:08 Etc/GMT", "purchase_date_ms":"1350157208000", "expires_date_formatted_pst":"2012-10-13 12:50:08 America/Los_Angeles", "purchase_date_pst":"2012-10-13 12:40:08 America/Los_Angeles", "original_purchase_date":"2012-10-11 14:45:40 Etc/GMT", "item_id":"570504929"}, "status":21006, "latest_receipt":"junk="} 2 | 3 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = itunes-receipt 2 | 3 | Handle iTunes In App Purchase Receipt Verification. 4 | 5 | == Installation 6 | 7 | gem install itunes-receipt 8 | 9 | == Usage 10 | 11 | require 'itunes/receipt' 12 | 13 | # pass Base64 encoded raw receipt data which you received from your iOS app 14 | receipt = Itunes::Receipt.verify! 'ewoJInNpZ25hdHVyZSIgPSAi...' 15 | receipt.product_id # => 'com.example.products.100gems' 16 | receipt.transaction_id # => '1234567890' 17 | : 18 | 19 | See lib/itunes/receipt.rb for more attributes. 20 | 21 | If you want to accept iTunes sandbox receipts, do like this. 22 | With allow_sandbox_receipt option specified, this gem post given receipt data to iTunes production first, and when iTunes production tells it's sandbox receipt, re-send it to iTunes sandbox again automatically. 23 | Without this option, this gem just raises an exception. 24 | 25 | receipt = Itunes::Receipt.verify! 'ewoJInNpZ25hdHVyZSIgPSAi...', :allow_sandbox_receipt 26 | receipt.sandbox? # => true/false 27 | 28 | If you want to accept ONLY iTunes sandbox receipts (in your stable server etc.), call Itunes.sandbox! somewhere before you call Itunes::Receipt.verify!. 29 | Then all verification call after that goes to iTunes sandbox, until you do Itunes.sandbox = false. 30 | 31 | == Note on Patches/Pull Requests 32 | 33 | * Fork the project. 34 | * Make your feature addition or bug fix. 35 | * Add tests for it. This is important so I don't break it in a 36 | future version unintentionally. 37 | * Commit, do not mess with rakefile, version, or history. 38 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 39 | * Send me a pull request. Bonus points for topic branches. 40 | 41 | == Copyright 42 | 43 | Copyright (c) 2011 nov matake. See LICENSE for details. 44 | -------------------------------------------------------------------------------- /spec/fake_json/valid_application.json: -------------------------------------------------------------------------------- 1 | {"status":0, 2 | "environment":"Sandbox", 3 | "receipt":{ 4 | "receipt_type":"ProductionSandbox", 5 | "adam_id":0, 6 | "bundle_id":"com.tekkinnovations.fars", 7 | "application_version":"1.80", 8 | "download_id":0, 9 | "request_date":"2013-11-26 14:49:09 Etc/GMT", 10 | "request_date_ms":"1385477349878", 11 | "request_date_pst":"2013-11-26 06:49:09 America/Los_Angeles", 12 | "original_purchase_date":"2013-08-01 07:00:00 Etc/GMT", 13 | "original_purchase_date_ms":"1375340400000", 14 | "original_purchase_date_pst":"2013-08-01 00:00:00 America/Los_Angeles", 15 | "original_application_version":"1.0", 16 | "in_app":[{"quantity":"1", 17 | "product_id":"com.tekkinnovations.fars.subscription.6.months", 18 | "transaction_id":"1000000091176126", 19 | "original_transaction_id":"1000000091176126", 20 | "purchase_date":"2013-11-26 05:58:48 Etc/GMT", 21 | "purchase_date_ms":"1385445528000", 22 | "purchase_date_pst":"2013-11-25 21:58:48 America/Los_Angeles", 23 | "original_purchase_date":"2013-10-24 04:55:56 Etc/GMT", 24 | "original_purchase_date_ms":"1382590556000", 25 | "original_purchase_date_pst":"2013-10-23 21:55:56 America/Los_Angeles", 26 | "is_trial_period":"false"}, 27 | {"quantity":"1", 28 | "product_id":"com.tekkinnovations.fars.subscription.3.months", 29 | "transaction_id":"1000000091221097", 30 | "original_transaction_id":"1000000091221097", 31 | "purchase_date":"2013-11-26 05:58:48 Etc/GMT", 32 | "purchase_date_ms":"1385445528000", 33 | "purchase_date_pst":"2013-11-25 21:58:48 America/Los_Angeles", 34 | "original_purchase_date":"2013-10-24 09:40:22 Etc/GMT", 35 | "original_purchase_date_ms":"1382607622000", 36 | "original_purchase_date_pst":"2013-10-24 02:40:22 America/Los_Angeles", 37 | "is_trial_period":"false"} 38 | ]}} 39 | -------------------------------------------------------------------------------- /spec/fake_json/array_of_latest_receipt_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment": "Sandbox", 3 | "latest_receipt": "BASE64ENCODED RECEIPT WAS HERE", 4 | "latest_receipt_info": [{ 5 | "expires_date": "2014-11-12 15:10:33 Etc/GMT", 6 | "expires_date_ms": "1415805033000", 7 | "expires_date_pst": "2014-11-12 07:10:33 America/Los_Angeles", 8 | "is_trial_period": "true", 9 | "original_purchase_date": "2014-11-12 15:07:34 Etc/GMT", 10 | "original_purchase_date_ms": "1415804854000", 11 | "original_purchase_date_pst": "2014-11-12 07:07:34 America/Los_Angeles", 12 | "original_transaction_id": "1000000131413546", 13 | "product_id": "com.musteus.musteus.subscription.month", 14 | "purchase_date": "2014-11-12 15:25:30 Etc/GMT", 15 | "purchase_date_ms": "1415805930044", 16 | "purchase_date_pst": "2014-11-12 07:25:30 America/Los_Angeles", 17 | "quantity": "1", 18 | "transaction_id": "1000000131413546", 19 | "web_order_line_item_id": "1000000028817186" 20 | }, { 21 | "expires_date": "2014-11-12 15:15:33 Etc/GMT", 22 | "expires_date_ms": "1415805333000", 23 | "expires_date_pst": "2014-11-12 07:15:33 America/Los_Angeles", 24 | "is_trial_period": "false", 25 | "original_purchase_date": "2014-11-12 15:08:34 Etc/GMT", 26 | "original_purchase_date_ms": "1415804914000", 27 | "original_purchase_date_pst": "2014-11-12 07:08:34 America/Los_Angeles", 28 | "original_transaction_id": "1000000131413546", 29 | "product_id": "com.musteus.musteus.subscription.month", 30 | "purchase_date": "2014-11-12 15:25:30 Etc/GMT", 31 | "purchase_date_ms": "1415805930044", 32 | "purchase_date_pst": "2014-11-12 07:25:30 America/Los_Angeles", 33 | "quantity": "1", 34 | "transaction_id": "1000000131413602", 35 | "web_order_line_item_id": "1000000028817187" 36 | }, { 37 | "expires_date": "2014-11-12 15:20:33 Etc/GMT", 38 | "expires_date_ms": "1415805633000", 39 | "expires_date_pst": "2014-11-12 07:20:33 America/Los_Angeles", 40 | "is_trial_period": "false", 41 | "original_purchase_date": "2014-11-12 15:14:15 Etc/GMT", 42 | "original_purchase_date_ms": "1415805255000", 43 | "original_purchase_date_pst": "2014-11-12 07:14:15 America/Los_Angeles", 44 | "original_transaction_id": "1000000131413546", 45 | "product_id": "com.musteus.musteus.subscription.month", 46 | "purchase_date": "2014-11-12 15:25:30 Etc/GMT", 47 | "purchase_date_ms": "1415805930044", 48 | "purchase_date_pst": "2014-11-12 07:25:30 America/Los_Angeles", 49 | "quantity": "1", 50 | "transaction_id": "1000000131414118", 51 | "web_order_line_item_id": "1000000028817195" 52 | }, { 53 | "expires_date": "2014-11-12 15:25:33 Etc/GMT", 54 | "expires_date_ms": "1415805933000", 55 | "expires_date_pst": "2014-11-12 07:25:33 America/Los_Angeles", 56 | "is_trial_period": "false", 57 | "original_purchase_date": "2014-11-12 15:18:35 Etc/GMT", 58 | "original_purchase_date_ms": "1415805515000", 59 | "original_purchase_date_pst": "2014-11-12 07:18:35 America/Los_Angeles", 60 | "original_transaction_id": "1000000131413546", 61 | "product_id": "com.musteus.musteus.subscription.month", 62 | "purchase_date": "2014-11-12 15:25:30 Etc/GMT", 63 | "purchase_date_ms": "1415805930044", 64 | "purchase_date_pst": "2014-11-12 07:25:30 America/Los_Angeles", 65 | "quantity": "1", 66 | "transaction_id": "1000000131415612", 67 | "web_order_line_item_id": "1000000028817228" 68 | }, { 69 | "expires_date": "2014-11-12 15:30:33 Etc/GMT", 70 | "expires_date_ms": "1415806233000", 71 | "expires_date_pst": "2014-11-12 07:30:33 America/Los_Angeles", 72 | "is_trial_period": "false", 73 | "original_purchase_date": "2014-11-12 15:23:35 Etc/GMT", 74 | "original_purchase_date_ms": "1415805815000", 75 | "original_purchase_date_pst": "2014-11-12 07:23:35 America/Los_Angeles", 76 | "original_transaction_id": "1000000131413546", 77 | "product_id": "com.musteus.musteus.subscription.month", 78 | "purchase_date": "2014-11-12 15:25:30 Etc/GMT", 79 | "purchase_date_ms": "1415805930044", 80 | "purchase_date_pst": "2014-11-12 07:25:30 America/Los_Angeles", 81 | "quantity": "1", 82 | "transaction_id": "1000000131415863", 83 | "web_order_line_item_id": "1000000028817256" 84 | }], 85 | "receipt": { 86 | "adam_id": 0, 87 | "app_item_id": 0, 88 | "application_version": "1.0", 89 | "bundle_id": "com.musteus.musteus", 90 | "download_id": 0, 91 | "in_app": [{ 92 | "expires_date": "2014-11-12 15:15:33 Etc/GMT", 93 | "expires_date_ms": "1415805333000", 94 | "expires_date_pst": "2014-11-12 07:15:33 America/Los_Angeles", 95 | "is_trial_period": "false", 96 | "original_purchase_date": "2014-11-12 15:08:34 Etc/GMT", 97 | "original_purchase_date_ms": "1415804914000", 98 | "original_purchase_date_pst": "2014-11-12 07:08:34 America/Los_Angeles", 99 | "original_transaction_id": "1000000131413546", 100 | "product_id": "com.musteus.musteus.subscription.month", 101 | "purchase_date": "2014-11-12 15:09:10 Etc/GMT", 102 | "purchase_date_ms": "1415804950000", 103 | "purchase_date_pst": "2014-11-12 07:09:10 America/Los_Angeles", 104 | "quantity": "1", 105 | "transaction_id": "1000000131413602", 106 | "web_order_line_item_id": "1000000028817187" 107 | }, { 108 | "expires_date": "2014-11-12 15:10:33 Etc/GMT", 109 | "expires_date_ms": "1415805033000", 110 | "expires_date_pst": "2014-11-12 07:10:33 America/Los_Angeles", 111 | "is_trial_period": "false", 112 | "original_purchase_date": "2014-11-12 15:07:34 Etc/GMT", 113 | "original_purchase_date_ms": "1415804854000", 114 | "original_purchase_date_pst": "2014-11-12 07:07:34 America/Los_Angeles", 115 | "original_transaction_id": "1000000131413546", 116 | "product_id": "com.musteus.musteus.subscription.month", 117 | "purchase_date": "2014-11-12 15:09:10 Etc/GMT", 118 | "purchase_date_ms": "1415804950000", 119 | "purchase_date_pst": "2014-11-12 07:09:10 America/Los_Angeles", 120 | "quantity": "1", 121 | "transaction_id": "1000000131413546", 122 | "web_order_line_item_id": "1000000028817186" 123 | }], 124 | "original_application_version": "1.0", 125 | "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 126 | "original_purchase_date_ms": "1375340400000", 127 | "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 128 | "receipt_type": "ProductionSandbox", 129 | "request_date": "2014-11-12 15:25:30 Etc/GMT", 130 | "request_date_ms": "1415805930053", 131 | "request_date_pst": "2014-11-12 07:25:30 America/Los_Angeles", 132 | "version_external_identifier": 0 133 | }, 134 | "status": 0 135 | } 136 | -------------------------------------------------------------------------------- /lib/itunes/receipt.rb: -------------------------------------------------------------------------------- 1 | require 'itunes' 2 | 3 | module Itunes 4 | class Receipt 5 | class VerificationFailed < StandardError 6 | attr_reader :status 7 | def initialize(attributes = {}) 8 | @status = attributes[:status] 9 | super attributes[:exception] 10 | end 11 | end 12 | 13 | class SandboxReceiptReceived < VerificationFailed; end; 14 | 15 | class ReceiptServerOffline < VerificationFailed; end; 16 | 17 | class ExpiredReceiptReceived < VerificationFailed 18 | attr_reader :receipt 19 | def initialize(attributes = {}) 20 | @receipt = attributes[:receipt] 21 | super attributes 22 | end 23 | end 24 | 25 | attr_reader( 26 | :adam_id, 27 | :app_item_id, 28 | :application_version, 29 | :bid, 30 | :bundle_id, 31 | :bvrs, 32 | :download_id, 33 | :expires_date, 34 | :in_app, 35 | :is_trial_period, 36 | :itunes_env, 37 | :latest, 38 | :original, 39 | :product_id, 40 | :purchase_date, 41 | :purchase_date_ms, 42 | :purchase_date_pst, 43 | :quantity, 44 | :receipt_data, 45 | :request_date, 46 | :request_date_ms, 47 | :request_date_pst, 48 | :transaction_id, 49 | :version_external_identifier, 50 | ) 51 | 52 | def initialize(attributes = {}) 53 | receipt_attributes = attributes.with_indifferent_access[:receipt] 54 | @adam_id = receipt_attributes[:adam_id] 55 | @app_item_id = receipt_attributes[:app_item_id] 56 | @application_version = receipt_attributes[:application_version] 57 | @bid = receipt_attributes[:bid] 58 | @bundle_id = receipt_attributes[:bundle_id] 59 | @bvrs = receipt_attributes[:bvrs] 60 | @download_id = receipt_attributes[:download_id] 61 | @expires_date = if receipt_attributes[:expires_date] 62 | Time.at(receipt_attributes[:expires_date].to_i / 1000) 63 | end 64 | @in_app = if receipt_attributes[:in_app] 65 | receipt_attributes[:in_app].map { |ia| self.class.new(:receipt => ia) } 66 | end 67 | @is_trial_period = if receipt_attributes[:is_trial_period] 68 | receipt_attributes[:is_trial_period] == "true" 69 | end 70 | @itunes_env = attributes[:itunes_env] || Itunes.itunes_env 71 | @latest = case attributes[:latest_receipt_info] 72 | when Hash 73 | self.class.new( 74 | :receipt => attributes[:latest_receipt_info], 75 | :latest_receipt => attributes[:latest_receipt], 76 | :receipt_type => :latest 77 | ) 78 | when Array 79 | attributes[:latest_receipt_info].collect do |latest_receipt_info| 80 | self.class.new( 81 | :receipt => latest_receipt_info, 82 | :latest_receipt => attributes[:latest_receipt], 83 | :receipt_type => :latest 84 | ) 85 | end 86 | end 87 | @original = if receipt_attributes[:original_transaction_id] || receipt_attributes[:original_purchase_date] 88 | self.class.new(:receipt => { 89 | :transaction_id => receipt_attributes[:original_transaction_id], 90 | :purchase_date => receipt_attributes[:original_purchase_date], 91 | :purchase_date_ms => receipt_attributes[:original_purchase_date_ms], 92 | :purchase_date_pst => receipt_attributes[:original_purchase_date_pst], 93 | :application_version => receipt_attributes[:original_application_version] 94 | }) 95 | end 96 | @product_id = receipt_attributes[:product_id] 97 | @purchase_date = if receipt_attributes[:purchase_date] 98 | Time.parse receipt_attributes[:purchase_date].sub('Etc/GMT', 'GMT') 99 | end 100 | @purchase_date_ms = if receipt_attributes[:purchase_date_ms] 101 | receipt_attributes[:purchase_date_ms].to_i 102 | end 103 | @purchase_date_pst = if receipt_attributes[:purchase_date_pst] 104 | Time.parse receipt_attributes[:purchase_date_pst].sub('America/Los_Angeles', 'PST') 105 | end 106 | @quantity = if receipt_attributes[:quantity] 107 | receipt_attributes[:quantity].to_i 108 | end 109 | @receipt_data = if attributes[:receipt_type] == :latest 110 | attributes[:latest_receipt] 111 | end 112 | @request_date = if receipt_attributes[:request_date] 113 | Time.parse receipt_attributes[:request_date].sub('Etc/', '') 114 | end 115 | @request_date_ms = if receipt_attributes[:request_date_ms] 116 | receipt_attributes[:request_date_ms].to_i 117 | end 118 | @request_date_pst = if receipt_attributes[:request_date_pst] 119 | Time.parse receipt_attributes[:request_date_pst].sub('America/Los_Angeles', 'PST') 120 | end 121 | @transaction_id = receipt_attributes[:transaction_id] 122 | @version_external_identifier = receipt_attributes[:version_external_identifier] 123 | end 124 | 125 | def application_receipt? 126 | !@bundle_id.nil? 127 | end 128 | 129 | def sandbox? 130 | itunes_env == :sandbox 131 | end 132 | 133 | def self.verify!(receipt_data, allow_sandbox_receipt = false) 134 | request_data = {:'receipt-data' => receipt_data} 135 | request_data.merge!(:password => Itunes.shared_secret) if Itunes.shared_secret 136 | response = post_to_endpoint(request_data) 137 | begin 138 | successful_response(response) 139 | rescue SandboxReceiptReceived => e 140 | # Retry with sandbox, as per: 141 | # http://developer.apple.com/library/ios/#technotes/tn2259/_index.html 142 | # FAQ#16 143 | if allow_sandbox_receipt 144 | sandbox_response = post_to_endpoint(request_data, Itunes::ENDPOINT[:sandbox]) 145 | successful_response( 146 | sandbox_response.merge(:itunes_env => :sandbox) 147 | ) 148 | else 149 | raise e 150 | end 151 | end 152 | end 153 | 154 | private 155 | 156 | def self.post_to_endpoint(request_data, endpoint = Itunes.endpoint) 157 | response = RestClient.post( 158 | endpoint, 159 | request_data.to_json 160 | ) 161 | response = JSON.parse(response).with_indifferent_access 162 | end 163 | 164 | def self.successful_response(response) 165 | case response[:status] 166 | when 0 167 | new response 168 | when 21005 169 | raise ReceiptServerOffline.new(response) 170 | when 21006 171 | raise ExpiredReceiptReceived.new(response) 172 | when 21007 173 | raise SandboxReceiptReceived.new(response) 174 | else 175 | raise VerificationFailed.new(response) 176 | end 177 | end 178 | 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/itunes/receipt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Itunes::Receipt do 4 | 5 | describe '.verify!' do 6 | it 'should support sandbox mode' do 7 | sandbox_mode do 8 | expect do 9 | Itunes::Receipt.verify! 'receipt-data' 10 | end.to post_to Itunes::ENDPOINT[:sandbox] 11 | end 12 | end 13 | 14 | it 'should not pass along shared secret if not set' do 15 | fake_json(:invalid) 16 | Itunes.shared_secret = nil 17 | RestClient.should_receive(:post).with(Itunes.endpoint, {:'receipt-data' => 'receipt-data'}.to_json).and_return("{}") 18 | expect do 19 | Itunes::Receipt.verify! 'receipt-data' 20 | end.to raise_error Itunes::Receipt::VerificationFailed 21 | end 22 | 23 | it 'should pass along shared secret if set' do 24 | fake_json(:invalid) 25 | Itunes.shared_secret = 'hey' 26 | RestClient.should_receive(:post).with(Itunes.endpoint, {:'receipt-data' => 'receipt-data', :password => 'hey'}.to_json).and_return("{}") 27 | expect do 28 | Itunes::Receipt.verify! 'receipt-data' 29 | end.to raise_error Itunes::Receipt::VerificationFailed 30 | end 31 | 32 | context 'when invalid' do 33 | before do 34 | fake_json :invalid 35 | end 36 | 37 | it 'should raise VerificationFailed' do 38 | expect do 39 | Itunes::Receipt.verify! 'invalid' 40 | end.to raise_error Itunes::Receipt::VerificationFailed 41 | end 42 | 43 | context 'due to a sandbox receipt reply' do 44 | before do 45 | fake_json :sandboxed 46 | sandbox_mode do 47 | fake_json :valid 48 | end 49 | end 50 | 51 | context 'when sandbox receipt accepted explicitly' do 52 | it 'should try and verify the receipt against the sandbox ' do 53 | receipt = Itunes::Receipt.verify! 'sandboxed', :allow_sandbox_receipt 54 | receipt.should be_instance_of Itunes::Receipt 55 | receipt.transaction_id.should == '1000000001479608' 56 | receipt.itunes_env.should == :sandbox 57 | receipt.sandbox?.should eq true 58 | end 59 | end 60 | 61 | context 'otherwise' do 62 | it 'should raise SandboxReceiptReceived exception' do 63 | expect do 64 | Itunes::Receipt.verify! 'sandboxed' 65 | end.to raise_error Itunes::Receipt::SandboxReceiptReceived 66 | end 67 | end 68 | end 69 | end 70 | 71 | context 'when valid' do 72 | before do 73 | fake_json :valid 74 | end 75 | 76 | it 'should not be an application receipt' do 77 | receipt = Itunes::Receipt.verify! 'valid_application' 78 | receipt.application_receipt?.should == false 79 | end 80 | 81 | it 'should return valid Receipt instance' do 82 | receipt = Itunes::Receipt.verify! 'valid' 83 | receipt.should be_instance_of Itunes::Receipt 84 | receipt.quantity == 1 85 | receipt.product_id.should == 'com.cerego.iknow.30d' 86 | receipt.transaction_id.should == '1000000001479608' 87 | receipt.purchase_date.should == Time.utc(2011, 2, 17, 6, 20, 57) 88 | receipt.bid.should == 'com.cerego.iknow' 89 | receipt.bvrs.should == '1.0' 90 | receipt.original.quantity.should be_nil 91 | receipt.original.transaction_id.should == '1000000001479608' 92 | receipt.original.purchase_date.should == Time.utc(2011, 2, 17, 6, 20, 57) 93 | receipt.expires_date.should be_nil 94 | receipt.receipt_data.should be_nil 95 | receipt.itunes_env.should == :production 96 | 97 | # Those attributes are not returned from iTunes Connect Sandbox 98 | receipt.app_item_id.should be_nil 99 | receipt.version_external_identifier.should be_nil 100 | end 101 | end 102 | 103 | context 'when application receipt' do 104 | before do 105 | fake_json :valid_application 106 | end 107 | 108 | it 'should be an application receipt' do 109 | receipt = Itunes::Receipt.verify! 'valid_application' 110 | receipt.application_receipt?.should == true 111 | end 112 | 113 | it 'should return valid Receipt instance' do 114 | receipt = Itunes::Receipt.verify! 'valid_application' 115 | receipt.bundle_id.should == 'com.tekkinnovations.fars' 116 | receipt.application_version.should == '1.80' 117 | receipt.in_app.should be_instance_of Array 118 | 119 | receipt.in_app[0].should be_instance_of Itunes::Receipt 120 | receipt.in_app[0].quantity.should == 1 121 | receipt.in_app[0].product_id.should == "com.tekkinnovations.fars.subscription.6.months" 122 | receipt.in_app[0].transaction_id.should == "1000000091176126" 123 | receipt.in_app[0].purchase_date.should == Time.utc(2013, 11, 26, 5, 58, 48) 124 | receipt.in_app[0].original.purchase_date.should == Time.utc(2013, 10, 24, 4, 55, 56) 125 | 126 | receipt.in_app[1].should be_instance_of Itunes::Receipt 127 | receipt.in_app[1].quantity.should == 1 128 | receipt.in_app[1].product_id.should == "com.tekkinnovations.fars.subscription.3.months" 129 | receipt.in_app[1].transaction_id.should == "1000000091221097" 130 | receipt.in_app[1].purchase_date.should == Time.utc(2013, 11, 26, 5, 58, 48) 131 | receipt.in_app[1].original.purchase_date.should == Time.utc(2013, 10, 24, 9, 40, 22) 132 | 133 | receipt.original.quantity.should be_nil 134 | receipt.original.transaction_id.should be_nil 135 | receipt.original.purchase_date.should == Time.utc(2013, 8, 1, 7, 00, 00) 136 | receipt.original.application_version.should == '1.0' 137 | 138 | receipt.should be_instance_of Itunes::Receipt 139 | receipt.quantity.should be_nil 140 | receipt.product_id.should be_nil 141 | receipt.transaction_id.should be_nil 142 | receipt.purchase_date.should be_nil 143 | receipt.bid.should be_nil 144 | receipt.bvrs.should be_nil 145 | receipt.expires_date.should be_nil 146 | receipt.receipt_data.should be_nil 147 | receipt.itunes_env.should == :production 148 | 149 | # Those attributes are not returned from iTunes Connect Sandbox 150 | receipt.app_item_id.should be_nil 151 | receipt.version_external_identifier.should be_nil 152 | end 153 | end 154 | 155 | context 'when autorenew subscription' do 156 | before do 157 | fake_json :autorenew_subscription 158 | end 159 | 160 | it 'should return valid Receipt instance for autorenew subscription' do 161 | original_transaction_id = '1000000057005439' 162 | original_purchase_date = Time.utc(2012, 10, 11, 14, 45, 40) 163 | receipt = Itunes::Receipt.verify! 'autorenew_subscription' 164 | receipt.should be_instance_of Itunes::Receipt 165 | receipt.quantity == 1 166 | receipt.product_id.should == 'com.notkeepingitreal.fizzbuzz.subscription.autorenew1m' 167 | receipt.transaction_id.should == '1000000055076747' 168 | receipt.purchase_date.should == Time.utc(2012, 10, 13, 19, 40, 8) 169 | receipt.bid.should == 'com.notkeepingitreal.fizzbuzz' 170 | receipt.bvrs.should == '1.0' 171 | receipt.original.quantity.should be_nil 172 | receipt.original.transaction_id.should == original_transaction_id 173 | receipt.original.purchase_date.should == original_purchase_date 174 | receipt.expires_date.should == Time.utc(2012, 10, 13, 19, 45, 8) 175 | receipt.receipt_data.should be_nil 176 | 177 | # Those attributes are not returned from iTunes Connect Sandbox 178 | receipt.app_item_id.should be_nil 179 | receipt.version_external_identifier.should be_nil 180 | 181 | latest = receipt.latest 182 | latest.should be_instance_of Itunes::Receipt 183 | latest.quantity == 1 184 | latest.product_id.should == 'com.notkeepingitreal.fizzbuzz.subscription.autorenew1m' 185 | latest.transaction_id.should == '1000000052076747' 186 | latest.purchase_date.should == Time.utc(2012, 10, 13, 19, 40, 8) 187 | latest.expires_date.should == Time.utc(2012, 10, 13, 19, 50, 8) # five minutes after the "old" receipt 188 | latest.bid.should == 'com.notkeepingitreal.fizzbuzz' 189 | latest.bvrs.should == '1.0' 190 | latest.original.quantity.should be_nil 191 | latest.original.transaction_id.should == original_transaction_id 192 | latest.original.purchase_date.should == original_purchase_date 193 | latest.receipt_data.should == 'junk=' 194 | 195 | # Those attributes are not returned from iTunes Connect Sandbox 196 | latest.app_item_id.should be_nil 197 | latest.version_external_identifier.should be_nil 198 | end 199 | end 200 | 201 | context 'when expired autorenew subscription' do 202 | before do 203 | fake_json :autorenew_subscription_expired 204 | end 205 | 206 | it 'should raise ExpiredReceiptReceived exception' do 207 | expect do 208 | Itunes::Receipt.verify! 'autorenew_subscription_expired' 209 | end.to raise_error Itunes::Receipt::ExpiredReceiptReceived do |e| 210 | e.receipt.should_not be_nil 211 | end 212 | end 213 | 214 | end 215 | 216 | context 'when offline' do 217 | before do 218 | fake_json :offline 219 | end 220 | 221 | it 'should raise ReceiptServerOffline exception' do 222 | expect do 223 | Itunes::Receipt.verify! 'offline' 224 | end.to raise_error Itunes::Receipt::ReceiptServerOffline 225 | end 226 | end 227 | 228 | describe '#latest' do 229 | let(:receipt) { Itunes::Receipt.verify! 'receipt-data' } 230 | subject { receipt.latest } 231 | 232 | context 'when latest_receipt_info is a Hash' do 233 | before do 234 | fake_json :autorenew_subscription 235 | end 236 | it { should be_a Itunes::Receipt } 237 | end 238 | 239 | context 'when latest_receipt_info is an Array' do 240 | before do 241 | fake_json :array_of_latest_receipt_info 242 | end 243 | it { should be_a Array } 244 | it 'should include only Itunes::Receipt' do 245 | receipt.latest.each do |element| 246 | element.should be_a Itunes::Receipt 247 | end 248 | end 249 | end 250 | end 251 | end 252 | end 253 | --------------------------------------------------------------------------------