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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -12
  3. data/Guardfile +42 -0
  4. data/MIGRATION_GUIDE_0_2_0.md +141 -0
  5. data/README.md +49 -26
  6. data/candy_check.gemspec +32 -26
  7. data/lib/candy_check/cli/app.rb +16 -33
  8. data/lib/candy_check/cli/commands/play_store.rb +12 -13
  9. data/lib/candy_check/play_store.rb +17 -10
  10. data/lib/candy_check/play_store/android_publisher_service.rb +6 -0
  11. data/lib/candy_check/play_store/product_purchases/product_purchase.rb +90 -0
  12. data/lib/candy_check/play_store/product_purchases/product_verification.rb +53 -0
  13. data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +141 -0
  14. data/lib/candy_check/play_store/subscription_purchases/subscription_verification.rb +55 -0
  15. data/lib/candy_check/play_store/verification_failure.rb +8 -6
  16. data/lib/candy_check/play_store/verifier.rb +24 -49
  17. data/lib/candy_check/version.rb +1 -1
  18. data/spec/candy_check_spec.rb +2 -2
  19. data/spec/cli/commands/play_store_spec.rb +10 -43
  20. data/spec/fixtures/play_store/random_dummy_key.json +12 -0
  21. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml +196 -0
  22. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml +183 -0
  23. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml +122 -0
  24. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml +196 -0
  25. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml +127 -0
  26. data/spec/play_store/product_purchases/product_purchase_spec.rb +110 -0
  27. data/spec/play_store/product_purchases/product_verification_spec.rb +49 -0
  28. data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +181 -0
  29. data/spec/play_store/subscription_purchases/subscription_verification_spec.rb +65 -0
  30. data/spec/play_store/verification_failure_spec.rb +18 -18
  31. data/spec/play_store/verifier_spec.rb +32 -96
  32. data/spec/spec_helper.rb +24 -11
  33. metadata +120 -47
  34. data/lib/candy_check/play_store/client.rb +0 -139
  35. data/lib/candy_check/play_store/config.rb +0 -51
  36. data/lib/candy_check/play_store/discovery_repository.rb +0 -33
  37. data/lib/candy_check/play_store/receipt.rb +0 -81
  38. data/lib/candy_check/play_store/subscription.rb +0 -139
  39. data/lib/candy_check/play_store/subscription_verification.rb +0 -30
  40. data/lib/candy_check/play_store/verification.rb +0 -48
  41. data/spec/fixtures/api_cache.dump +0 -1
  42. data/spec/fixtures/play_store/api_cache.dump +0 -1
  43. data/spec/fixtures/play_store/auth_failure.txt +0 -18
  44. data/spec/fixtures/play_store/auth_success.txt +0 -20
  45. data/spec/fixtures/play_store/discovery.txt +0 -2841
  46. data/spec/fixtures/play_store/dummy.p12 +0 -0
  47. data/spec/fixtures/play_store/empty.txt +0 -17
  48. data/spec/fixtures/play_store/products_failure.txt +0 -29
  49. data/spec/fixtures/play_store/products_success.txt +0 -22
  50. data/spec/play_store/client_spec.rb +0 -125
  51. data/spec/play_store/config_spec.rb +0 -96
  52. data/spec/play_store/discovery_respository_spec.rb +0 -31
  53. data/spec/play_store/receipt_spec.rb +0 -88
  54. data/spec/play_store/subscription_spec.rb +0 -138
  55. data/spec/play_store/subscription_verification_spec.rb +0 -97
  56. 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 package [String]
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] :issuer to use for API access
12
- # @option options [String] :key_file to use for API access
13
- # @option options [String] :key_secret to decrypt the key file
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(config)
26
- verifier.boot!
27
- result = verifier.verify(@package, @product_id, @token)
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 config
35
- CandyCheck::PlayStore::Config.new(options)
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 'google/api_client'
1
+ require "google/apis/androidpublisher_v3"
2
2
 
3
- require 'candy_check/play_store/discovery_repository'
4
- require 'candy_check/play_store/client'
5
- require 'candy_check/play_store/config'
6
- require 'candy_check/play_store/receipt'
7
- require 'candy_check/play_store/subscription'
8
- require 'candy_check/play_store/verification'
9
- require 'candy_check/play_store/subscription_verification'
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,6 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ class AndroidPublisherService < Google::Apis::AndroidpublisherV3::AndroidPublisherService
4
+ end
5
+ end
6
+ 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 :attributes
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 attributes [Hash]
13
- def initialize(attributes)
14
- @attributes = attributes || {}
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
- read('code') || -1
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
- read('message') || 'Unknown error'
28
+ error.message || "Unknown error"
27
29
  end
28
30
  end
29
31
  end