candy_check 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|