candy_check 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/README.md +28 -0
- data/candy_check.gemspec +1 -1
- data/lib/candy_check/play_store.rb +3 -0
- data/lib/candy_check/play_store/acknowledger.rb +19 -0
- data/lib/candy_check/play_store/product_acknowledgements/acknowledgement.rb +45 -0
- data/lib/candy_check/play_store/product_acknowledgements/response.rb +24 -0
- data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +13 -0
- data/lib/candy_check/version.rb +1 -1
- data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml +105 -0
- data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml +124 -0
- data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml +122 -0
- data/spec/play_store/acknowledger_spec.rb +48 -0
- data/spec/play_store/product_acknowledgements/acknowledgement_spec.rb +54 -0
- data/spec/play_store/product_acknowledgements/response_spec.rb +66 -0
- data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +56 -0
- data/spec/spec_helper.rb +7 -0
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 417a8850353b9726251dcae68864b46f77d30de0e73db9488c68c52282a90e70
|
4
|
+
data.tar.gz: 5dbcba7f14f6c81d73a0baa7554c01f0e53cb7167e0ff22029a6ddd8deac6b23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6777436feed72baacb24e07634fa921f9723c37f207f8c9ab373aa68b007f326236877ecff04a32001ee046c0c5200d033218939e59b5af429e2b65d3e15504c
|
7
|
+
data.tar.gz: bc28443c9277dbad870f04fcc85a4c92705c7ce72d4a88b9a4ab9c882a5e90e7281a3ef13a77e1f12866e0c77b3896d8ea6be542184041a16d3eb15d16758917
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
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:
|
data/candy_check.gemspec
CHANGED
@@ -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.
|
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
|
data/lib/candy_check/version.rb
CHANGED
@@ -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,
|
data/spec/spec_helper.rb
CHANGED
@@ -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.
|
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:
|
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.
|
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.
|
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.
|
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
|