candy_check 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ce2db5208c39a8d9cda9eb6308fae0ae95b34460a053036b16cf2496c5fdffb
4
- data.tar.gz: 1eba58dfa69e6c8732ae1f2e33553d2386e85653c5ba867603253d531c2e0891
3
+ metadata.gz: 417a8850353b9726251dcae68864b46f77d30de0e73db9488c68c52282a90e70
4
+ data.tar.gz: 5dbcba7f14f6c81d73a0baa7554c01f0e53cb7167e0ff22029a6ddd8deac6b23
5
5
  SHA512:
6
- metadata.gz: 0f58665b4df59f402b0e43f6f9a09a74b40caf8ab7fd6511732b7ff742b35f8a1a32f53821a777a423effdc386757aaec88965aa1a63686ca463b9e98f7fc54d
7
- data.tar.gz: 245dad3d70c0eccc33c992a5420f2ac2ddb8521a524a06b0361418516d35ee9471f71c12bff4771ce6a00390ba16858d12b836060e463524cd2c3b0310ab59d8
6
+ metadata.gz: 6777436feed72baacb24e07634fa921f9723c37f207f8c9ab373aa68b007f326236877ecff04a32001ee046c0c5200d033218939e59b5af429e2b65d3e15504c
7
+ data.tar.gz: bc28443c9277dbad870f04fcc85a4c92705c7ce72d4a88b9a4ab9c882a5e90e7281a3ef13a77e1f12866e0c77b3896d8ea6be542184041a16d3eb15d16758917
@@ -1 +1 @@
1
- 2.5.1
1
+ 2.7.1
data/README.md CHANGED
@@ -150,6 +150,34 @@ result = verifier.verify_subscription_purchase(
150
150
 
151
151
  Please see documenation for [`CandyCheck::PlayStore::SubscriptionPurchases::SubscriptionPurchase`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/PlayStore/SubscriptionPurchases/SubscriptionPurchase) for further details.
152
152
 
153
+
154
+ ##### Building an acknowledger
155
+
156
+ With the `authorization` object in place, we can build an acknowledger:
157
+
158
+ ```ruby
159
+ acknowledger = CandyCheck::PlayStore::Acknowledger.new(authorization: authorization)
160
+ ```
161
+
162
+ > **Note:** If you need to acknowledge against multiple Google Service Accounts, just instantiate a new acknowledger with another authorization object that got build with a different `.json` key file.
163
+
164
+ #### Acknowledging the purchase
165
+ Google Play purchases if not acknowledged are automatically refunded. The acknowledgement can be done client-side or server-side. If server-side validation is being used we recommend to proceed with the acknowledgement afterwards, since it is the only save way to ensure the users received what they have paid for.
166
+
167
+ ```ruby
168
+ result = acknowledger.acknowledge_product_purchase(
169
+ package_name: "my-package-name",
170
+ product_id: "my-subscription-id",
171
+ token: "my-token"
172
+ )
173
+ # => ProductAcknowledgements::Response
174
+ ```
175
+
176
+ The acknowledger response has 2 methods:
177
+
178
+ - `#acknowledged?` returns a boolean. It returns true only if it has been acknowledged at this moment (that's Google Play behaviour).
179
+ - `#error` returns a hash with `status_code` and `body` keys.
180
+
153
181
  ## CLI
154
182
 
155
183
  This gem ships with an executable to verify in-app purchases directly from your terminal:
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.required_ruby_version = Gem::Requirement.new(">= 2.4")
22
22
 
23
- spec.add_dependency "google-api-client", "~> 0.34.0"
23
+ spec.add_dependency "google-api-client", "~> 0.43.0"
24
24
  spec.add_dependency "multi_json", "~> 1.10"
25
25
  spec.add_dependency "thor", "~> 0.19"
26
26
 
@@ -4,9 +4,12 @@ require "candy_check/play_store/android_publisher_service"
4
4
  require "candy_check/play_store/product_purchases/product_purchase"
5
5
  require "candy_check/play_store/subscription_purchases/subscription_purchase"
6
6
  require "candy_check/play_store/product_purchases/product_verification"
7
+ require "candy_check/play_store/product_acknowledgements/acknowledgement"
8
+ require "candy_check/play_store/product_acknowledgements/response"
7
9
  require "candy_check/play_store/subscription_purchases/subscription_verification"
8
10
  require "candy_check/play_store/verification_failure"
9
11
  require "candy_check/play_store/verifier"
12
+ require "candy_check/play_store/acknowledger"
10
13
 
11
14
  module CandyCheck
12
15
  # Module to request and verify a AppStore receipt
@@ -0,0 +1,19 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ class Acknowledger
4
+ def initialize(authorization:)
5
+ @authorization = authorization
6
+ end
7
+
8
+ def acknowledge_product_purchase(package_name:, product_id:, token:)
9
+ acknowledger = CandyCheck::PlayStore::ProductAcknowledgements::Acknowledgement.new(
10
+ package_name: package_name,
11
+ product_id: product_id,
12
+ token: token,
13
+ authorization: @authorization,
14
+ )
15
+ acknowledger.call!
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ module ProductAcknowledgements
4
+ # Verifies a purchase token against the PlayStore API
5
+
6
+ class Acknowledgement
7
+ # @return [String] the package_name which will be queried
8
+ attr_reader :package_name
9
+ # @return [String] the item id which will be queried
10
+ attr_reader :product_id
11
+ # @return [String] the token for authentication
12
+ attr_reader :token
13
+
14
+ # Initializes a new call to the API
15
+ # @param package_name [String]
16
+ # @param product_id [String]
17
+ # @param token [String]
18
+ def initialize(package_name:, product_id:, token:, authorization:)
19
+ @package_name = package_name
20
+ @product_id = product_id
21
+ @token = token
22
+ @authorization = authorization
23
+ end
24
+
25
+ def call!
26
+ acknowledge!
27
+
28
+ CandyCheck::PlayStore::ProductAcknowledgements::Response.new(
29
+ result: @response[:result], error_data: @response[:error_data])
30
+ end
31
+
32
+ private
33
+
34
+ def acknowledge!
35
+ service = CandyCheck::PlayStore::AndroidPublisherService.new
36
+
37
+ service.authorization = @authorization
38
+ service.acknowledge_purchase_product(package_name, product_id, token) do |result, error_data|
39
+ @response = { result: result, error_data: error_data }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ module ProductAcknowledgements
4
+ class Response
5
+ def initialize(result:, error_data:)
6
+ @result = result
7
+ @error_data = error_data
8
+ end
9
+
10
+ def acknowledged?
11
+ !!result
12
+ end
13
+
14
+ def error
15
+ return unless error_data
16
+
17
+ { status_code: error_data.status_code, body: error_data.body }
18
+ end
19
+
20
+ attr_reader :result, :error_data
21
+ end
22
+ end
23
+ end
24
+ end
@@ -124,6 +124,13 @@ module CandyCheck
124
124
  @subscription_purchase.expiry_time_millis
125
125
  end
126
126
 
127
+ # Get cancellation time for subscription in milliseconds since Epoch.
128
+ # Only present if cancelReason is 0.
129
+ # @return [Integer]
130
+ def user_cancellation_time_millis
131
+ @subscription_purchase.user_cancellation_time_millis if canceled_by_user?
132
+ end
133
+
127
134
  # Get start time in UTC
128
135
  # @return [DateTime]
129
136
  def starts_at
@@ -135,6 +142,12 @@ module CandyCheck
135
142
  def expires_at
136
143
  Time.at(expiry_time_millis / 1000).utc.to_datetime
137
144
  end
145
+
146
+ # Get cancellation time in UTC
147
+ # @return [DateTime]
148
+ def canceled_at
149
+ Time.at(user_cancellation_time_millis / 1000).utc.to_datetime if user_cancellation_time_millis
150
+ end
138
151
  end
139
152
  end
140
153
  end
@@ -1,4 +1,4 @@
1
1
  module CandyCheck
2
2
  # The current gem's version
3
- VERSION = "0.2.1".freeze
3
+ VERSION = "0.3.0".freeze
4
4
  end
@@ -0,0 +1,105 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://www.googleapis.com/oauth2/v4/token
6
+ body:
7
+ encoding: ASCII-8BIT
8
+ string: params
9
+ headers:
10
+ User-Agent:
11
+ - Faraday v1.0.1
12
+ Content-Type:
13
+ - application/x-www-form-urlencoded
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ Accept:
17
+ - "*/*"
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Content-Type:
24
+ - application/json; charset=UTF-8
25
+ Vary:
26
+ - Origin
27
+ - Referer
28
+ - X-Origin
29
+ Date:
30
+ - Mon, 22 Jun 2020 11:38:56 GMT
31
+ Server:
32
+ - scaffolding on HTTPServer2
33
+ Cache-Control:
34
+ - private
35
+ X-Xss-Protection:
36
+ - '0'
37
+ X-Frame-Options:
38
+ - SAMEORIGIN
39
+ X-Content-Type-Options:
40
+ - nosniff
41
+ Alt-Svc:
42
+ - h3-28=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443";
43
+ ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443";
44
+ ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
45
+ ma=2592000; v="46,43"
46
+ Transfer-Encoding:
47
+ - chunked
48
+ body:
49
+ encoding: ASCII-8BIT
50
+ string: '{"access_token":"access_token","expires_in":3599,"token_type":"Bearer"}'
51
+ recorded_at: Mon, 22 Jun 2020 11:38:56 GMT
52
+ - request:
53
+ method: post
54
+ uri: https://www.googleapis.com/androidpublisher/v3/applications/fake_package_name/purchases/products/fake_product_id/tokens/fake_token:acknowledge
55
+ body:
56
+ encoding: UTF-8
57
+ string: ''
58
+ headers:
59
+ User-Agent:
60
+ - unknown/0.0.0 google-api-ruby-client/0.34.1 Mac OS X/10.14.6 (gzip)
61
+ Accept:
62
+ - "*/*"
63
+ Accept-Encoding:
64
+ - gzip,deflate
65
+ Date:
66
+ - Mon, 22 Jun 2020 11:38:56 GMT
67
+ X-Goog-Api-Client:
68
+ - gl-ruby/2.5.1 gdcl/0.34.1
69
+ Authorization:
70
+ - Bearer some_token
71
+ Content-Type:
72
+ - application/x-www-form-urlencoded
73
+ response:
74
+ status:
75
+ code: 204
76
+ message: No Content
77
+ headers:
78
+ Content-Type:
79
+ - application/json; charset=UTF-8
80
+ Vary:
81
+ - Origin
82
+ - Referer
83
+ - X-Origin
84
+ Date:
85
+ - Mon, 22 Jun 2020 11:38:57 GMT
86
+ Server:
87
+ - ESF
88
+ Content-Length:
89
+ - '0'
90
+ X-Xss-Protection:
91
+ - '0'
92
+ X-Frame-Options:
93
+ - SAMEORIGIN
94
+ X-Content-Type-Options:
95
+ - nosniff
96
+ Alt-Svc:
97
+ - h3-28=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443";
98
+ ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443";
99
+ ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
100
+ ma=2592000; v="46,43"
101
+ body:
102
+ encoding: UTF-8
103
+ string: ''
104
+ recorded_at: Mon, 22 Jun 2020 11:38:57 GMT
105
+ recorded_with: VCR 6.0.0
@@ -0,0 +1,124 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://www.googleapis.com/oauth2/v4/token
6
+ body:
7
+ encoding: ASCII-8BIT
8
+ string: params
9
+ headers:
10
+ User-Agent:
11
+ - Faraday v1.0.1
12
+ Content-Type:
13
+ - application/x-www-form-urlencoded
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ Accept:
17
+ - "*/*"
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Content-Type:
24
+ - application/json; charset=UTF-8
25
+ Vary:
26
+ - Origin
27
+ - Referer
28
+ - X-Origin
29
+ Date:
30
+ - Mon, 22 Jun 2020 13:11:45 GMT
31
+ Server:
32
+ - scaffolding on HTTPServer2
33
+ Cache-Control:
34
+ - private
35
+ X-Xss-Protection:
36
+ - '0'
37
+ X-Frame-Options:
38
+ - SAMEORIGIN
39
+ X-Content-Type-Options:
40
+ - nosniff
41
+ Alt-Svc:
42
+ - h3-28=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443";
43
+ ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443";
44
+ ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
45
+ ma=2592000; v="46,43"
46
+ Transfer-Encoding:
47
+ - chunked
48
+ body:
49
+ encoding: ASCII-8BIT
50
+ string: '{"access_token":"access_token","expires_in":3599,"token_type":"Bearer"}'
51
+ recorded_at: Mon, 22 Jun 2020 13:11:45 GMT
52
+ - request:
53
+ method: post
54
+ uri: https://www.googleapis.com/androidpublisher/v3/applications/fake_package_name/purchases/products/fake_product_id/tokens/fake_token:acknowledge
55
+ body:
56
+ encoding: UTF-8
57
+ string: ''
58
+ headers:
59
+ User-Agent:
60
+ - unknown/0.0.0 google-api-ruby-client/0.34.1 Mac OS X/10.14.6 (gzip)
61
+ Accept:
62
+ - "*/*"
63
+ Accept-Encoding:
64
+ - gzip,deflate
65
+ Date:
66
+ - Mon, 22 Jun 2020 13:11:45 GMT
67
+ X-Goog-Api-Client:
68
+ - gl-ruby/2.5.1 gdcl/0.34.1
69
+ Authorization:
70
+ - Bearer some_token
71
+ Content-Type:
72
+ - application/x-www-form-urlencoded
73
+ response:
74
+ status:
75
+ code: 400
76
+ message: Bad Request
77
+ headers:
78
+ Content-Type:
79
+ - application/json; charset=UTF-8
80
+ Vary:
81
+ - Origin
82
+ - Referer
83
+ - X-Origin
84
+ Content-Encoding:
85
+ - gzip
86
+ Date:
87
+ - Mon, 22 Jun 2020 13:11:46 GMT
88
+ Server:
89
+ - ESF
90
+ Cache-Control:
91
+ - private
92
+ X-Xss-Protection:
93
+ - '0'
94
+ X-Frame-Options:
95
+ - SAMEORIGIN
96
+ X-Content-Type-Options:
97
+ - nosniff
98
+ Alt-Svc:
99
+ - h3-28=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443";
100
+ ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443";
101
+ ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
102
+ ma=2592000; v="46,43"
103
+ Transfer-Encoding:
104
+ - chunked
105
+ body:
106
+ encoding: UTF-8
107
+ string: |
108
+ {
109
+ "error": {
110
+ "code": 400,
111
+ "message": "The purchase is not in a valid state to perform the desired operation.",
112
+ "errors": [
113
+ {
114
+ "message": "The purchase is not in a valid state to perform the desired operation.",
115
+ "domain": "androidpublisher",
116
+ "reason": "invalidPurchaseState",
117
+ "location": "token",
118
+ "locationType": "parameter"
119
+ }
120
+ ]
121
+ }
122
+ }
123
+ recorded_at: Mon, 22 Jun 2020 13:11:46 GMT
124
+ recorded_with: VCR 6.0.0
@@ -0,0 +1,122 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://www.googleapis.com/oauth2/v4/token
6
+ body:
7
+ encoding: ASCII-8BIT
8
+ string: params
9
+ headers:
10
+ User-Agent:
11
+ - Faraday v1.0.1
12
+ Content-Type:
13
+ - application/x-www-form-urlencoded
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ Accept:
17
+ - "*/*"
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Content-Type:
24
+ - application/json; charset=UTF-8
25
+ Vary:
26
+ - Origin
27
+ - Referer
28
+ - X-Origin
29
+ Date:
30
+ - Mon, 22 Jun 2020 14:37:46 GMT
31
+ Server:
32
+ - scaffolding on HTTPServer2
33
+ Cache-Control:
34
+ - private
35
+ X-Xss-Protection:
36
+ - '0'
37
+ X-Frame-Options:
38
+ - SAMEORIGIN
39
+ X-Content-Type-Options:
40
+ - nosniff
41
+ Alt-Svc:
42
+ - h3-28=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443";
43
+ ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443";
44
+ ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
45
+ ma=2592000; v="46,43"
46
+ Transfer-Encoding:
47
+ - chunked
48
+ body:
49
+ encoding: ASCII-8BIT
50
+ string: '{"access_token":"access_token","expires_in":3599,"token_type":"Bearer"}'
51
+ recorded_at: Mon, 22 Jun 2020 14:37:46 GMT
52
+ - request:
53
+ method: post
54
+ uri: https://www.googleapis.com/androidpublisher/v3/applications/fake_package_name/purchases/products/fake_product_id/tokens/fake_token:acknowledge
55
+ body:
56
+ encoding: UTF-8
57
+ string: ''
58
+ headers:
59
+ User-Agent:
60
+ - unknown/0.0.0 google-api-ruby-client/0.34.1 Mac OS X/10.14.6 (gzip)
61
+ Accept:
62
+ - "*/*"
63
+ Accept-Encoding:
64
+ - gzip,deflate
65
+ Date:
66
+ - Mon, 22 Jun 2020 14:37:46 GMT
67
+ X-Goog-Api-Client:
68
+ - gl-ruby/2.5.1 gdcl/0.34.1
69
+ Authorization:
70
+ - Bearer some_token
71
+ Content-Type:
72
+ - application/x-www-form-urlencoded
73
+ response:
74
+ status:
75
+ code: 400
76
+ message: Bad Request
77
+ headers:
78
+ Content-Type:
79
+ - application/json; charset=UTF-8
80
+ Vary:
81
+ - Origin
82
+ - Referer
83
+ - X-Origin
84
+ Content-Encoding:
85
+ - gzip
86
+ Date:
87
+ - Mon, 22 Jun 2020 14:37:47 GMT
88
+ Server:
89
+ - ESF
90
+ Cache-Control:
91
+ - private
92
+ X-Xss-Protection:
93
+ - '0'
94
+ X-Frame-Options:
95
+ - SAMEORIGIN
96
+ X-Content-Type-Options:
97
+ - nosniff
98
+ Alt-Svc:
99
+ - h3-28=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-25=":443"; ma=2592000,h3-T050=":443";
100
+ ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443";
101
+ ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443";
102
+ ma=2592000; v="46,43"
103
+ Transfer-Encoding:
104
+ - chunked
105
+ body:
106
+ encoding: UTF-8
107
+ string: |
108
+ {
109
+ "error": {
110
+ "code": 400,
111
+ "message": "The product purchase is not owned by the user.",
112
+ "errors": [
113
+ {
114
+ "message": "The product purchase is not owned by the user.",
115
+ "domain": "androidpublisher",
116
+ "reason": "productNotOwnedByUser"
117
+ }
118
+ ]
119
+ }
120
+ }
121
+ recorded_at: Mon, 22 Jun 2020 14:37:47 GMT
122
+ recorded_with: VCR 6.0.0
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ describe CandyCheck::PlayStore::Acknowledger do
4
+ let(:json_key_file) { File.expand_path("../fixtures/play_store/random_dummy_key.json", __dir__) }
5
+ subject { CandyCheck::PlayStore::Acknowledger.new(authorization: authorization) }
6
+
7
+ let(:package_name) { "fake_package_name" }
8
+ let(:product_id) { "fake_product_id" }
9
+ let(:token) { "fake_token" }
10
+
11
+ let(:authorization) { CandyCheck::PlayStore.authorization(json_key_file) }
12
+
13
+ describe "#acknowledge_product_purchase" do
14
+ it "when acknowledgement succeeds" do
15
+ VCR.use_cassette("play_store/product_acknowledgements/acknowledged") do
16
+ result = subject.acknowledge_product_purchase(package_name: package_name, product_id: product_id, token: token)
17
+
18
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
19
+ result.acknowledged?.must_be_true
20
+ result.error.must_be_nil
21
+ end
22
+ end
23
+ it "when already acknowledged" do
24
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"errors\": [\n {\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"invalidPurchaseState\",\n \"location\": \"token\",\n \"locationType\": \"parameter\"\n }\n ]\n }\n}\n"
25
+
26
+ VCR.use_cassette("play_store/product_acknowledgements/already_acknowledged") do
27
+ result = subject.acknowledge_product_purchase(package_name: package_name, product_id: product_id, token: token)
28
+
29
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
30
+ result.acknowledged?.must_be_false
31
+ result.error[:body].must_equal(error_body)
32
+ result.error[:status_code].must_equal(400)
33
+ end
34
+ end
35
+ it "when it has been refunded" do
36
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The product purchase is not owned by the user.\",\n \"errors\": [\n {\n \"message\": \"The product purchase is not owned by the user.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"productNotOwnedByUser\"\n }\n ]\n }\n}\n"
37
+
38
+ VCR.use_cassette("play_store/product_acknowledgements/refunded") do
39
+ result = subject.acknowledge_product_purchase(package_name: package_name, product_id: product_id, token: token)
40
+
41
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
42
+ result.acknowledged?.must_be_false
43
+ result.error[:body].must_equal(error_body)
44
+ result.error[:status_code].must_equal(400)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ describe CandyCheck::PlayStore::ProductAcknowledgements::Acknowledgement do
4
+ subject do
5
+ CandyCheck::PlayStore::ProductAcknowledgements::Acknowledgement.new(
6
+ package_name: package_name,
7
+ product_id: product_id,
8
+ token: token,
9
+ authorization: authorization,
10
+ )
11
+ end
12
+
13
+ let(:package_name) { "fake_package_name" }
14
+ let(:product_id) { "fake_product_id" }
15
+ let(:token) { "fake_token" }
16
+ let(:json_key_file) { File.expand_path("../../fixtures/play_store/random_dummy_key.json", __dir__) }
17
+ let(:authorization) { CandyCheck::PlayStore.authorization(json_key_file) }
18
+
19
+ describe "#call!" do
20
+ it "when acknowledgement succeeds" do
21
+ VCR.use_cassette("play_store/product_acknowledgements/acknowledged") do
22
+ result = subject.call!
23
+
24
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
25
+ result.acknowledged?.must_be_true
26
+ result.error.must_be_nil
27
+ end
28
+ end
29
+ it "when already acknowledged" do
30
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"errors\": [\n {\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"invalidPurchaseState\",\n \"location\": \"token\",\n \"locationType\": \"parameter\"\n }\n ]\n }\n}\n"
31
+
32
+ VCR.use_cassette("play_store/product_acknowledgements/already_acknowledged") do
33
+ result = subject.call!
34
+
35
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
36
+ result.acknowledged?.must_be_false
37
+ result.error[:body].must_equal(error_body)
38
+ result.error[:status_code].must_equal(400)
39
+ end
40
+ end
41
+ it "when it has been refunded" do
42
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The product purchase is not owned by the user.\",\n \"errors\": [\n {\n \"message\": \"The product purchase is not owned by the user.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"productNotOwnedByUser\"\n }\n ]\n }\n}\n"
43
+
44
+ VCR.use_cassette("play_store/product_acknowledgements/refunded") do
45
+ result = subject.call!
46
+
47
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
48
+ result.acknowledged?.must_be_false
49
+ result.error[:body].must_equal(error_body)
50
+ result.error[:status_code].must_equal(400)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe CandyCheck::PlayStore::ProductAcknowledgements::Response do
4
+ subject do
5
+ CandyCheck::PlayStore::ProductAcknowledgements::Response.new(result: result, error_data: error_data)
6
+ end
7
+
8
+ describe '#acknowledged?' do
9
+ context 'when result present' do
10
+ let(:result) { '' }
11
+ let(:error_data) { nil }
12
+
13
+ it 'returns true' do
14
+ result = subject.acknowledged?
15
+
16
+ result.must_be_true
17
+ end
18
+ end
19
+
20
+ context 'when result is not present' do
21
+ let(:result) { nil }
22
+ let(:error_data) { nil }
23
+
24
+ it 'returns false' do
25
+ result = subject.acknowledged?
26
+
27
+ result.must_be_false
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '#error' do
33
+ context 'when error present' do
34
+ let(:result) { nil }
35
+ let(:error_data) do
36
+ Module.new do
37
+ def status_code
38
+ 400
39
+ end
40
+ def body
41
+ 'A String describing the issue'
42
+ end
43
+ module_function :status_code, :body
44
+ end
45
+ end
46
+
47
+ it 'returns the expected data' do
48
+ result = subject.error
49
+
50
+ result[:status_code].must_equal(400)
51
+ result[:body].must_equal('A String describing the issue')
52
+ end
53
+ end
54
+
55
+ context 'when error is not present' do
56
+ let(:result) { '' }
57
+ let(:error_data) { nil }
58
+
59
+ it 'returns false' do
60
+ result = subject.error
61
+
62
+ result.must_be_nil
63
+ end
64
+ end
65
+ end
66
+ end
@@ -109,6 +109,61 @@ describe CandyCheck::PlayStore::SubscriptionPurchases::SubscriptionPurchase do
109
109
  end
110
110
  end
111
111
 
112
+ describe "subscription cancelation by user" do
113
+ describe "when subscription is not canceled" do
114
+ let(:fake_subscription_purchase) do
115
+ FakeSubscriptionPurchase.new(
116
+ kind: "androidpublisher#subscriptionPurchase",
117
+ start_time_millis: 1459540113244,
118
+ expiry_time_millis: 1462132088610,
119
+ auto_renewing: true,
120
+ developer_payload: "payload that gets stored and returned",
121
+ payment_state: 1,
122
+ )
123
+ end
124
+
125
+ it "is not canceled?" do
126
+ subject.canceled_by_user?.must_be_false
127
+ end
128
+
129
+ it "returns nil user_cancellation_time_millis" do
130
+ subject.user_cancellation_time_millis.must_be_nil
131
+ end
132
+
133
+ it "returns nil canceled_at" do
134
+ subject.canceled_at.must_be_nil
135
+ end
136
+ end
137
+
138
+ describe "when subscription is canceled" do
139
+ let(:fake_subscription_purchase) do
140
+ FakeSubscriptionPurchase.new(
141
+ kind: "androidpublisher#subscriptionPurchase",
142
+ start_time_millis: 1459540113244,
143
+ expiry_time_millis: 1462132088610,
144
+ user_cancellation_time_millis: 1461872888000,
145
+ auto_renewing: true,
146
+ developer_payload: "payload that gets stored and returned",
147
+ cancel_reason: 0,
148
+ payment_state: 1,
149
+ )
150
+ end
151
+
152
+ it "is canceled?" do
153
+ subject.canceled_by_user?.must_be_true
154
+ end
155
+
156
+ it "returns the user_cancellation_time_millis" do
157
+ subject.user_cancellation_time_millis.must_equal 1_461_872_888_000
158
+ end
159
+
160
+ it "returns the starts_at" do
161
+ expected = DateTime.new(2016, 4, 28, 19, 48, 8)
162
+ subject.canceled_at.must_equal expected
163
+ end
164
+ end
165
+ end
166
+
112
167
  describe "expired with pending payment" do
113
168
  let(:fake_subscription_purchase) do
114
169
  FakeSubscriptionPurchase.new(
@@ -162,6 +217,7 @@ describe CandyCheck::PlayStore::SubscriptionPurchases::SubscriptionPurchase do
162
217
  :kind,
163
218
  :start_time_millis,
164
219
  :expiry_time_millis,
220
+ :user_cancellation_time_millis,
165
221
  :auto_renewing,
166
222
  :developer_payload,
167
223
  :cancel_reason,
@@ -25,6 +25,13 @@ require_relative "support/with_command"
25
25
 
26
26
  ENV["DEBUG"] && Google::APIClient.logger.level = Logger::DEBUG
27
27
 
28
+ class MiniTest::Spec
29
+ class << self
30
+ alias :context :describe
31
+ end
32
+ end
33
+
34
+
28
35
  module MiniTest
29
36
  module Assertions
30
37
  # The first parameter must be ```true```, not coercible to true.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: candy_check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Thiel
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-11-07 00:00:00.000000000 Z
12
+ date: 2020-10-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-api-client
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: 0.34.0
20
+ version: 0.43.0
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: 0.34.0
27
+ version: 0.43.0
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: multi_json
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -302,7 +302,10 @@ files:
302
302
  - lib/candy_check/cli/commands/version.rb
303
303
  - lib/candy_check/cli/out.rb
304
304
  - lib/candy_check/play_store.rb
305
+ - lib/candy_check/play_store/acknowledger.rb
305
306
  - lib/candy_check/play_store/android_publisher_service.rb
307
+ - lib/candy_check/play_store/product_acknowledgements/acknowledgement.rb
308
+ - lib/candy_check/play_store/product_acknowledgements/response.rb
306
309
  - lib/candy_check/play_store/product_purchases/product_purchase.rb
307
310
  - lib/candy_check/play_store/product_purchases/product_verification.rb
308
311
  - lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb
@@ -328,11 +331,17 @@ files:
328
331
  - spec/cli/commands/version_spec.rb
329
332
  - spec/cli/out_spec.rb
330
333
  - spec/fixtures/play_store/random_dummy_key.json
334
+ - spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml
335
+ - spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml
336
+ - spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml
331
337
  - spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml
332
338
  - spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml
333
339
  - spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml
334
340
  - spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml
335
341
  - spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml
342
+ - spec/play_store/acknowledger_spec.rb
343
+ - spec/play_store/product_acknowledgements/acknowledgement_spec.rb
344
+ - spec/play_store/product_acknowledgements/response_spec.rb
336
345
  - spec/play_store/product_purchases/product_purchase_spec.rb
337
346
  - spec/play_store/product_purchases/product_verification_spec.rb
338
347
  - spec/play_store/subscription_purchases/subscription_purchase_spec.rb
@@ -362,7 +371,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
362
371
  - !ruby/object:Gem::Version
363
372
  version: '0'
364
373
  requirements: []
365
- rubygems_version: 3.0.3
374
+ rubygems_version: 3.1.2
366
375
  signing_key:
367
376
  specification_version: 4
368
377
  summary: Check and verify in-app receipts
@@ -382,11 +391,17 @@ test_files:
382
391
  - spec/cli/commands/version_spec.rb
383
392
  - spec/cli/out_spec.rb
384
393
  - spec/fixtures/play_store/random_dummy_key.json
394
+ - spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml
395
+ - spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml
396
+ - spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml
385
397
  - spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml
386
398
  - spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml
387
399
  - spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml
388
400
  - spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml
389
401
  - spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml
402
+ - spec/play_store/acknowledger_spec.rb
403
+ - spec/play_store/product_acknowledgements/acknowledgement_spec.rb
404
+ - spec/play_store/product_acknowledgements/response_spec.rb
390
405
  - spec/play_store/product_purchases/product_purchase_spec.rb
391
406
  - spec/play_store/product_purchases/product_verification_spec.rb
392
407
  - spec/play_store/subscription_purchases/subscription_purchase_spec.rb