candy_check 0.1.2 → 0.2.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/.travis.yml +5 -12
- data/Guardfile +42 -0
- data/MIGRATION_GUIDE_0_2_0.md +141 -0
- data/README.md +49 -26
- data/candy_check.gemspec +32 -26
- data/lib/candy_check/cli/app.rb +16 -33
- data/lib/candy_check/cli/commands/play_store.rb +12 -13
- data/lib/candy_check/play_store.rb +17 -10
- data/lib/candy_check/play_store/android_publisher_service.rb +6 -0
- data/lib/candy_check/play_store/product_purchases/product_purchase.rb +90 -0
- data/lib/candy_check/play_store/product_purchases/product_verification.rb +53 -0
- data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +141 -0
- data/lib/candy_check/play_store/subscription_purchases/subscription_verification.rb +55 -0
- data/lib/candy_check/play_store/verification_failure.rb +8 -6
- data/lib/candy_check/play_store/verifier.rb +24 -49
- data/lib/candy_check/version.rb +1 -1
- data/spec/candy_check_spec.rb +2 -2
- data/spec/cli/commands/play_store_spec.rb +10 -43
- data/spec/fixtures/play_store/random_dummy_key.json +12 -0
- data/spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml +196 -0
- data/spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml +183 -0
- data/spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml +122 -0
- data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml +196 -0
- data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml +127 -0
- data/spec/play_store/product_purchases/product_purchase_spec.rb +110 -0
- data/spec/play_store/product_purchases/product_verification_spec.rb +49 -0
- data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +181 -0
- data/spec/play_store/subscription_purchases/subscription_verification_spec.rb +65 -0
- data/spec/play_store/verification_failure_spec.rb +18 -18
- data/spec/play_store/verifier_spec.rb +32 -96
- data/spec/spec_helper.rb +24 -11
- metadata +120 -47
- data/lib/candy_check/play_store/client.rb +0 -139
- data/lib/candy_check/play_store/config.rb +0 -51
- data/lib/candy_check/play_store/discovery_repository.rb +0 -33
- data/lib/candy_check/play_store/receipt.rb +0 -81
- data/lib/candy_check/play_store/subscription.rb +0 -139
- data/lib/candy_check/play_store/subscription_verification.rb +0 -30
- data/lib/candy_check/play_store/verification.rb +0 -48
- data/spec/fixtures/api_cache.dump +0 -1
- data/spec/fixtures/play_store/api_cache.dump +0 -1
- data/spec/fixtures/play_store/auth_failure.txt +0 -18
- data/spec/fixtures/play_store/auth_success.txt +0 -20
- data/spec/fixtures/play_store/discovery.txt +0 -2841
- data/spec/fixtures/play_store/dummy.p12 +0 -0
- data/spec/fixtures/play_store/empty.txt +0 -17
- data/spec/fixtures/play_store/products_failure.txt +0 -29
- data/spec/fixtures/play_store/products_success.txt +0 -22
- data/spec/play_store/client_spec.rb +0 -125
- data/spec/play_store/config_spec.rb +0 -96
- data/spec/play_store/discovery_respository_spec.rb +0 -31
- data/spec/play_store/receipt_spec.rb +0 -88
- data/spec/play_store/subscription_spec.rb +0 -138
- data/spec/play_store/subscription_verification_spec.rb +0 -97
- data/spec/play_store/verification_spec.rb +0 -81
@@ -4,17 +4,13 @@ module CandyCheck
|
|
4
4
|
# Command to verify an PlayStore purchase
|
5
5
|
class PlayStore < Base
|
6
6
|
# Prepare a verification run from the terminal
|
7
|
-
# @param
|
7
|
+
# @param package_name [String]
|
8
8
|
# @param product_id [String]
|
9
9
|
# @param token [String]
|
10
10
|
# @param options [Hash]
|
11
|
-
# @option options [String] :
|
12
|
-
|
13
|
-
|
14
|
-
# @option options [String] :application_name for the API call
|
15
|
-
# @option options [String] :application_version for the API call
|
16
|
-
def initialize(package, product_id, token, options)
|
17
|
-
@package = package
|
11
|
+
# @option options [String] :json_key_file to use for API access
|
12
|
+
def initialize(package_name, product_id, token, options)
|
13
|
+
@package = package_name
|
18
14
|
@product_id = product_id
|
19
15
|
@token = token
|
20
16
|
super(options)
|
@@ -22,17 +18,20 @@ module CandyCheck
|
|
22
18
|
|
23
19
|
# Print the result of the verification to the terminal
|
24
20
|
def run
|
25
|
-
verifier = CandyCheck::PlayStore::Verifier.new(
|
26
|
-
verifier.
|
27
|
-
|
21
|
+
verifier = CandyCheck::PlayStore::Verifier.new(authorization: authorization)
|
22
|
+
result = verifier.verify_product_purchase(
|
23
|
+
package_name: @package,
|
24
|
+
product_id: @product_id,
|
25
|
+
token: @token,
|
26
|
+
)
|
28
27
|
out.print "#{result.class}:"
|
29
28
|
out.pretty result
|
30
29
|
end
|
31
30
|
|
32
31
|
private
|
33
32
|
|
34
|
-
def
|
35
|
-
CandyCheck::PlayStore
|
33
|
+
def authorization
|
34
|
+
CandyCheck::PlayStore.authorization(options["json_key_file"])
|
36
35
|
end
|
37
36
|
end
|
38
37
|
end
|
@@ -1,17 +1,24 @@
|
|
1
|
-
require
|
1
|
+
require "google/apis/androidpublisher_v3"
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require 'candy_check/play_store/verification_failure'
|
11
|
-
require 'candy_check/play_store/verifier'
|
3
|
+
require "candy_check/play_store/android_publisher_service"
|
4
|
+
require "candy_check/play_store/product_purchases/product_purchase"
|
5
|
+
require "candy_check/play_store/subscription_purchases/subscription_purchase"
|
6
|
+
require "candy_check/play_store/product_purchases/product_verification"
|
7
|
+
require "candy_check/play_store/subscription_purchases/subscription_verification"
|
8
|
+
require "candy_check/play_store/verification_failure"
|
9
|
+
require "candy_check/play_store/verifier"
|
12
10
|
|
13
11
|
module CandyCheck
|
14
12
|
# Module to request and verify a AppStore receipt
|
15
13
|
module PlayStore
|
14
|
+
# Build an authorization object
|
15
|
+
# @param json_key_file [String]
|
16
|
+
# @return [Google::Auth::ServiceAccountCredentials]
|
17
|
+
def self.authorization(json_key_file)
|
18
|
+
Google::Auth::ServiceAccountCredentials.make_creds(
|
19
|
+
json_key_io: File.open(json_key_file),
|
20
|
+
scope: "https://www.googleapis.com/auth/androidpublisher",
|
21
|
+
)
|
22
|
+
end
|
16
23
|
end
|
17
24
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module PlayStore
|
3
|
+
module ProductPurchases
|
4
|
+
# Describes a successful response from the PlayStore verification server
|
5
|
+
class ProductPurchase
|
6
|
+
include Utils::AttributeReader
|
7
|
+
|
8
|
+
# Returns the raw ProductPurchase from google-api-client gem
|
9
|
+
# @return [Google::Apis::AndroidpublisherV3::ProductPurchase]
|
10
|
+
attr_reader :product_purchase
|
11
|
+
|
12
|
+
# Purchased product (0 is purchased, don't ask me why)
|
13
|
+
# @see https://developers.google.com/android-publisher/api-ref/purchases/products
|
14
|
+
PURCHASE_STATE_PURCHASED = 0
|
15
|
+
|
16
|
+
# A consumed product
|
17
|
+
CONSUMPTION_STATE_CONSUMED = 1
|
18
|
+
|
19
|
+
# Initializes a new instance which bases on a JSON result
|
20
|
+
# from PlayStore API servers
|
21
|
+
# @param product_purchase [Google::Apis::AndroidpublisherV3::ProductPurchase]
|
22
|
+
def initialize(product_purchase)
|
23
|
+
@product_purchase = product_purchase
|
24
|
+
end
|
25
|
+
|
26
|
+
# The purchase state of the order. Possible values are:
|
27
|
+
# * 0: Purchased
|
28
|
+
# * 1: Cancelled
|
29
|
+
# @return [Fixnum]
|
30
|
+
def purchase_state
|
31
|
+
@product_purchase.purchase_state
|
32
|
+
end
|
33
|
+
|
34
|
+
# The consumption state of the inapp product. Possible values are:
|
35
|
+
# * 0: Yet to be consumed
|
36
|
+
# * 1: Consumed
|
37
|
+
# @return [Fixnum]
|
38
|
+
def consumption_state
|
39
|
+
@product_purchase.consumption_state
|
40
|
+
end
|
41
|
+
|
42
|
+
# The developer payload which was used when buying the product
|
43
|
+
# @return [String]
|
44
|
+
def developer_payload
|
45
|
+
@product_purchase.developer_payload
|
46
|
+
end
|
47
|
+
|
48
|
+
# This kind represents an inappPurchase object in the androidpublisher
|
49
|
+
# service.
|
50
|
+
# @return [String]
|
51
|
+
def kind
|
52
|
+
@product_purchase.kind
|
53
|
+
end
|
54
|
+
|
55
|
+
# The order id
|
56
|
+
# @return [String]
|
57
|
+
def order_id
|
58
|
+
@product_purchase.order_id
|
59
|
+
end
|
60
|
+
|
61
|
+
# The time the product was purchased, in milliseconds since the
|
62
|
+
# epoch (Jan 1, 1970)
|
63
|
+
# @return [Fixnum]
|
64
|
+
def purchase_time_millis
|
65
|
+
@product_purchase.purchase_time_millis
|
66
|
+
end
|
67
|
+
|
68
|
+
# A product may be purchased or canceled. Ensure a receipt
|
69
|
+
# is valid before granting some candy
|
70
|
+
# @return [Boolean]
|
71
|
+
def valid?
|
72
|
+
purchase_state == PURCHASE_STATE_PURCHASED
|
73
|
+
end
|
74
|
+
|
75
|
+
# A purchased product may already be consumed. In this case you
|
76
|
+
# should grant candy even if it's valid.
|
77
|
+
# @return [Boolean]
|
78
|
+
def consumed?
|
79
|
+
consumption_state == CONSUMPTION_STATE_CONSUMED
|
80
|
+
end
|
81
|
+
|
82
|
+
# The date and time the product was purchased
|
83
|
+
# @return [DateTime]
|
84
|
+
def purchased_at
|
85
|
+
Time.at(purchase_time_millis / 1000).utc.to_datetime
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module PlayStore
|
3
|
+
module ProductPurchases
|
4
|
+
# Verifies a purchase token against the PlayStore API
|
5
|
+
# The call return either a {ProductPurchase} or a {VerificationFailure}
|
6
|
+
class ProductVerification
|
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
|
+
# Performs the verification against the remote server
|
26
|
+
# @return [ProductPurchase] if successful
|
27
|
+
# @return [VerificationFailure] otherwise
|
28
|
+
def call!
|
29
|
+
verify!
|
30
|
+
if valid?
|
31
|
+
CandyCheck::PlayStore::ProductPurchases::ProductPurchase.new(@response[:result])
|
32
|
+
else
|
33
|
+
CandyCheck::PlayStore::VerificationFailure.new(@response[:error])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def valid?
|
40
|
+
@response[:result] && @response[:result].purchase_state && @response[:result].consumption_state
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify!
|
44
|
+
service = CandyCheck::PlayStore::AndroidPublisherService.new
|
45
|
+
service.authorization = @authorization
|
46
|
+
service.get_purchase_product(package_name, product_id, token) do |result, error|
|
47
|
+
@response = { result: result, error: error }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module PlayStore
|
3
|
+
module SubscriptionPurchases
|
4
|
+
# Describes a successfully validated subscription
|
5
|
+
class SubscriptionPurchase
|
6
|
+
include Utils::AttributeReader
|
7
|
+
|
8
|
+
# @return [Google::Apis::AndroidpublisherV3::SubscriptionPurchase] the raw subscription purchase from google-api-client
|
9
|
+
attr_reader :subscription_purchase
|
10
|
+
|
11
|
+
# The payment of the subscription is pending (paymentState)
|
12
|
+
PAYMENT_PENDING = 0
|
13
|
+
# The payment of the subscript is received (paymentState)
|
14
|
+
PAYMENT_RECEIVED = 1
|
15
|
+
# The subscription was canceled by the user (cancelReason)
|
16
|
+
PAYMENT_CANCELED = 0
|
17
|
+
# The payment failed during processing (cancelReason)
|
18
|
+
PAYMENT_FAILED = 1
|
19
|
+
|
20
|
+
# Initializes a new instance which bases on a JSON result
|
21
|
+
# from Google's servers
|
22
|
+
# @param subscription_purchase [Google::Apis::AndroidpublisherV3::SubscriptionPurchase]
|
23
|
+
def initialize(subscription_purchase)
|
24
|
+
@subscription_purchase = subscription_purchase
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if the expiration date is passed
|
28
|
+
# @return [bool]
|
29
|
+
def expired?
|
30
|
+
overdue_days > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check if in trial. This is actually not given by Google, but we assume
|
34
|
+
# that it is a trial going on if the paid amount is 0 and
|
35
|
+
# renewal is activated.
|
36
|
+
# @return [bool]
|
37
|
+
def trial?
|
38
|
+
price_is_zero = price_amount_micros == 0
|
39
|
+
price_is_zero && payment_received?
|
40
|
+
end
|
41
|
+
|
42
|
+
# see if payment is ok
|
43
|
+
# @return [bool]
|
44
|
+
def payment_received?
|
45
|
+
payment_state == PAYMENT_RECEIVED
|
46
|
+
end
|
47
|
+
|
48
|
+
# see if payment is pending
|
49
|
+
# @return [bool]
|
50
|
+
def payment_pending?
|
51
|
+
payment_state == PAYMENT_PENDING
|
52
|
+
end
|
53
|
+
|
54
|
+
# see if payment has failed according to Google
|
55
|
+
# @return [bool]
|
56
|
+
def payment_failed?
|
57
|
+
cancel_reason == PAYMENT_FAILED
|
58
|
+
end
|
59
|
+
|
60
|
+
# see if this the user has canceled its subscription
|
61
|
+
# @return [bool]
|
62
|
+
def canceled_by_user?
|
63
|
+
cancel_reason == PAYMENT_CANCELED
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get number of overdue days. If this is negative, it is not overdue.
|
67
|
+
# @return [Integer]
|
68
|
+
def overdue_days
|
69
|
+
(Date.today - expires_at.to_date).to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the auto renewal status as given by Google
|
73
|
+
# @return [bool] true if renewing automatically, false otherwise
|
74
|
+
def auto_renewing?
|
75
|
+
@subscription_purchase.auto_renewing
|
76
|
+
end
|
77
|
+
|
78
|
+
# Get the payment state as given by Google
|
79
|
+
# @return [Integer]
|
80
|
+
def payment_state
|
81
|
+
@subscription_purchase.payment_state
|
82
|
+
end
|
83
|
+
|
84
|
+
# Get the price amount for the subscription in micros in the payed
|
85
|
+
# currency
|
86
|
+
# @return [Integer]
|
87
|
+
def price_amount_micros
|
88
|
+
@subscription_purchase.price_amount_micros
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get the cancel reason, as given by Google
|
92
|
+
# @return [Integer]
|
93
|
+
def cancel_reason
|
94
|
+
@subscription_purchase.cancel_reason
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get the kind of subscription as stored in the android publisher service
|
98
|
+
# @return [String]
|
99
|
+
def kind
|
100
|
+
@subscription_purchase.kind
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get developer-specified supplemental information about the order
|
104
|
+
# @return [String]
|
105
|
+
def developer_payload
|
106
|
+
@subscription_purchase.developer_payload
|
107
|
+
end
|
108
|
+
|
109
|
+
# Get the currency code in ISO 4217 format, e.g. "GBP" for British pounds
|
110
|
+
# @return [String]
|
111
|
+
def price_currency_code
|
112
|
+
@subscription_purchase.price_currency_code
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get start time for subscription in milliseconds since Epoch
|
116
|
+
# @return [Integer]
|
117
|
+
def start_time_millis
|
118
|
+
@subscription_purchase.start_time_millis
|
119
|
+
end
|
120
|
+
|
121
|
+
# Get expiry time for subscription in milliseconds since Epoch
|
122
|
+
# @return [Integer]
|
123
|
+
def expiry_time_millis
|
124
|
+
@subscription_purchase.expiry_time_millis
|
125
|
+
end
|
126
|
+
|
127
|
+
# Get start time in UTC
|
128
|
+
# @return [DateTime]
|
129
|
+
def starts_at
|
130
|
+
Time.at(start_time_millis / 1000).utc.to_datetime
|
131
|
+
end
|
132
|
+
|
133
|
+
# Get expiration time in UTC
|
134
|
+
# @return [DateTime]
|
135
|
+
def expires_at
|
136
|
+
Time.at(expiry_time_millis / 1000).utc.to_datetime
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module PlayStore
|
3
|
+
module SubscriptionPurchases
|
4
|
+
# Verifies a purchase token against the Google API
|
5
|
+
# The call return either an {SubscriptionPurchase} or an {VerificationFailure}
|
6
|
+
class SubscriptionVerification
|
7
|
+
# @return [String] the package which will be queried
|
8
|
+
attr_reader :package_name
|
9
|
+
# @return [String] the item id which will be queried
|
10
|
+
attr_reader :subscription_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 subscription_id [String]
|
17
|
+
# @param token [String]
|
18
|
+
def initialize(package_name:, subscription_id:, token:, authorization:)
|
19
|
+
@package_name = package_name
|
20
|
+
@subscription_id = subscription_id
|
21
|
+
@token = token
|
22
|
+
@authorization = authorization
|
23
|
+
end
|
24
|
+
|
25
|
+
# Performs the verification against the remote server
|
26
|
+
# @return [SubscriptionPurchase] if successful
|
27
|
+
# @return [VerificationFailure] otherwise
|
28
|
+
def call!
|
29
|
+
verify!
|
30
|
+
if valid?
|
31
|
+
CandyCheck::PlayStore::SubscriptionPurchases::SubscriptionPurchase.new(@response[:result])
|
32
|
+
else
|
33
|
+
CandyCheck::PlayStore::VerificationFailure.new(@response[:error])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def valid?
|
40
|
+
return false unless @response[:result]
|
41
|
+
ok_kind = @response[:result].kind == "androidpublisher#subscriptionPurchase"
|
42
|
+
@response && @response[:result].expiry_time_millis && ok_kind
|
43
|
+
end
|
44
|
+
|
45
|
+
def verify!
|
46
|
+
service = CandyCheck::PlayStore::AndroidPublisherService.new
|
47
|
+
service.authorization = @authorization
|
48
|
+
service.get_purchase_subscription(package_name, subscription_id, token) do |result, error|
|
49
|
+
@response = { result: result, error: error }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -5,25 +5,27 @@ module CandyCheck
|
|
5
5
|
include Utils::AttributeReader
|
6
6
|
|
7
7
|
# @return [Hash] the raw attributes returned from the server
|
8
|
-
attr_reader :
|
8
|
+
attr_reader :error
|
9
9
|
|
10
10
|
# Initializes a new instance which bases on a JSON result
|
11
11
|
# from Google API servers
|
12
|
-
# @param
|
13
|
-
def initialize(
|
14
|
-
@
|
12
|
+
# @param error [Hash]
|
13
|
+
def initialize(error)
|
14
|
+
@error = error
|
15
15
|
end
|
16
16
|
|
17
17
|
# The code of the failure
|
18
18
|
# @return [Fixnum]
|
19
19
|
def code
|
20
|
-
|
20
|
+
Integer(error.status_code)
|
21
|
+
rescue
|
22
|
+
-1
|
21
23
|
end
|
22
24
|
|
23
25
|
# The message of the failure
|
24
26
|
# @return [String]
|
25
27
|
def message
|
26
|
-
|
28
|
+
error.message || "Unknown error"
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|