venice 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -95,7 +95,11 @@ module Venice
95
95
  raise TimeoutError
96
96
  end
97
97
 
98
- JSON.parse(response.body)
98
+ begin
99
+ JSON.parse(response.body)
100
+ rescue JSON::ParserError
101
+ raise InvalidResponseError
102
+ end
99
103
  end
100
104
  end
101
105
 
@@ -104,4 +108,10 @@ module Venice
104
108
  'The App Store timed out.'
105
109
  end
106
110
  end
111
+
112
+ class Client::InvalidResponseError < StandardError
113
+ def message
114
+ 'The App Store returned invalid response'
115
+ end
116
+ end
107
117
  end
@@ -5,6 +5,9 @@ module Venice
5
5
  # For detailed explanations on these keys/values, see
6
6
  # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW12
7
7
 
8
+ # Original JSON data returned from Apple for an InAppReceipt object.
9
+ attr_reader :original_json_data
10
+
8
11
  # The number of items purchased. This value corresponds to the quantity property of
9
12
  # the SKPayment object stored in the transaction’s payment property.
10
13
  attr_reader :quantity
@@ -44,9 +47,18 @@ module Venice
44
47
  attr_reader :expires_at
45
48
 
46
49
  # For a transaction that was canceled by Apple customer support, the time and date of the cancellation.
50
+ # For an auto-renewable subscription plan that was upgraded, the time and date of the upgrade transaction.
47
51
  attr_reader :cancellation_at
48
52
 
53
+ # Only present for auto-renewable subscription receipts. Value is true if the customer’s subscription is
54
+ # currently in the free trial period, false if not, nil if key is not present on receipt.
55
+ attr_reader :is_trial_period
56
+ # Only present for auto-renewable subscription receipts. Value is true if the customer’s subscription is
57
+ # currently in an introductory price period, false if not, nil if key is not present on receipt.
58
+ attr_reader :is_in_intro_offer_period
59
+
49
60
  def initialize(attributes = {})
61
+ @original_json_data = attributes
50
62
  @quantity = Integer(attributes['quantity']) if attributes['quantity']
51
63
  @product_id = attributes['product_id']
52
64
  @transaction_id = attributes['transaction_id']
@@ -54,7 +66,8 @@ module Venice
54
66
  @purchased_at = DateTime.parse(attributes['purchase_date']) if attributes['purchase_date']
55
67
  @app_item_id = attributes['app_item_id']
56
68
  @version_external_identifier = attributes['version_external_identifier']
57
- @is_trial_period = attributes['is_trial_period'].to_s == 'true'
69
+ @is_trial_period = attributes['is_trial_period'].to_s == 'true' if attributes['is_trial_period']
70
+ @is_in_intro_offer_period = attributes['is_in_intro_offer_period'] == 'true' if attributes['is_in_intro_offer_period']
58
71
 
59
72
  # expires_date is in ms since the Epoch, Time.at expects seconds
60
73
  if attributes['expires_date_ms']
@@ -88,6 +101,7 @@ module Venice
88
101
  app_item_id: @app_item_id,
89
102
  version_external_identifier: @version_external_identifier,
90
103
  is_trial_period: @is_trial_period,
104
+ is_in_intro_offer_period: @is_in_intro_offer_period,
91
105
  expires_at: (@expires_at.httpdate rescue nil),
92
106
  cancellation_at: (@cancellation_at.httpdate rescue nil)
93
107
  }
@@ -1,5 +1,8 @@
1
1
  module Venice
2
2
  class PendingRenewalInfo
3
+ # Original JSON data returned from Apple for a PendingRenewalInfo object.
4
+ attr_reader :original_json_data
5
+
3
6
  # For an expired subscription, the reason for the subscription expiration.
4
7
  # This key is only present for a receipt containing an expired auto-renewable subscription.
5
8
  attr_reader :expiration_intent
@@ -29,6 +32,7 @@ module Venice
29
32
  attr_reader :cancellation_reason
30
33
 
31
34
  def initialize(attributes)
35
+ @original_json_data = attributes
32
36
  @expiration_intent = Integer(attributes['expiration_intent']) if attributes['expiration_intent']
33
37
  @auto_renew_status = Integer(attributes['auto_renew_status']) if attributes['auto_renew_status']
34
38
  @auto_renew_product_id = attributes['auto_renew_product_id']
@@ -2,6 +2,8 @@ require 'time'
2
2
 
3
3
  module Venice
4
4
  class Receipt
5
+ MAX_RE_VERIFY_COUNT = 3
6
+
5
7
  # For detailed explanations on these keys/values, see
6
8
  # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
7
9
 
@@ -29,6 +31,7 @@ module Venice
29
31
  attr_reader :download_id
30
32
  attr_reader :requested_at
31
33
  attr_reader :receipt_created_at
34
+ attr_reader :expiration_intent
32
35
 
33
36
  # Original json response from AppStore
34
37
  attr_reader :original_json_response
@@ -56,6 +59,7 @@ module Venice
56
59
  @download_id = attributes['download_id']
57
60
  @requested_at = DateTime.parse(attributes['request_date']) if attributes['request_date']
58
61
  @receipt_created_at = DateTime.parse(attributes['receipt_creation_date']) if attributes['receipt_creation_date']
62
+ @expiration_intent = Integer(original_json_response['expiration_intent']) if original_json_response['expiration_intent']
59
63
 
60
64
  @in_app = []
61
65
  if attributes['in_app']
@@ -105,6 +109,7 @@ module Venice
105
109
  def verify!(data, options = {})
106
110
  client = Client.production
107
111
 
112
+ retry_count = 0
108
113
  begin
109
114
  client.verify!(data, options)
110
115
  rescue VerificationError => error
@@ -116,8 +121,20 @@ module Venice
116
121
  client = Client.production
117
122
  retry
118
123
  else
124
+ retry_count += 1
125
+ if error.retryable? && retry_count <= MAX_RE_VERIFY_COUNT
126
+ retry
127
+ end
128
+
119
129
  raise error
120
130
  end
131
+ rescue Net::ReadTimeout, Timeout::Error, OpenSSL::SSL::SSLError,
132
+ Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
133
+ # verifyReceipt is idempotent so we can retry it.
134
+ # Net::Http has retry logic for some idempotent http methods but it verifyReceipt is POST.
135
+ retry_count += 1
136
+ retry if retry_count <= MAX_RE_VERIFY_COUNT
137
+ raise
121
138
  end
122
139
  end
123
140
 
@@ -1,3 +1,3 @@
1
1
  module Venice
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'
3
3
  end
@@ -0,0 +1,56 @@
1
+ {
2
+ "auto_renew_status": 0,
3
+ "latest_expired_receipt_info": {
4
+ "original_purchase_date_pst": "2019-02-09 22:49:48 America/Los_Angeles",
5
+ "quantity": "1",
6
+ "unique_vendor_identifier": "88DE8B5A-0B5C-48F6-A014-CBA8A07858BC",
7
+ "bvrs": "13",
8
+ "expires_date_formatted": "2019-02-17 06:49:48 Etc/GMT",
9
+ "is_in_intro_offer_period": "false",
10
+ "purchase_date_ms": "1549781388000",
11
+ "expires_date_formatted_pst": "2019-02-16 22:49:48 America/Los_Angeles",
12
+ "is_trial_period": "true",
13
+ "item_id": "1450637621",
14
+ "unique_identifier": "00008020-001624500AF2002E",
15
+ "original_transaction_id": "140000527258824",
16
+ "expires_date": "1550386188000",
17
+ "app_item_id": "1387514950",
18
+ "transaction_id": "140000527258824",
19
+ "web_order_line_item_id": "140000151774411",
20
+ "bid": "com.foo.bar",
21
+ "product_id": "com.foo.product1",
22
+ "purchase_date": "2019-02-10 06:49:48 Etc/GMT",
23
+ "original_purchase_date": "2019-02-10 06:49:48 Etc/GMT",
24
+ "purchase_date_pst": "2019-02-09 22:49:48 America/Los_Angeles",
25
+ "original_purchase_date_ms": "1549781388000"
26
+ },
27
+ "status": 21006,
28
+ "auto_renew_product_id": "com.foo.product1",
29
+ "receipt": {
30
+ "original_purchase_date_pst": "2019-02-09 22:49:48 America/Los_Angeles",
31
+ "quantity": "1",
32
+ "unique_vendor_identifier": "88DE8B5A-0B5C-48F6-A014-CBA8A07858BC",
33
+ "bvrs": "68",
34
+ "expires_date_formatted": "2019-02-17 06:49:48 Etc/GMT",
35
+ "is_in_intro_offer_period": "false",
36
+ "purchase_date_ms": "1549781388000",
37
+ "expires_date_formatted_pst": "2019-02-16 22:49:48 America/Los_Angeles",
38
+ "is_trial_period": "true",
39
+ "item_id": "1450637621",
40
+ "unique_identifier": "00008020-001624500AF2002E",
41
+ "original_transaction_id": "140000527258824",
42
+ "expires_date": "1550386188000",
43
+ "app_item_id": "1387514950",
44
+ "transaction_id": "140000527258824",
45
+ "web_order_line_item_id": "140000151774411",
46
+ "version_external_identifier": "827235835",
47
+ "product_id": "com.foo.product1",
48
+ "purchase_date": "2019-02-10 06:49:48 Etc/GMT",
49
+ "original_purchase_date": "2019-02-10 06:49:48 Etc/GMT",
50
+ "purchase_date_pst": "2019-02-09 22:49:48 America/Los_Angeles",
51
+ "bid": "com.foo.bar",
52
+ "original_purchase_date_ms": "1549781388000"
53
+ },
54
+ "expiration_intent": "1",
55
+ "is_in_billing_retry_period": "0"
56
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "status": 0,
3
+ "environment": "Production",
4
+ "receipt": {
5
+ "receipt_type": "Production",
6
+ "adam_id": 7654321,
7
+ "bundle_id": "com.foo.bar",
8
+ "application_version": "2",
9
+ "download_id": 1234567,
10
+ "receipt_creation_date": "2014-06-04 23:20:47 Etc/GMT",
11
+ "receipt_creation_date_ms": "1401924047883",
12
+ "receipt_creation_date_pst": "2014-06-04 16:20:47 America/Los_Angeles",
13
+ "request_date": "2014-06-04 23:20:47 Etc/GMT",
14
+ "request_date_ms": "1401924047883",
15
+ "request_date_pst": "2014-06-04 16:20:47 America/Los_Angeles",
16
+ "original_purchase_date": "2014-05-17 02:09:45 Etc/GMT",
17
+ "original_purchase_date_ms": "1400292585000",
18
+ "original_purchase_date_pst": "2014-05-16 19:09:45 America/Los_Angeles",
19
+ "original_application_version": "1",
20
+ "expiration_date": "1401924047883",
21
+ "in_app": [
22
+ {
23
+ "quantity": "1",
24
+ "product_id": "com.foo.product1",
25
+ "transaction_id": "1000000070107111",
26
+ "original_transaction_id": "1000000061051111",
27
+ "web_order_line_item_id": "1000000026812043",
28
+ "purchase_date": "2014-05-28 14:47:53 Etc/GMT",
29
+ "purchase_date_ms": "1401288473000",
30
+ "purchase_date_pst": "2014-05-28 07:47:53 America/Los_Angeles",
31
+ "original_purchase_date": "2014-05-28 14:47:53 Etc/GMT",
32
+ "original_purchase_date_ms": "1401288473000",
33
+ "original_purchase_date_pst": "2014-05-28 07:47:53 America/Los_Angeles",
34
+ "expires_date": "2014-06-28 14:47:53 Etc/GMT",
35
+ "is_trial_period": "false"
36
+ }
37
+ ],
38
+ "original_json_response": {
39
+ "pending_renewal_info": [
40
+ {
41
+ "auto_renew_product_id": "com.foo.product1",
42
+ "original_transaction_id": "37xxxxxxxxx89",
43
+ "product_id": "com.foo.product1",
44
+ "auto_renew_status": "0",
45
+ "is_in_billing_retry_period": "0",
46
+ "expiration_intent": "1"
47
+ }
48
+ ]
49
+ }
50
+ }
51
+ }
@@ -16,6 +16,7 @@ describe Venice::InAppReceipt do
16
16
  'original_purchase_date_ms' => '1401288473000',
17
17
  'original_purchase_date_pst' => '2014-05-28 07:47:53 America/Los_Angeles',
18
18
  'is_trial_period' => 'false',
19
+ 'is_in_intro_offer_period' => 'true',
19
20
  'version_external_identifier' => '123',
20
21
  'app_item_id' => 'com.foo.app1',
21
22
  'expires_date' => '2014-06-28 07:47:53 America/Los_Angeles',
@@ -35,8 +36,10 @@ describe Venice::InAppReceipt do
35
36
  its(:app_item_id) { 'com.foo.app1' }
36
37
  its(:version_external_identifier) { '123' }
37
38
  its(:is_trial_period) { false }
39
+ its(:is_in_intro_offer_period) { true }
38
40
  its(:original) { should be_instance_of Venice::InAppReceipt }
39
41
  its(:expires_at) { should be_instance_of Time }
42
+ its(:original_json_data) { attributes }
40
43
 
41
44
  it "should parse the 'original' attributes" do
42
45
  subject.original.should be_instance_of Venice::InAppReceipt
@@ -56,6 +59,7 @@ describe Venice::InAppReceipt do
56
59
  transaction_id: '1000000070107235',
57
60
  web_order_line_item_id: '1000000026812043',
58
61
  is_trial_period: false,
62
+ is_in_intro_offer_period: true,
59
63
  purchase_date: 'Wed, 28 May 2014 14:47:53 GMT',
60
64
  original_purchase_date: 'Wed, 28 May 2014 14:47:53 GMT')
61
65
  end
@@ -23,6 +23,7 @@ describe Venice::PendingRenewalInfo do
23
23
  expect(subject.auto_renew_product_id).to eql('com.foo.product1')
24
24
  expect(subject.is_in_billing_retry_period).to eql(false)
25
25
  expect(subject.product_id).to eql('com.foo.product1')
26
+ expect(subject.original_json_data).to eql(attributes)
26
27
  end
27
28
 
28
29
  it 'outputs attributes in hash' do
@@ -2,59 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Venice::Receipt do
4
4
  describe 'parsing the response' do
5
- let(:response) do
6
- {
7
- 'status' => 0,
8
- 'environment' => 'Production',
9
- 'receipt' => {
10
- 'receipt_type' => 'Production',
11
- 'adam_id' => 7654321,
12
- 'bundle_id' => 'com.foo.bar',
13
- 'application_version' => '2',
14
- 'download_id' => 1234567,
15
- 'receipt_creation_date' => '2014-06-04 23:20:47 Etc/GMT',
16
- 'receipt_creation_date_ms' => '1401924047883',
17
- 'receipt_creation_date_pst' => '2014-06-04 16:20:47 America/Los_Angeles',
18
- 'request_date' => '2014-06-04 23:20:47 Etc/GMT',
19
- 'request_date_ms' => '1401924047883',
20
- 'request_date_pst' => '2014-06-04 16:20:47 America/Los_Angeles',
21
- 'original_purchase_date' => '2014-05-17 02:09:45 Etc/GMT',
22
- 'original_purchase_date_ms' => '1400292585000',
23
- 'original_purchase_date_pst' => '2014-05-16 19:09:45 America/Los_Angeles',
24
- 'original_application_version' => '1',
25
- 'expiration_date' => '1401924047883',
26
- 'in_app' => [
27
- {
28
- 'quantity' => '1',
29
- 'product_id' => 'com.foo.product1',
30
- 'transaction_id' => '1000000070107111',
31
- 'original_transaction_id' => '1000000061051111',
32
- 'web_order_line_item_id' => '1000000026812043',
33
- 'purchase_date' => '2014-05-28 14:47:53 Etc/GMT',
34
- 'purchase_date_ms' => '1401288473000',
35
- 'purchase_date_pst' => '2014-05-28 07:47:53 America/Los_Angeles',
36
- 'original_purchase_date' => '2014-05-28 14:47:53 Etc/GMT',
37
- 'original_purchase_date_ms' => '1401288473000',
38
- 'original_purchase_date_pst' => '2014-05-28 07:47:53 America/Los_Angeles',
39
- 'expires_date' => '2014-06-28 14:47:53 Etc/GMT',
40
- 'is_trial_period' => 'false'
41
- }
42
- ],
43
- 'original_json_response' => {
44
- 'pending_renewal_info' => [
45
- {
46
- 'auto_renew_product_id' => 'com.foo.product1',
47
- 'original_transaction_id' => '37xxxxxxxxx89',
48
- 'product_id' => 'com.foo.product1',
49
- 'auto_renew_status' => '0',
50
- 'is_in_billing_retry_period' => '0',
51
- 'expiration_intent' => '1'
52
- }
53
- ]
54
- }
55
- }
56
- }
57
- end
5
+ let(:response) { JSON.parse(File.read('./spec/fixtures/receipt_not_expired_cancelled.json')) }
58
6
 
59
7
  subject { Venice::Receipt.new(response['receipt']) }
60
8
 
@@ -69,16 +17,119 @@ describe Venice::Receipt do
69
17
  its(:adam_id) { 7654321 }
70
18
  its(:download_id) { 1234567 }
71
19
  its(:requested_at) { should be_instance_of DateTime }
20
+ its(:expiration_intent) { nil }
21
+
22
+ context 'response is for expired cancelled receipt' do
23
+ let(:response) { JSON.parse(File.read('./spec/fixtures/receipt_expired_cancelled.json')) }
24
+
25
+ its(:expiration_intent) { 1 }
26
+ end
27
+
28
+ describe '.verify!' do
29
+ subject { described_class.verify!('asdf') }
72
30
 
73
- describe '#verify!' do
74
31
  before do
75
32
  Venice::Client.any_instance.stub(:json_response_from_verifying_data).and_return(response)
76
33
  end
77
34
 
78
- let(:receipt) { Venice::Receipt.verify('asdf') }
35
+ it 'creates the receipt' do
36
+ expect(subject).to be_an_instance_of(Venice::Receipt)
37
+ end
38
+
39
+ describe 'retrying VerificationError' do
40
+ let(:retryable_error_response) do
41
+ {
42
+ 'status' => 21000,
43
+ 'receipt' => {},
44
+ 'is_retryable' => true
45
+ }
46
+ end
47
+
48
+ let(:error_response) do
49
+ {
50
+ 'status' => 21000,
51
+ 'receipt' => {},
52
+ 'is_retryable' => false
53
+ }
54
+ end
55
+
56
+ context 'with a retryable error response' do
57
+ before do
58
+ Venice::Client.any_instance.stub(:json_response_from_verifying_data).and_return(retryable_error_response, response)
59
+ end
60
+
61
+ it 'creates the receipt' do
62
+ expect(subject).to be_an_instance_of(Venice::Receipt)
63
+ end
64
+ end
65
+
66
+ context 'with 4 retryable error responses' do
67
+ before do
68
+ Venice::Client.any_instance.stub(:json_response_from_verifying_data).and_return(
69
+ retryable_error_response,
70
+ retryable_error_response,
71
+ retryable_error_response,
72
+ retryable_error_response,
73
+ response
74
+ )
75
+ end
76
+
77
+ it { expect { subject }.to raise_error(Venice::Receipt::VerificationError) }
78
+ end
79
+
80
+ context 'with a not retryable error response' do
81
+ before do
82
+ Venice::Client.any_instance.stub(:json_response_from_verifying_data).and_return(error_response, response)
83
+ end
84
+
85
+ it { expect { subject }.to raise_error(Venice::Receipt::VerificationError) }
86
+ end
87
+ end
88
+
89
+ describe 'retrying http error' do
90
+ def stub_json_response_from_verifying_data(returns)
91
+ counter = 0
92
+ Venice::Client.any_instance.stub(:json_response_from_verifying_data) do
93
+ begin
94
+ returns[counter].call
95
+ ensure
96
+ counter += 1
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'given 3 http errors' do
102
+ before do
103
+ returns = [
104
+ -> { raise(Net::ReadTimeout) },
105
+ -> { raise(OpenSSL::SSL::SSLError) },
106
+ -> { raise(Errno::ECONNRESET) },
107
+ -> { response }
108
+ ]
109
+ stub_json_response_from_verifying_data(returns)
110
+ end
111
+
112
+ it 'creates the receipt' do
113
+ expect(subject).to be_an_instance_of(Venice::Receipt)
114
+ end
115
+ end
116
+
117
+ context 'given 4 Net::ReadTimeout' do
118
+ before do
119
+ returns = [
120
+ -> { raise(Net::ReadTimeout) },
121
+ -> { raise(Net::ReadTimeout) },
122
+ -> { raise(Net::ReadTimeout) },
123
+ -> { raise(Net::ReadTimeout) },
124
+ -> { response }
125
+ ]
126
+ stub_json_response_from_verifying_data(returns)
127
+ end
79
128
 
80
- it 'should create the receipt' do
81
- receipt.should_not be_nil
129
+ it 'raises http error' do
130
+ expect { subject }.to raise_error(Net::ReadTimeout)
131
+ end
132
+ end
82
133
  end
83
134
  end
84
135