candy_check 0.1.0.pre → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +23 -0
- data/.ruby-version +1 -1
- data/.travis.yml +7 -8
- data/Guardfile +42 -0
- data/MIGRATION_GUIDE_0_2_0.md +141 -0
- data/README.md +86 -26
- data/Rakefile +1 -1
- data/candy_check.gemspec +33 -25
- data/lib/candy_check/app_store/receipt_collection.rb +5 -3
- data/lib/candy_check/app_store/subscription_verification.rb +25 -1
- data/lib/candy_check/app_store/verification.rb +1 -1
- data/lib/candy_check/app_store/verifier.rb +11 -11
- 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 +20 -10
- data/lib/candy_check/play_store/acknowledger.rb +19 -0
- data/lib/candy_check/play_store/android_publisher_service.rb +6 -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/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 +154 -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/utils/config.rb +5 -3
- data/lib/candy_check/version.rb +1 -1
- data/spec/app_store/receipt_collection_spec.rb +33 -0
- data/spec/app_store/subscription_verification_spec.rb +35 -2
- data/spec/app_store/verifier_spec.rb +24 -5
- 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_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/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/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/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 +237 -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 +32 -10
- metadata +175 -75
- data/lib/candy_check/play_store/client.rb +0 -126
- 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 -138
- 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 -98
- data/spec/play_store/verification_spec.rb +0 -82
@@ -1,126 +0,0 @@
|
|
1
|
-
module CandyCheck
|
2
|
-
module PlayStore
|
3
|
-
# A client which uses the official Google API SDK to authenticate
|
4
|
-
# and request product information from Google's API.
|
5
|
-
#
|
6
|
-
# @example Usage
|
7
|
-
# config = ClientConfig.new({...})
|
8
|
-
# client = Client.new(config)
|
9
|
-
# client.boot! # a single time
|
10
|
-
# client.verify('my.bundle', 'product_1', 'a-very-long-secure-token')
|
11
|
-
# # ... multiple calls from now on
|
12
|
-
# client.verify('my.bundle', 'product_1', 'another-long-token')
|
13
|
-
class Client
|
14
|
-
# Error thrown if the discovery of the API wasn't successful
|
15
|
-
class DiscoveryError < RuntimeError; end
|
16
|
-
|
17
|
-
# API endpoint
|
18
|
-
API_URL = 'https://accounts.google.com/o/oauth2/token'.freeze
|
19
|
-
# API scope for Android services
|
20
|
-
API_SCOPE = 'https://www.googleapis.com/auth/androidpublisher'.freeze
|
21
|
-
# API discovery namespace
|
22
|
-
API_DISCOVER = 'androidpublisher'.freeze
|
23
|
-
# API version
|
24
|
-
API_VERSION = 'v2'.freeze
|
25
|
-
|
26
|
-
# Initializes a client using a configuration.
|
27
|
-
# @param config [ClientConfig]
|
28
|
-
def initialize(config)
|
29
|
-
@config = config
|
30
|
-
end
|
31
|
-
|
32
|
-
# Boots a client by discovering the API's services and then authorizes
|
33
|
-
# by fetching an access token.
|
34
|
-
# If the config has a cache_file the client tries to load discovery
|
35
|
-
def boot!
|
36
|
-
@api_client = Google::APIClient.new(
|
37
|
-
application_name: config.application_name,
|
38
|
-
application_version: config.application_version
|
39
|
-
)
|
40
|
-
discover!
|
41
|
-
authorize!
|
42
|
-
end
|
43
|
-
|
44
|
-
# Calls the remote API to load the product information for a specific
|
45
|
-
# combination of parameter which should be loaded from the client.
|
46
|
-
# @param package [String] the app's package name
|
47
|
-
# @param product_id [String] the app's item id
|
48
|
-
# @param token [String] the purchase token
|
49
|
-
# @return [Hash] result of the API call
|
50
|
-
def verify(package, product_id, token)
|
51
|
-
parameters = {
|
52
|
-
'packageName' => package,
|
53
|
-
'productId' => product_id,
|
54
|
-
'token' => token
|
55
|
-
}
|
56
|
-
execute(parameters, rpc.purchases.products.get)
|
57
|
-
end
|
58
|
-
|
59
|
-
# Calls the remote API to load the product information for a specific
|
60
|
-
# combination of parameter which should be loaded from the client.
|
61
|
-
# @param package [String] the app's package name
|
62
|
-
# @param subscription_id [String] the app's item id
|
63
|
-
# @param token [String] the purchase token
|
64
|
-
# @return [Hash] result of the API call
|
65
|
-
def verify_subscription(package, subscription_id, token)
|
66
|
-
parameters = {
|
67
|
-
'packageName' => package,
|
68
|
-
'subscriptionId' => subscription_id,
|
69
|
-
'token' => token
|
70
|
-
}
|
71
|
-
execute(parameters, rpc.purchases.subscriptions.get)
|
72
|
-
end
|
73
|
-
|
74
|
-
private
|
75
|
-
|
76
|
-
attr_reader :config, :api_client, :rpc
|
77
|
-
|
78
|
-
# Execute api call through the API Client's HTTP command class
|
79
|
-
# @param parameters [hash] the parameters to send to the command
|
80
|
-
# @param api_method [Method] which api method to call
|
81
|
-
# @return [hash] the data response, as a hash
|
82
|
-
def execute(parameters, api_method)
|
83
|
-
api_client.execute(
|
84
|
-
api_method: api_method,
|
85
|
-
parameters: parameters
|
86
|
-
).data.to_hash
|
87
|
-
end
|
88
|
-
|
89
|
-
def discover!
|
90
|
-
@rpc = load_discover_dump || request_discover
|
91
|
-
validate_rpc!
|
92
|
-
write_discover_dump
|
93
|
-
end
|
94
|
-
|
95
|
-
def request_discover
|
96
|
-
api_client.discovered_api(API_DISCOVER, API_VERSION)
|
97
|
-
end
|
98
|
-
|
99
|
-
def authorize!
|
100
|
-
api_client.authorization = Signet::OAuth2::Client.new(
|
101
|
-
token_credential_uri: API_URL,
|
102
|
-
audience: API_URL,
|
103
|
-
scope: API_SCOPE,
|
104
|
-
issuer: config.issuer,
|
105
|
-
signing_key: config.api_key
|
106
|
-
)
|
107
|
-
api_client.authorization.fetch_access_token!
|
108
|
-
end
|
109
|
-
|
110
|
-
def validate_rpc!
|
111
|
-
return if rpc.purchases.products.get
|
112
|
-
raise DiscoveryError, 'Unable to get the API discovery'
|
113
|
-
rescue NoMethodError
|
114
|
-
raise DiscoveryError, 'Unable to get the API discovery'
|
115
|
-
end
|
116
|
-
|
117
|
-
def load_discover_dump
|
118
|
-
DiscoveryRepository.new(config.cache_file).load
|
119
|
-
end
|
120
|
-
|
121
|
-
def write_discover_dump
|
122
|
-
DiscoveryRepository.new(config.cache_file).save(rpc)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
module CandyCheck
|
2
|
-
module PlayStore
|
3
|
-
# Configure the usage of the official Google API SDK client
|
4
|
-
class Config < Utils::Config
|
5
|
-
# @return [String] your application name
|
6
|
-
attr_reader :application_name
|
7
|
-
# @return [String] your application's version
|
8
|
-
attr_reader :application_version
|
9
|
-
# @return [String] an optional file to cache the discovery API result
|
10
|
-
attr_reader :cache_file
|
11
|
-
# @return [String] your issuer's service account e-mail
|
12
|
-
attr_reader :issuer
|
13
|
-
# @return [String] the path to your local *.p12 certificate file
|
14
|
-
attr_reader :key_file
|
15
|
-
# @return [String] the secret to load your certificate file
|
16
|
-
attr_reader :key_secret
|
17
|
-
|
18
|
-
# Initializes a new configuration from a hash
|
19
|
-
# @param attributes [Hash]
|
20
|
-
# @example Initialize with a discovery cache file
|
21
|
-
# ClientConfig.new(
|
22
|
-
# application_name: 'YourApplication',
|
23
|
-
# application_version: '1.0',
|
24
|
-
# cache_file: 'tmp/google_api_cache',
|
25
|
-
# issuer: 'abcdefg@developer.gserviceaccount.com',
|
26
|
-
# key_file: 'local/google.p12',
|
27
|
-
# key_secret: 'notasecret'
|
28
|
-
# )
|
29
|
-
def initialize(attributes)
|
30
|
-
super
|
31
|
-
end
|
32
|
-
|
33
|
-
# @return [String] the decrypted API key from Google
|
34
|
-
def api_key
|
35
|
-
@api_key ||= begin
|
36
|
-
Google::APIClient::KeyUtils.load_from_pkcs12(key_file, key_secret)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def validate!
|
43
|
-
validates_presence(:application_name)
|
44
|
-
validates_presence(:application_version)
|
45
|
-
validates_presence(:issuer)
|
46
|
-
validates_presence(:key_file)
|
47
|
-
validates_presence(:key_secret)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
module CandyCheck
|
2
|
-
module PlayStore
|
3
|
-
# A file-based repository to cache a local copy of the Google API
|
4
|
-
# discovery as suggested by Google.
|
5
|
-
# @see https://github.com/google/google-api-ruby-client
|
6
|
-
class DiscoveryRepository
|
7
|
-
# Create a new instance bound to a single file path
|
8
|
-
# @param file_path [String] to save and load the cached copy
|
9
|
-
def initialize(file_path)
|
10
|
-
@file_path = file_path
|
11
|
-
end
|
12
|
-
|
13
|
-
# Tries to load a cached copy of the discovery API. Me be nil if
|
14
|
-
# no cached version is available
|
15
|
-
# @return [Google::APIClient::API]
|
16
|
-
def load
|
17
|
-
return unless @file_path && File.exist?(@file_path)
|
18
|
-
File.open(@file_path, 'rb') do |file|
|
19
|
-
return Marshal.load(file)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# Tries to save a local copy of the discovery API.
|
24
|
-
# @param discovery [Google::APIClient::API]
|
25
|
-
def save(discovery)
|
26
|
-
return unless @file_path && discovery
|
27
|
-
File.open(@file_path, 'wb') do |file|
|
28
|
-
Marshal.dump(discovery, file)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
module CandyCheck
|
2
|
-
module PlayStore
|
3
|
-
# Describes a successful response from the Google verification server
|
4
|
-
class Receipt
|
5
|
-
include Utils::AttributeReader
|
6
|
-
|
7
|
-
# @return [Hash] the raw attributes returned from the server
|
8
|
-
attr_reader :attributes
|
9
|
-
|
10
|
-
# Purchased product (0 is purchased, don't ask me why)
|
11
|
-
# @see https://developers.google.com/android-publisher/api-ref/purchases/products
|
12
|
-
PURCHASE_STATE_PURCHASED = 0
|
13
|
-
|
14
|
-
# A consumed product
|
15
|
-
CONSUMPTION_STATE_CONSUMED = 1
|
16
|
-
|
17
|
-
# Initializes a new instance which bases on a JSON result
|
18
|
-
# from Google API servers
|
19
|
-
# @param attributes [Hash]
|
20
|
-
def initialize(attributes)
|
21
|
-
@attributes = attributes
|
22
|
-
end
|
23
|
-
|
24
|
-
# A product may be purchased or canceled. Ensure a receipt
|
25
|
-
# is valid before granting some candy
|
26
|
-
# @return [Boolean]
|
27
|
-
def valid?
|
28
|
-
purchase_state == PURCHASE_STATE_PURCHASED
|
29
|
-
end
|
30
|
-
|
31
|
-
# A purchased product may already be consumed. In this case you
|
32
|
-
# should grant candy even if it's valid.
|
33
|
-
# @return [Boolean]
|
34
|
-
def consumed?
|
35
|
-
consumption_state == CONSUMPTION_STATE_CONSUMED
|
36
|
-
end
|
37
|
-
|
38
|
-
# The purchase state of the order. Possible values are:
|
39
|
-
# * 0: Purchased
|
40
|
-
# * 1: Cancelled
|
41
|
-
# @return [Fixnum]
|
42
|
-
def purchase_state
|
43
|
-
read_integer('purchaseState')
|
44
|
-
end
|
45
|
-
|
46
|
-
# The consumption state of the inapp product. Possible values are:
|
47
|
-
# * 0: Yet to be consumed
|
48
|
-
# * 1: Consumed
|
49
|
-
# @return [Fixnum]
|
50
|
-
def consumption_state
|
51
|
-
read_integer('consumptionState')
|
52
|
-
end
|
53
|
-
|
54
|
-
# The developer payload which was used when buying the product
|
55
|
-
# @return [String]
|
56
|
-
def developer_payload
|
57
|
-
read('developerPayload')
|
58
|
-
end
|
59
|
-
|
60
|
-
# This kind represents an inappPurchase object in the androidpublisher
|
61
|
-
# service.
|
62
|
-
# @return [String]
|
63
|
-
def kind
|
64
|
-
read('kind')
|
65
|
-
end
|
66
|
-
|
67
|
-
# The time the product was purchased, in milliseconds since the
|
68
|
-
# epoch (Jan 1, 1970)
|
69
|
-
# @return [Fixnum]
|
70
|
-
def purchase_time_millis
|
71
|
-
read_integer('purchaseTimeMillis')
|
72
|
-
end
|
73
|
-
|
74
|
-
# The date and time the product was purchased
|
75
|
-
# @return [DateTime]
|
76
|
-
def purchased_at
|
77
|
-
read_datetime_from_millis('purchaseTimeMillis')
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
@@ -1,138 +0,0 @@
|
|
1
|
-
module CandyCheck
|
2
|
-
module PlayStore
|
3
|
-
# Describes a succeful subscription validation
|
4
|
-
class Subscription
|
5
|
-
include Utils::AttributeReader
|
6
|
-
|
7
|
-
# @return [Hash] the raw attributes returned from the server
|
8
|
-
attr_reader :attributes
|
9
|
-
|
10
|
-
# The payment of the subscription is pending (paymentState)
|
11
|
-
PAYMENT_PENDING = 0
|
12
|
-
# The payment of the subscript is received (paymentState)
|
13
|
-
PAYMENT_RECEIVED = 1
|
14
|
-
# The subscription was canceled by the user (cancelReason)
|
15
|
-
PAYMENT_CANCELED = 0
|
16
|
-
# The payment failed during processing (cancelReason)
|
17
|
-
PAYMENT_FAILED = 1
|
18
|
-
|
19
|
-
# Initializes a new instance which bases on a JSON result
|
20
|
-
# from Google's servers
|
21
|
-
# @param attributes [Hash]
|
22
|
-
def initialize(attributes)
|
23
|
-
@attributes = attributes
|
24
|
-
end
|
25
|
-
|
26
|
-
# Check if the expiration date is passed
|
27
|
-
# @return [bool]
|
28
|
-
def expired?
|
29
|
-
overdue_days > 0
|
30
|
-
end
|
31
|
-
|
32
|
-
# Check if in trial. This is actually not given by Google, but we assume
|
33
|
-
# that it is a trial going on if the paid amount is 0 and
|
34
|
-
# renewal is activated.
|
35
|
-
# @return [bool]
|
36
|
-
def trial?
|
37
|
-
price_is_zero = price_amount_micros == 0
|
38
|
-
price_is_zero && payment_received?
|
39
|
-
end
|
40
|
-
|
41
|
-
# see if payment is ok
|
42
|
-
# @return [bool]
|
43
|
-
def payment_received?
|
44
|
-
payment_state == PAYMENT_RECEIVED
|
45
|
-
end
|
46
|
-
|
47
|
-
# see if payment is pending
|
48
|
-
# @return [bool]
|
49
|
-
def payment_pending?
|
50
|
-
payment_state == PAYMENT_PENDING
|
51
|
-
end
|
52
|
-
|
53
|
-
# see if payment has failed according to Google
|
54
|
-
# @return [bool]
|
55
|
-
def payment_failed?
|
56
|
-
cancel_reason == PAYMENT_FAILED
|
57
|
-
end
|
58
|
-
|
59
|
-
# see if this the user has canceled its subscription
|
60
|
-
# @return [bool]
|
61
|
-
def canceled_by_user?
|
62
|
-
cancel_reason == PAYMENT_CANCELED
|
63
|
-
end
|
64
|
-
|
65
|
-
# Get number of overdue days. If this is negative, it is not overdue.
|
66
|
-
# @return [Integer]
|
67
|
-
def overdue_days
|
68
|
-
(Date.today - expires_at.to_date).to_i
|
69
|
-
end
|
70
|
-
|
71
|
-
# Get the auto renewal status as given by Google
|
72
|
-
# @return [bool] true if renewing automatically, false otherwise
|
73
|
-
def auto_renewing?
|
74
|
-
read_bool('autoRenewing')
|
75
|
-
end
|
76
|
-
|
77
|
-
# Get the payment state as given by Google
|
78
|
-
# @return [Integer]
|
79
|
-
def payment_state
|
80
|
-
read_integer('paymentState')
|
81
|
-
end
|
82
|
-
|
83
|
-
# Get the price amount for the subscription in micros in the payd currency
|
84
|
-
# @return [Integer]
|
85
|
-
def price_amount_micros
|
86
|
-
read_integer('priceAmountMicros')
|
87
|
-
end
|
88
|
-
|
89
|
-
# Get the cancel reason, as given by Google
|
90
|
-
# @return [Integer]
|
91
|
-
def cancel_reason
|
92
|
-
read_integer('cancelReason')
|
93
|
-
end
|
94
|
-
|
95
|
-
# Get the kind of subscription as stored in the android publisher service
|
96
|
-
# @return [String]
|
97
|
-
def kind
|
98
|
-
read('kind')
|
99
|
-
end
|
100
|
-
|
101
|
-
# Get developer-specified supplemental information about the order
|
102
|
-
# @return [String]
|
103
|
-
def developer_payload
|
104
|
-
read('developerPayload')
|
105
|
-
end
|
106
|
-
|
107
|
-
# Get the currency code in ISO 4217 format, e.g. "GBP" for British pounds
|
108
|
-
# @return [String]
|
109
|
-
def price_currency_code
|
110
|
-
read('priceCurrencyCode')
|
111
|
-
end
|
112
|
-
|
113
|
-
# Get start time for subscription in milliseconds since Epoch
|
114
|
-
# @return [Integer]
|
115
|
-
def start_time_millis
|
116
|
-
read_integer('startTimeMillis')
|
117
|
-
end
|
118
|
-
|
119
|
-
# Get expiry time for subscription in milliseconds since Epoch
|
120
|
-
# @return [Integer]
|
121
|
-
def expiry_time_millis
|
122
|
-
read_integer('expiryTimeMillis')
|
123
|
-
end
|
124
|
-
|
125
|
-
# Get start time in UTC
|
126
|
-
# @return [DateTime]
|
127
|
-
def starts_at
|
128
|
-
read_datetime_from_millis('startTimeMillis')
|
129
|
-
end
|
130
|
-
|
131
|
-
# Get expiration time in UTC
|
132
|
-
# @return [DateTime]
|
133
|
-
def expires_at
|
134
|
-
read_datetime_from_millis('expiryTimeMillis')
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
module CandyCheck
|
2
|
-
module PlayStore
|
3
|
-
# Verifies a purchase token against the Google API
|
4
|
-
# The call return either an {Receipt} or an {VerificationFailure}
|
5
|
-
class SubscriptionVerification < Verification
|
6
|
-
# Performs the verification against the remote server
|
7
|
-
# @return [Subscription] if successful
|
8
|
-
# @return [VerificationFailure] otherwise
|
9
|
-
def call!
|
10
|
-
verify!
|
11
|
-
if valid?
|
12
|
-
Subscription.new(@response)
|
13
|
-
else
|
14
|
-
VerificationFailure.new(@response['error'])
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def valid?
|
21
|
-
ok_kind = @response['kind'] == 'androidpublisher#subscriptionPurchase'
|
22
|
-
@response && @response['expiryTimeMillis'] && ok_kind
|
23
|
-
end
|
24
|
-
|
25
|
-
def verify!
|
26
|
-
@response = @client.verify_subscription(package, product_id, token)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|