folked-venice 0.5.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module Venice
2
+ class PendingRenewalInfo
3
+ # Original JSON data returned from Apple for a PendingRenewalInfo object.
4
+ attr_reader :original_json_data
5
+
6
+ # For an expired subscription, the reason for the subscription expiration.
7
+ # This key is only present for a receipt containing an expired auto-renewable subscription.
8
+ attr_reader :expiration_intent
9
+
10
+ # The current renewal status for the auto-renewable subscription.
11
+ # This key is only present for auto-renewable subscription receipts, for active or expired subscriptions
12
+ attr_reader :auto_renew_status
13
+
14
+ # The current renewal preference for the auto-renewable subscription.
15
+ # The value for this key corresponds to the productIdentifier property of the product that the customer’s subscription renews.
16
+ attr_reader :auto_renew_product_id
17
+
18
+ # For an expired subscription, whether or not Apple is still attempting to automatically renew the subscription.
19
+ # If the customer’s subscription failed to renew because the App Store was unable to complete the transaction, this value will reflect whether or not the App Store is still trying to renew the subscription.
20
+ attr_reader :is_in_billing_retry_period
21
+
22
+ # The product identifier of the item that was purchased.
23
+ # This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
24
+ attr_reader :product_id
25
+
26
+ # The current price consent status for a subscription price increase
27
+ # This key is only present for auto-renewable subscription receipts if the subscription price was increased without keeping the existing price for active subscribers
28
+ attr_reader :price_consent_status
29
+
30
+ # For a transaction that was cancelled, the reason for cancellation.
31
+ # Use this value along with the cancellation date to identify possible issues in your app that may lead customers to contact Apple customer support.
32
+ attr_reader :cancellation_reason
33
+
34
+ def initialize(attributes)
35
+ @original_json_data = attributes
36
+ @expiration_intent = Integer(attributes['expiration_intent']) if attributes['expiration_intent']
37
+ @auto_renew_status = Integer(attributes['auto_renew_status']) if attributes['auto_renew_status']
38
+ @auto_renew_product_id = attributes['auto_renew_product_id']
39
+
40
+ if attributes['is_in_billing_retry_period']
41
+ @is_in_billing_retry_period = (attributes['is_in_billing_retry_period'] == '1')
42
+ end
43
+
44
+ @product_id = attributes['product_id']
45
+
46
+ @price_consent_status = Integer(attributes['price_consent_status']) if attributes['price_consent_status']
47
+ @cancellation_reason = Integer(attributes['cancellation_reason']) if attributes['cancellation_reason']
48
+ end
49
+
50
+ def to_hash
51
+ {
52
+ expiration_intent: @expiration_intent,
53
+ auto_renew_status: @auto_renew_status,
54
+ auto_renew_product_id: @auto_renew_product_id,
55
+ is_in_billing_retry_period: @is_in_billing_retry_period,
56
+ product_id: @product_id,
57
+ price_consent_status: @price_consent_status,
58
+ cancellation_reason: @cancellation_reason
59
+ }
60
+ end
61
+
62
+ alias_method :to_h, :to_hash
63
+
64
+ def to_json
65
+ to_hash.to_json
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,186 @@
1
+ require 'time'
2
+
3
+ module Venice
4
+ class Receipt
5
+ MAX_RE_VERIFY_COUNT = 3
6
+
7
+ # For detailed explanations on these keys/values, see
8
+ # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
9
+
10
+ # The app’s bundle identifier.
11
+ attr_reader :bundle_id
12
+
13
+ # The app’s version number.
14
+ attr_reader :application_version
15
+
16
+ # The receipt for an in-app purchase.
17
+ attr_reader :in_app
18
+
19
+ # The version of the app that was originally purchased.
20
+ attr_reader :original_application_version
21
+
22
+ # The original purchase date
23
+ attr_reader :original_purchase_date
24
+
25
+ # The date that the app receipt expires.
26
+ attr_reader :expires_at
27
+
28
+ # Non-Documented receipt keys/values
29
+ attr_reader :receipt_type
30
+ attr_reader :adam_id
31
+ attr_reader :download_id
32
+ attr_reader :requested_at
33
+ attr_reader :receipt_created_at
34
+
35
+ # Original json response from AppStore
36
+ attr_reader :original_json_response
37
+
38
+ attr_accessor :latest_receipt_info
39
+
40
+ # Information about the status of the customer's auto-renewable subscriptions
41
+ attr_reader :pending_renewal_info
42
+
43
+ def initialize(attributes = {})
44
+ @original_json_response = attributes['original_json_response']
45
+
46
+ @bundle_id = attributes['bundle_id']
47
+ @application_version = attributes['application_version']
48
+ @original_application_version = attributes['original_application_version']
49
+ if attributes['original_purchase_date']
50
+ @original_purchase_date = DateTime.parse(attributes['original_purchase_date'])
51
+ end
52
+ if attributes['expiration_date']
53
+ @expires_at = Time.at(attributes['expiration_date'].to_i / 1000).to_datetime
54
+ end
55
+
56
+ @receipt_type = attributes['receipt_type']
57
+ @adam_id = attributes['adam_id']
58
+ @download_id = attributes['download_id']
59
+ @requested_at = DateTime.parse(attributes['request_date']) if attributes['request_date']
60
+ @receipt_created_at = DateTime.parse(attributes['receipt_creation_date']) if attributes['receipt_creation_date']
61
+
62
+ @in_app = []
63
+ if attributes['in_app']
64
+ attributes['in_app'].each do |in_app_purchase_attributes|
65
+ @in_app << InAppReceipt.new(in_app_purchase_attributes)
66
+ end
67
+ end
68
+
69
+ @pending_renewal_info = []
70
+ if original_json_response && original_json_response['pending_renewal_info']
71
+ original_json_response['pending_renewal_info'].each do |pending_renewal_attributes|
72
+ @pending_renewal_info << PendingRenewalInfo.new(pending_renewal_attributes)
73
+ end
74
+ end
75
+ end
76
+
77
+ def to_hash
78
+ {
79
+ bundle_id: @bundle_id,
80
+ application_version: @application_version,
81
+ original_application_version: @original_application_version,
82
+ original_purchase_date: (@original_purchase_date.httpdate rescue nil),
83
+ expires_at: (@expires_at.httpdate rescue nil),
84
+ receipt_type: @receipt_type,
85
+ adam_id: @adam_id,
86
+ download_id: @download_id,
87
+ requested_at: (@requested_at.httpdate rescue nil),
88
+ receipt_created_at: (@receipt_created_at.httpdate rescue nil),
89
+ in_app: @in_app.map(&:to_h),
90
+ pending_renewal_info: @pending_renewal_info.map(&:to_h),
91
+ latest_receipt_info: @latest_receipt_info
92
+ }
93
+ end
94
+ alias_method :to_h, :to_hash
95
+
96
+ def to_json
97
+ to_hash.to_json
98
+ end
99
+
100
+ class << self
101
+ def verify(data, options = {})
102
+ verify!(data, options)
103
+ rescue VerificationError, Client::TimeoutError
104
+ false
105
+ end
106
+
107
+ def verify!(data, options = {})
108
+ client = Client.production
109
+
110
+ retry_count = 0
111
+ begin
112
+ client.verify!(data, options)
113
+ rescue VerificationError => error
114
+ case error.code
115
+ when 21007
116
+ client = Client.development
117
+ retry
118
+ when 21008
119
+ client = Client.production
120
+ retry
121
+ else
122
+ retry_count += 1
123
+ if error.retryable? && retry_count <= MAX_RE_VERIFY_COUNT
124
+ retry
125
+ end
126
+
127
+ raise error
128
+ end
129
+ rescue Net::ReadTimeout, Timeout::Error, OpenSSL::SSL::SSLError,
130
+ Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
131
+ # verifyReceipt is idempotent so we can retry it.
132
+ # Net::Http has retry logic for some idempotent http methods but it verifyReceipt is POST.
133
+ retry_count += 1
134
+ retry if retry_count <= MAX_RE_VERIFY_COUNT
135
+ raise
136
+ end
137
+ end
138
+
139
+ alias :validate :verify
140
+ alias :validate! :verify!
141
+ end
142
+
143
+ class VerificationError < StandardError
144
+ attr_accessor :json
145
+
146
+ def initialize(json)
147
+ @json = json
148
+ end
149
+
150
+ def code
151
+ Integer(json['status'])
152
+ end
153
+
154
+ def retryable?
155
+ json['is_retryable']
156
+ end
157
+
158
+ def message
159
+ case code
160
+ when 21000
161
+ 'The App Store could not read the JSON object you provided.'
162
+ when 21002
163
+ 'The data in the receipt-data property was malformed.'
164
+ when 21003
165
+ 'The receipt could not be authenticated.'
166
+ when 21004
167
+ 'The shared secret you provided does not match the shared secret on file for your account.'
168
+ when 21005
169
+ 'The receipt server is not currently available.'
170
+ when 21006
171
+ 'This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.'
172
+ when 21007
173
+ 'This receipt is a sandbox receipt, but it was sent to the production service for verification.'
174
+ when 21008
175
+ 'This receipt is a production receipt, but it was sent to the sandbox service for verification.'
176
+ when 21010
177
+ 'This receipt could not be authorized. Treat this the same as if a purchase was never made.'
178
+ when 21100..21199
179
+ 'Internal data access error.'
180
+ else
181
+ "Unknown Error: #{code}"
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,3 @@
1
+ module Venice
2
+ VERSION = '0.5.0'
3
+ end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+
3
+ describe Venice::Client do
4
+ let(:receipt_data) { 'asdfzxcvjklqwer' }
5
+ let(:client) { subject }
6
+
7
+ describe '#verify!' do
8
+ context 'with a receipt response' do
9
+ before do
10
+ client.stub(:json_response_from_verifying_data).and_return(response)
11
+ end
12
+
13
+ let(:response) do
14
+ {
15
+ 'status' => 0,
16
+ 'receipt' => {}
17
+ }
18
+ end
19
+
20
+ it 'does not generate a self-referencing Hash' do
21
+ receipt = client.verify! 'asdf'
22
+ expect(receipt.original_json_response['receipt']).not_to have_key('original_json_response')
23
+ end
24
+ end
25
+
26
+ context 'no shared_secret' do
27
+ before do
28
+ client.shared_secret = nil
29
+ Venice::Receipt.stub :new
30
+ end
31
+
32
+ it 'should only include the receipt_data' do
33
+ Net::HTTP.any_instance.should_receive(:request) do |post|
34
+ post.body.should eq({ 'receipt-data' => receipt_data }.to_json)
35
+ post
36
+ end
37
+ client.verify! receipt_data
38
+ end
39
+ end
40
+
41
+ context 'with a shared secret' do
42
+ let(:secret) { 'shhhhhh' }
43
+
44
+ before do
45
+ Venice::Receipt.stub :new
46
+ end
47
+
48
+ context 'set secret manually' do
49
+ before do
50
+ client.shared_secret = secret
51
+ end
52
+
53
+ it 'should include the secret in the post' do
54
+ Net::HTTP.any_instance.should_receive(:request) do |post|
55
+ post.body.should eq({ 'receipt-data' => receipt_data, 'password' => secret }.to_json)
56
+ post
57
+ end
58
+ client.verify! receipt_data
59
+ end
60
+ end
61
+
62
+ context 'set secret when verification' do
63
+ let(:options) { { shared_secret: secret } }
64
+
65
+ it 'should include the secret in the post' do
66
+ Net::HTTP.any_instance.should_receive(:request) do |post|
67
+ post.body.should eq({ 'receipt-data' => receipt_data, 'password' => secret }.to_json)
68
+ post
69
+ end
70
+ client.verify! receipt_data, options
71
+ end
72
+ end
73
+ end
74
+
75
+ context 'with a latest receipt info attribute' do
76
+ let(:response) do
77
+ {
78
+ 'status' => 0,
79
+ 'receipt' => {},
80
+ 'latest_receipt' => '<encoded string>',
81
+ 'latest_receipt_info' => [
82
+ {
83
+ 'original_purchase_date_pst' => '2012-12-30 09:39:24 America/Los_Angeles',
84
+ 'unique_identifier' => '0000b01147b8',
85
+ 'original_transaction_id' => '1000000061051565',
86
+ 'expires_date' => '1365114731000',
87
+ 'transaction_id' => '1000000070104252',
88
+ 'quantity' => '1',
89
+ 'product_id' => 'com.ficklebits.nsscreencast.monthly_sub',
90
+ 'original_purchase_date_ms' => '1356889164000',
91
+ 'bid' => 'com.ficklebits.nsscreencast',
92
+ 'web_order_line_item_id' => '1000000026812043',
93
+ 'bvrs' => '0.1',
94
+ 'expires_date_formatted' => '2013-04-04 22:32:11 Etc/GMT',
95
+ 'purchase_date' => '2013-04-04 22:27:11 Etc/GMT',
96
+ 'purchase_date_ms' => '1365114431000',
97
+ 'expires_date_formatted_pst' => '2013-04-04 15:32:11 America/Los_Angeles',
98
+ 'purchase_date_pst' => '2013-04-04 15:27:11 America/Los_Angeles',
99
+ 'original_purchase_date' => '2012-12-30 17:39:24 Etc/GMT',
100
+ 'item_id' => '590265423'
101
+ }
102
+ ]
103
+ }
104
+ end
105
+
106
+ it 'should create a latest receipt' do
107
+ client.stub(:json_response_from_verifying_data).and_return(response)
108
+ receipt = client.verify! 'asdf'
109
+ receipt.latest_receipt_info.should_not be_nil
110
+ receipt.latest_receipt_info.first.product_id.should eq 'com.ficklebits.nsscreencast.monthly_sub'
111
+ end
112
+
113
+ context 'when latest_receipt_info is a hash instead of an array' do
114
+ it 'should still create a latest receipt' do
115
+ response['latest_receipt_info'] = response['latest_receipt_info'].first
116
+ client.stub(:json_response_from_verifying_data).and_return(response)
117
+ receipt = client.verify! 'asdf'
118
+ receipt.latest_receipt_info.should_not be_nil
119
+ receipt.latest_receipt_info.first.product_id.should eq 'com.ficklebits.nsscreencast.monthly_sub'
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'with an error response' do
125
+ before do
126
+ client.stub(:json_response_from_verifying_data).and_return(response)
127
+ end
128
+
129
+ let(:response) do
130
+ {
131
+ 'status' => 21000,
132
+ 'receipt' => {}
133
+ }
134
+ end
135
+
136
+ it 'raises a VerificationError' do
137
+ expect do
138
+ client.verify!('asdf')
139
+ end.to raise_error(Venice::Receipt::VerificationError) do |error|
140
+ expect(error.json).to eq(response)
141
+ expect(error.code).to eq(21000)
142
+ expect(error).not_to be_retryable
143
+ end
144
+ end
145
+ end
146
+
147
+ context 'with a retryable error response' do
148
+ before do
149
+ client.stub(:json_response_from_verifying_data).and_return(response)
150
+ end
151
+
152
+ let(:response) do
153
+ {
154
+ 'status' => 21000,
155
+ 'receipt' => {},
156
+ 'is_retryable' => true
157
+ }
158
+ end
159
+
160
+ it 'raises a VerificationError' do
161
+ expect do
162
+ client.verify!('asdf')
163
+ end.to raise_error(Venice::Receipt::VerificationError) do |error|
164
+ expect(error).to be_retryable
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Venice::InAppReceipt do
4
+ describe '.new' do
5
+ let :attributes do
6
+ {
7
+ 'quantity' => 1,
8
+ 'product_id' => 'com.foo.product1',
9
+ 'transaction_id' => '1000000070107235',
10
+ 'web_order_line_item_id' => '1000000026812043',
11
+ 'purchase_date' => '2014-05-28 14:47:53 Etc/GMT',
12
+ 'purchase_date_ms' => '1401288473000',
13
+ 'purchase_date_pst' => '2014-05-28 07:47:53 America/Los_Angeles',
14
+ 'original_transaction_id' => '140xxx867509',
15
+ 'original_purchase_date' => '2014-05-28 14:47:53 Etc/GMT',
16
+ 'original_purchase_date_ms' => '1401288473000',
17
+ 'original_purchase_date_pst' => '2014-05-28 07:47:53 America/Los_Angeles',
18
+ 'is_trial_period' => 'false',
19
+ 'is_in_intro_offer_period' => 'true',
20
+ 'version_external_identifier' => '123',
21
+ 'app_item_id' => 'com.foo.app1',
22
+ 'expires_date' => '2014-06-28 07:47:53 America/Los_Angeles',
23
+ 'expires_date_ms' => '1403941673000'
24
+ }
25
+ end
26
+
27
+ subject(:in_app_receipt) do
28
+ Venice::InAppReceipt.new attributes
29
+ end
30
+
31
+ its(:quantity) { 1 }
32
+ its(:product_id) { 'com.foo.product1' }
33
+ its(:transaction_id) { '1000000070107235' }
34
+ its(:web_order_line_item_id) { '1000000026812043' }
35
+ its(:purchased_at) { should be_instance_of DateTime }
36
+ its(:app_item_id) { 'com.foo.app1' }
37
+ its(:version_external_identifier) { '123' }
38
+ its(:is_trial_period) { false }
39
+ its(:is_in_intro_offer_period) { true }
40
+ its(:original) { should be_instance_of Venice::InAppReceipt }
41
+ its(:expires_at) { should be_instance_of Time }
42
+ its(:original_json_data) { attributes }
43
+
44
+ it "should parse the 'original' attributes" do
45
+ subject.original.should be_instance_of Venice::InAppReceipt
46
+ subject.original.transaction_id.should == '140xxx867509'
47
+ subject.original.purchased_at.should be_instance_of DateTime
48
+ end
49
+
50
+ it 'should parse expires_date when expires_date_ms is missing and expires_date is the ms epoch' do
51
+ attributes.delete('expires_date_ms')
52
+ attributes['expires_date'] = '1403941685000'
53
+ in_app_receipt.expires_at.should eq Time.at(1403941685000 / 1000)
54
+ end
55
+
56
+ it 'should output a hash with attributes' do
57
+ in_app_receipt.to_h.should include(quantity: 1,
58
+ product_id: 'com.foo.product1',
59
+ transaction_id: '1000000070107235',
60
+ web_order_line_item_id: '1000000026812043',
61
+ is_trial_period: false,
62
+ is_in_intro_offer_period: true,
63
+ purchase_date: 'Wed, 28 May 2014 14:47:53 GMT',
64
+ original_purchase_date: 'Wed, 28 May 2014 14:47:53 GMT')
65
+ end
66
+ end
67
+ end