candy_check 0.1.1 → 0.4.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 (86) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -1
  3. data/.rubocop.yml +6 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +7 -10
  6. data/Guardfile +42 -0
  7. data/MIGRATION_GUIDE_0_2_0.md +141 -0
  8. data/README.md +85 -27
  9. data/Rakefile +1 -1
  10. data/candy_check.gemspec +32 -26
  11. data/lib/candy_check/app_store/receipt_collection.rb +4 -2
  12. data/lib/candy_check/app_store/subscription_verification.rb +25 -1
  13. data/lib/candy_check/app_store/verification.rb +1 -1
  14. data/lib/candy_check/app_store/verifier.rb +11 -11
  15. data/lib/candy_check/cli/app.rb +20 -33
  16. data/lib/candy_check/cli/commands/play_store.rb +12 -13
  17. data/lib/candy_check/play_store.rb +20 -10
  18. data/lib/candy_check/play_store/acknowledger.rb +19 -0
  19. data/lib/candy_check/play_store/android_publisher_service.rb +6 -0
  20. data/lib/candy_check/play_store/product_acknowledgements/acknowledgement.rb +45 -0
  21. data/lib/candy_check/play_store/product_acknowledgements/response.rb +24 -0
  22. data/lib/candy_check/play_store/product_purchases/product_purchase.rb +90 -0
  23. data/lib/candy_check/play_store/product_purchases/product_verification.rb +53 -0
  24. data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +154 -0
  25. data/lib/candy_check/play_store/subscription_purchases/subscription_verification.rb +55 -0
  26. data/lib/candy_check/play_store/verification_failure.rb +8 -6
  27. data/lib/candy_check/play_store/verifier.rb +24 -49
  28. data/lib/candy_check/version.rb +1 -1
  29. data/spec/app_store/client_spec.rb +2 -2
  30. data/spec/app_store/config_spec.rb +5 -5
  31. data/spec/app_store/receipt_collection_spec.rb +35 -8
  32. data/spec/app_store/receipt_spec.rb +16 -16
  33. data/spec/app_store/subscription_verification_spec.rb +49 -16
  34. data/spec/app_store/verifcation_failure_spec.rb +6 -6
  35. data/spec/app_store/verification_spec.rb +12 -12
  36. data/spec/app_store/verifier_spec.rb +36 -17
  37. data/spec/candy_check_spec.rb +3 -3
  38. data/spec/cli/app_spec.rb +10 -6
  39. data/spec/cli/commands/app_store_spec.rb +6 -6
  40. data/spec/cli/commands/play_store_spec.rb +10 -43
  41. data/spec/cli/commands/version_spec.rb +1 -1
  42. data/spec/cli/out_spec.rb +4 -4
  43. data/spec/fixtures/play_store/random_dummy_key.json +12 -0
  44. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml +105 -0
  45. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml +124 -0
  46. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml +122 -0
  47. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml +196 -0
  48. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml +183 -0
  49. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml +122 -0
  50. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml +196 -0
  51. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml +127 -0
  52. data/spec/play_store/acknowledger_spec.rb +48 -0
  53. data/spec/play_store/product_acknowledgements/acknowledgement_spec.rb +54 -0
  54. data/spec/play_store/product_acknowledgements/response_spec.rb +66 -0
  55. data/spec/play_store/product_purchases/product_purchase_spec.rb +110 -0
  56. data/spec/play_store/product_purchases/product_verification_spec.rb +49 -0
  57. data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +237 -0
  58. data/spec/play_store/subscription_purchases/subscription_verification_spec.rb +65 -0
  59. data/spec/play_store/verification_failure_spec.rb +20 -20
  60. data/spec/play_store/verifier_spec.rb +32 -96
  61. data/spec/spec_helper.rb +31 -11
  62. data/spec/support/with_command.rb +0 -3
  63. metadata +167 -81
  64. data/lib/candy_check/play_store/client.rb +0 -126
  65. data/lib/candy_check/play_store/config.rb +0 -51
  66. data/lib/candy_check/play_store/discovery_repository.rb +0 -33
  67. data/lib/candy_check/play_store/receipt.rb +0 -81
  68. data/lib/candy_check/play_store/subscription.rb +0 -139
  69. data/lib/candy_check/play_store/subscription_verification.rb +0 -30
  70. data/lib/candy_check/play_store/verification.rb +0 -48
  71. data/spec/fixtures/api_cache.dump +0 -1
  72. data/spec/fixtures/play_store/api_cache.dump +0 -1
  73. data/spec/fixtures/play_store/auth_failure.txt +0 -18
  74. data/spec/fixtures/play_store/auth_success.txt +0 -20
  75. data/spec/fixtures/play_store/discovery.txt +0 -2841
  76. data/spec/fixtures/play_store/dummy.p12 +0 -0
  77. data/spec/fixtures/play_store/empty.txt +0 -17
  78. data/spec/fixtures/play_store/products_failure.txt +0 -29
  79. data/spec/fixtures/play_store/products_success.txt +0 -22
  80. data/spec/play_store/client_spec.rb +0 -125
  81. data/spec/play_store/config_spec.rb +0 -96
  82. data/spec/play_store/discovery_respository_spec.rb +0 -31
  83. data/spec/play_store/receipt_spec.rb +0 -88
  84. data/spec/play_store/subscription_spec.rb +0 -138
  85. data/spec/play_store/subscription_verification_spec.rb +0 -97
  86. data/spec/play_store/verification_spec.rb +0 -81
@@ -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,139 +0,0 @@
1
- module CandyCheck
2
- module PlayStore
3
- # Describes a successfully validated subscription
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 payed
84
- # currency
85
- # @return [Integer]
86
- def price_amount_micros
87
- read_integer('priceAmountMicros')
88
- end
89
-
90
- # Get the cancel reason, as given by Google
91
- # @return [Integer]
92
- def cancel_reason
93
- read_integer('cancelReason')
94
- end
95
-
96
- # Get the kind of subscription as stored in the android publisher service
97
- # @return [String]
98
- def kind
99
- read('kind')
100
- end
101
-
102
- # Get developer-specified supplemental information about the order
103
- # @return [String]
104
- def developer_payload
105
- read('developerPayload')
106
- end
107
-
108
- # Get the currency code in ISO 4217 format, e.g. "GBP" for British pounds
109
- # @return [String]
110
- def price_currency_code
111
- read('priceCurrencyCode')
112
- end
113
-
114
- # Get start time for subscription in milliseconds since Epoch
115
- # @return [Integer]
116
- def start_time_millis
117
- read_integer('startTimeMillis')
118
- end
119
-
120
- # Get expiry time for subscription in milliseconds since Epoch
121
- # @return [Integer]
122
- def expiry_time_millis
123
- read_integer('expiryTimeMillis')
124
- end
125
-
126
- # Get start time in UTC
127
- # @return [DateTime]
128
- def starts_at
129
- read_datetime_from_millis('startTimeMillis')
130
- end
131
-
132
- # Get expiration time in UTC
133
- # @return [DateTime]
134
- def expires_at
135
- read_datetime_from_millis('expiryTimeMillis')
136
- end
137
- end
138
- end
139
- 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