venice 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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