candy_check 0.1.0.pre → 0.3.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 (76) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +23 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +7 -8
  5. data/Guardfile +42 -0
  6. data/MIGRATION_GUIDE_0_2_0.md +141 -0
  7. data/README.md +86 -26
  8. data/Rakefile +1 -1
  9. data/candy_check.gemspec +33 -25
  10. data/lib/candy_check/app_store/receipt_collection.rb +5 -3
  11. data/lib/candy_check/app_store/subscription_verification.rb +25 -1
  12. data/lib/candy_check/app_store/verification.rb +1 -1
  13. data/lib/candy_check/app_store/verifier.rb +11 -11
  14. data/lib/candy_check/cli/app.rb +16 -33
  15. data/lib/candy_check/cli/commands/play_store.rb +12 -13
  16. data/lib/candy_check/play_store.rb +20 -10
  17. data/lib/candy_check/play_store/acknowledger.rb +19 -0
  18. data/lib/candy_check/play_store/android_publisher_service.rb +6 -0
  19. data/lib/candy_check/play_store/product_acknowledgements/acknowledgement.rb +45 -0
  20. data/lib/candy_check/play_store/product_acknowledgements/response.rb +24 -0
  21. data/lib/candy_check/play_store/product_purchases/product_purchase.rb +90 -0
  22. data/lib/candy_check/play_store/product_purchases/product_verification.rb +53 -0
  23. data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +154 -0
  24. data/lib/candy_check/play_store/subscription_purchases/subscription_verification.rb +55 -0
  25. data/lib/candy_check/play_store/verification_failure.rb +8 -6
  26. data/lib/candy_check/play_store/verifier.rb +24 -49
  27. data/lib/candy_check/utils/config.rb +5 -3
  28. data/lib/candy_check/version.rb +1 -1
  29. data/spec/app_store/receipt_collection_spec.rb +33 -0
  30. data/spec/app_store/subscription_verification_spec.rb +35 -2
  31. data/spec/app_store/verifier_spec.rb +24 -5
  32. data/spec/candy_check_spec.rb +2 -2
  33. data/spec/cli/commands/play_store_spec.rb +10 -43
  34. data/spec/fixtures/play_store/random_dummy_key.json +12 -0
  35. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml +105 -0
  36. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml +124 -0
  37. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml +122 -0
  38. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml +196 -0
  39. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml +183 -0
  40. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml +122 -0
  41. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml +196 -0
  42. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml +127 -0
  43. data/spec/play_store/acknowledger_spec.rb +48 -0
  44. data/spec/play_store/product_acknowledgements/acknowledgement_spec.rb +54 -0
  45. data/spec/play_store/product_acknowledgements/response_spec.rb +66 -0
  46. data/spec/play_store/product_purchases/product_purchase_spec.rb +110 -0
  47. data/spec/play_store/product_purchases/product_verification_spec.rb +49 -0
  48. data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +237 -0
  49. data/spec/play_store/subscription_purchases/subscription_verification_spec.rb +65 -0
  50. data/spec/play_store/verification_failure_spec.rb +18 -18
  51. data/spec/play_store/verifier_spec.rb +32 -96
  52. data/spec/spec_helper.rb +32 -10
  53. metadata +175 -75
  54. data/lib/candy_check/play_store/client.rb +0 -126
  55. data/lib/candy_check/play_store/config.rb +0 -51
  56. data/lib/candy_check/play_store/discovery_repository.rb +0 -33
  57. data/lib/candy_check/play_store/receipt.rb +0 -81
  58. data/lib/candy_check/play_store/subscription.rb +0 -138
  59. data/lib/candy_check/play_store/subscription_verification.rb +0 -30
  60. data/lib/candy_check/play_store/verification.rb +0 -48
  61. data/spec/fixtures/api_cache.dump +0 -1
  62. data/spec/fixtures/play_store/api_cache.dump +0 -1
  63. data/spec/fixtures/play_store/auth_failure.txt +0 -18
  64. data/spec/fixtures/play_store/auth_success.txt +0 -20
  65. data/spec/fixtures/play_store/discovery.txt +0 -2841
  66. data/spec/fixtures/play_store/dummy.p12 +0 -0
  67. data/spec/fixtures/play_store/empty.txt +0 -17
  68. data/spec/fixtures/play_store/products_failure.txt +0 -29
  69. data/spec/fixtures/play_store/products_success.txt +0 -22
  70. data/spec/play_store/client_spec.rb +0 -125
  71. data/spec/play_store/config_spec.rb +0 -96
  72. data/spec/play_store/discovery_respository_spec.rb +0 -31
  73. data/spec/play_store/receipt_spec.rb +0 -88
  74. data/spec/play_store/subscription_spec.rb +0 -138
  75. data/spec/play_store/subscription_verification_spec.rb +0 -98
  76. 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