candy_check 0.1.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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,154 @@
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
+ (Time.now.utc.to_date - 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 cancellation time for subscription in milliseconds since Epoch.
128
+ # Only present if cancelReason is 0.
129
+ # @return [Integer]
130
+ def user_cancellation_time_millis
131
+ @subscription_purchase.user_cancellation_time_millis if canceled_by_user?
132
+ end
133
+
134
+ # Get start time in UTC
135
+ # @return [DateTime]
136
+ def starts_at
137
+ Time.at(start_time_millis / 1000).utc.to_datetime
138
+ end
139
+
140
+ # Get expiration time in UTC
141
+ # @return [DateTime]
142
+ def expires_at
143
+ Time.at(expiry_time_millis / 1000).utc.to_datetime
144
+ end
145
+
146
+ # Get cancellation time in UTC
147
+ # @return [DateTime]
148
+ def canceled_at
149
+ Time.at(user_cancellation_time_millis / 1000).utc.to_datetime if user_cancellation_time_millis
150
+ end
151
+ end
152
+ end
153
+ end
154
+ 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
@@ -1,69 +1,44 @@
1
1
  module CandyCheck
2
2
  module PlayStore
3
3
  # Verifies purchase tokens against the Google API.
4
- # The call return either an {Receipt} or a {VerificationFailure}
4
+ # The call return either a {SubscriptionPurchases::SubscriptionPurchase} or a {VerificationFailure}
5
5
  class Verifier
6
- # Error thrown when the verifier isn't booted before the first
7
- # verification check or on double invocation
8
- class BootRequiredError < RuntimeError; end
9
-
10
- # @return [Config] the current configuration
11
- attr_reader :config
12
-
13
- # Initializes a new verifier for the application which is bound
14
- # to a configuration
15
- # @param config [Config]
16
- def initialize(config)
17
- @config = config
18
- end
19
-
20
- # Boot the module
21
- def boot!
22
- boot_error('You\'re only allowed to boot the verifier once') if booted?
23
- @client = Client.new(config)
24
- @client.boot!
6
+ # Initializes a new verifier which is bound to an authorization
7
+ # @param authorization [Google::Auth::ServiceAccountCredentials] to use against the PlayStore API
8
+ def initialize(authorization:)
9
+ @authorization = authorization
25
10
  end
26
11
 
27
12
  # Contacts the Google API and requests the product state
28
- # @param package [String] to query
13
+ # @param package_name [String] to query
29
14
  # @param product_id [String] to query
30
15
  # @param token [String] to use for authentication
31
- # @return [Receipt] if successful
16
+ # @return [ProductPurchases::ProductPurchase] if successful
32
17
  # @return [VerificationFailure] otherwise
33
- def verify(package, product_id, token)
34
- check_boot!
35
- verification = Verification.new(@client, package, product_id, token)
36
- verification.call!
18
+ def verify_product_purchase(package_name:, product_id:, token:)
19
+ verifier = CandyCheck::PlayStore::ProductPurchases::ProductVerification.new(
20
+ package_name: package_name,
21
+ product_id: product_id,
22
+ token: token,
23
+ authorization: @authorization,
24
+ )
25
+ verifier.call!
37
26
  end
38
27
 
39
28
  # Contacts the Google API and requests the product state
40
- # @param package [String] to query
29
+ # @param package_name [String] to query
41
30
  # @param subscription_id [String] to query
42
31
  # @param token [String] to use for authentication
43
- # @return [Receipt] if successful
32
+ # @return [SubscriptionPurchases::SubscriptionPurchase] if successful
44
33
  # @return [VerificationFailure] otherwise
45
- def verify_subscription(package, subscription_id, token)
46
- check_boot!
47
- v = SubscriptionVerification.new(
48
- @client, package, subscription_id, token
34
+ def verify_subscription_purchase(package_name:, subscription_id:, token:)
35
+ verifier = CandyCheck::PlayStore::SubscriptionPurchases::SubscriptionVerification.new(
36
+ package_name: package_name,
37
+ subscription_id: subscription_id,
38
+ token: token,
39
+ authorization: @authorization,
49
40
  )
50
- v.call!
51
- end
52
-
53
- private
54
-
55
- def booted?
56
- instance_variable_defined?(:@client)
57
- end
58
-
59
- def check_boot!
60
- return if booted?
61
- boot_error 'You need to boot the verifier service first: '\
62
- 'CandyCheck::PlayStore::Verifier#boot!'
63
- end
64
-
65
- def boot_error(message)
66
- raise BootRequiredError, message
41
+ verifier.call!
67
42
  end
68
43
  end
69
44
  end
@@ -1,4 +1,4 @@
1
1
  module CandyCheck
2
2
  # The current gem's version
3
- VERSION = '0.1.1'.freeze
3
+ VERSION = "0.4.0".freeze
4
4
  end
@@ -39,7 +39,7 @@ describe CandyCheck::AppStore::Client do
39
39
  body: response
40
40
  )
41
41
  result = subject.verify(receipt_data)
42
- result.must_equal expected
42
+ _(result).must_equal expected
43
43
  end
44
44
 
45
45
  it 'sends JSON and parses the JSON response with a secret' do
@@ -54,7 +54,7 @@ describe CandyCheck::AppStore::Client do
54
54
  body: response
55
55
  )
56
56
  result = subject.verify(receipt_data, password)
57
- result.must_equal expected
57
+ _(result).must_equal expected
58
58
  end
59
59
  end
60
60
 
@@ -11,16 +11,16 @@ describe CandyCheck::AppStore::Config do
11
11
  end
12
12
 
13
13
  it 'returns environment' do
14
- subject.environment.must_equal :sandbox
14
+ _(subject.environment).must_equal :sandbox
15
15
  end
16
16
 
17
17
  it 'checks for production?' do
18
- subject.production?.must_be_false
18
+ _(subject.production?).must_be_false
19
19
 
20
20
  other = CandyCheck::AppStore::Config.new(
21
21
  environment: :production
22
22
  )
23
- other.production?.must_be_true
23
+ _(other.production?).must_be_true
24
24
  end
25
25
  end
26
26
 
@@ -30,12 +30,12 @@ describe CandyCheck::AppStore::Config do
30
30
  end
31
31
 
32
32
  it 'needs an environment' do
33
- proc { subject }.must_raise ArgumentError
33
+ _(proc { subject }).must_raise ArgumentError
34
34
  end
35
35
 
36
36
  it 'needs an included environment' do
37
37
  attributes[:environment] = :invalid
38
- proc { subject }.must_raise ArgumentError
38
+ _(proc { subject }).must_raise ArgumentError
39
39
  end
40
40
  end
41
41
  end