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
@@ -8,15 +8,17 @@ module CandyCheck
|
|
8
8
|
|
9
9
|
# Initializes a new instance which bases on a JSON result
|
10
10
|
# from Apple's verification server
|
11
|
-
# @param attributes [Array<Hash>]
|
11
|
+
# @param attributes [Array<Hash>] raw data from Apple's server
|
12
12
|
def initialize(attributes)
|
13
|
-
@receipts = attributes.map {
|
13
|
+
@receipts = attributes.map {|r| Receipt.new(r) }.sort{ |a, b|
|
14
|
+
a.purchase_date - b.purchase_date
|
15
|
+
}
|
14
16
|
end
|
15
17
|
|
16
18
|
# Check if the latest expiration date is passed
|
17
19
|
# @return [bool]
|
18
20
|
def expired?
|
19
|
-
|
21
|
+
expires_at.to_time <= Time.now.utc
|
20
22
|
end
|
21
23
|
|
22
24
|
# Check if in trial
|
@@ -3,13 +3,28 @@ module CandyCheck
|
|
3
3
|
# Verifies a latest_receipt_info block against a verification server.
|
4
4
|
# The call return either an {ReceiptCollection} or a {VerificationFailure}
|
5
5
|
class SubscriptionVerification < CandyCheck::AppStore::Verification
|
6
|
+
# Builds a fresh verification run
|
7
|
+
# @param endpoint_url [String] the verification URL to use
|
8
|
+
# @param receipt_data [String] the raw data to be verified
|
9
|
+
# @param secret [String] optional: shared secret
|
10
|
+
# @param product_ids [Array<String>] optional: select specific products
|
11
|
+
def initialize(
|
12
|
+
endpoint_url,
|
13
|
+
receipt_data,
|
14
|
+
secret = nil,
|
15
|
+
product_ids = nil
|
16
|
+
)
|
17
|
+
super(endpoint_url, receipt_data, secret)
|
18
|
+
@product_ids = product_ids
|
19
|
+
end
|
20
|
+
|
6
21
|
# Performs the verification against the remote server
|
7
22
|
# @return [ReceiptCollection] if successful
|
8
23
|
# @return [VerificationFailure] otherwise
|
9
24
|
def call!
|
10
25
|
verify!
|
11
26
|
if valid?
|
12
|
-
|
27
|
+
build_collection(@response['latest_receipt_info'])
|
13
28
|
else
|
14
29
|
VerificationFailure.fetch(@response['status'])
|
15
30
|
end
|
@@ -17,6 +32,15 @@ module CandyCheck
|
|
17
32
|
|
18
33
|
private
|
19
34
|
|
35
|
+
def build_collection(latest_receipt_info)
|
36
|
+
unless @product_ids.nil?
|
37
|
+
latest_receipt_info = latest_receipt_info.select do |info|
|
38
|
+
@product_ids.include?(info['product_id'])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
ReceiptCollection.new(latest_receipt_info)
|
42
|
+
end
|
43
|
+
|
20
44
|
def valid?
|
21
45
|
status_is_ok = @response['status'] == STATUS_OK
|
22
46
|
@response && status_is_ok && @response['latest_receipt_info']
|
@@ -16,7 +16,7 @@ module CandyCheck
|
|
16
16
|
# Builds a fresh verification run
|
17
17
|
# @param endpoint_url [String] the verification URL to use
|
18
18
|
# @param receipt_data [String] the raw data to be verified
|
19
|
-
# @param secret [String]
|
19
|
+
# @param secret [String] optional: shared secret
|
20
20
|
def initialize(endpoint_url, receipt_data, secret = nil)
|
21
21
|
@endpoint_url = endpoint_url
|
22
22
|
@receipt_data = receipt_data
|
@@ -30,33 +30,33 @@ module CandyCheck
|
|
30
30
|
# @return [Receipt] if successful
|
31
31
|
# @return [VerificationFailure] otherwise
|
32
32
|
def verify(receipt_data, secret = nil)
|
33
|
-
|
34
|
-
fetch_receipt_information(receipt_data, secret)
|
33
|
+
fetch_receipt_information(Verification, [receipt_data, secret])
|
35
34
|
end
|
36
35
|
|
37
36
|
# Calls a subscription verification for the given input
|
38
37
|
# @param receipt_data [String] the raw data to be verified
|
39
|
-
# @param secret [
|
38
|
+
# @param secret [String] optional: shared secret
|
39
|
+
# @param product_ids [Array<String>] optional: products to filter
|
40
40
|
# @return [ReceiptCollection] if successful
|
41
41
|
# @return [Verification] otherwise
|
42
|
-
def verify_subscription(receipt_data, secret = nil)
|
43
|
-
|
44
|
-
fetch_receipt_information(
|
42
|
+
def verify_subscription(receipt_data, secret = nil, product_ids = nil)
|
43
|
+
args = [receipt_data, secret, product_ids]
|
44
|
+
fetch_receipt_information(SubscriptionVerification, args)
|
45
45
|
end
|
46
46
|
|
47
47
|
private
|
48
48
|
|
49
|
-
def fetch_receipt_information(
|
49
|
+
def fetch_receipt_information(verifier_class, args)
|
50
50
|
default_endpoint, opposite_endpoint = endpoints
|
51
|
-
result = call_for(
|
51
|
+
result = call_for(verifier_class, args.dup.unshift(default_endpoint))
|
52
52
|
if should_retry?(result)
|
53
|
-
return call_for(
|
53
|
+
return call_for(verifier_class, args.dup.unshift(opposite_endpoint))
|
54
54
|
end
|
55
55
|
result
|
56
56
|
end
|
57
57
|
|
58
|
-
def call_for(
|
59
|
-
|
58
|
+
def call_for(verifier_class, args)
|
59
|
+
verifier_class.new(*args).call!
|
60
60
|
end
|
61
61
|
|
62
62
|
def should_retry?(result)
|
data/lib/candy_check/cli/app.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "thor"
|
2
2
|
|
3
3
|
module CandyCheck
|
4
4
|
module CLI
|
@@ -6,54 +6,37 @@ module CandyCheck
|
|
6
6
|
# @example
|
7
7
|
# $> candy_check help
|
8
8
|
class App < Thor
|
9
|
-
package_name
|
9
|
+
package_name "CandyCheck"
|
10
10
|
|
11
|
-
desc
|
11
|
+
desc "app_store RECEIPT_DATA", "Verify a base64 encoded AppStore receipt"
|
12
12
|
method_option :environment,
|
13
|
-
default:
|
13
|
+
default: "production",
|
14
14
|
type: :string,
|
15
15
|
enum: %w(production sandbox),
|
16
|
-
aliases:
|
17
|
-
desc:
|
16
|
+
aliases: "-e",
|
17
|
+
desc: "The environment to use for verfication"
|
18
18
|
method_option :secret,
|
19
|
-
aliases:
|
19
|
+
aliases: "-s",
|
20
20
|
type: :string,
|
21
|
-
desc:
|
21
|
+
desc: "The shared secret for auto-renewable subscriptions"
|
22
|
+
|
22
23
|
def app_store(receipt)
|
23
24
|
Commands::AppStore.run(receipt, options)
|
24
25
|
end
|
25
26
|
|
26
|
-
desc
|
27
|
-
method_option :
|
28
|
-
required: true,
|
29
|
-
type: :string,
|
30
|
-
aliases: '-i',
|
31
|
-
desc: 'The issuer\'s email address for the API call'
|
32
|
-
method_option :key_file,
|
27
|
+
desc "play_store PACKAGE PRODUCT_ID TOKEN", "Verify PlayStore purchase"
|
28
|
+
method_option :json_key_file,
|
33
29
|
required: true,
|
34
30
|
type: :string,
|
35
|
-
aliases:
|
36
|
-
desc:
|
37
|
-
|
38
|
-
default: 'notasecret',
|
39
|
-
type: :string,
|
40
|
-
aliases: '-s',
|
41
|
-
desc: 'The secret to decrypt the key_file'
|
42
|
-
method_option :application_name,
|
43
|
-
default: 'CandyCheck',
|
44
|
-
type: :string,
|
45
|
-
aliases: '-a',
|
46
|
-
desc: 'Your application\'s name'
|
47
|
-
method_option :application_version,
|
48
|
-
default: CandyCheck::VERSION,
|
49
|
-
type: :string,
|
50
|
-
aliases: '-v',
|
51
|
-
desc: 'Your application\'s version'
|
31
|
+
aliases: "-k",
|
32
|
+
desc: "The json key file to use for API authentication"
|
33
|
+
|
52
34
|
def play_store(package, product_id, token)
|
53
35
|
Commands::PlayStore.run(package, product_id, token, options)
|
54
36
|
end
|
55
37
|
|
56
|
-
desc
|
38
|
+
desc "version", 'Print the gem\'s version'
|
39
|
+
|
57
40
|
def version
|
58
41
|
Commands::Version.run
|
59
42
|
end
|
@@ -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,27 @@
|
|
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
|
11
|
-
require
|
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/product_acknowledgements/acknowledgement"
|
8
|
+
require "candy_check/play_store/product_acknowledgements/response"
|
9
|
+
require "candy_check/play_store/subscription_purchases/subscription_verification"
|
10
|
+
require "candy_check/play_store/verification_failure"
|
11
|
+
require "candy_check/play_store/verifier"
|
12
|
+
require "candy_check/play_store/acknowledger"
|
12
13
|
|
13
14
|
module CandyCheck
|
14
15
|
# Module to request and verify a AppStore receipt
|
15
16
|
module PlayStore
|
17
|
+
# Build an authorization object
|
18
|
+
# @param json_key_file [String]
|
19
|
+
# @return [Google::Auth::ServiceAccountCredentials]
|
20
|
+
def self.authorization(json_key_file)
|
21
|
+
Google::Auth::ServiceAccountCredentials.make_creds(
|
22
|
+
json_key_io: File.open(json_key_file),
|
23
|
+
scope: "https://www.googleapis.com/auth/androidpublisher",
|
24
|
+
)
|
25
|
+
end
|
16
26
|
end
|
17
27
|
end
|
@@ -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
|
@@ -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
|