├── 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 |
--------------------------------------------------------------------------------