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.
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