candy_check 0.1.0.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -5,9 +5,11 @@ module CandyCheck
5
5
  # Initializes a new configuration from a hash
6
6
  # @param attributes [Hash]
7
7
  def initialize(attributes)
8
- attributes.each do |k, v|
9
- instance_variable_set "@#{k}", v
10
- end if attributes.is_a? Hash
8
+ if attributes.is_a?(Hash)
9
+ attributes.each do |k, v|
10
+ instance_variable_set "@#{k}", v
11
+ end
12
+ end
11
13
  validate!
12
14
  end
13
15
 
@@ -1,4 +1,4 @@
1
1
  module CandyCheck
2
2
  # The current gem's version
3
- VERSION = '0.1.0.pre'.freeze
3
+ VERSION = "0.3.0".freeze
4
4
  end
@@ -8,10 +8,12 @@ describe CandyCheck::AppStore::ReceiptCollection do
8
8
  [{
9
9
  'expires_date' => '2014-04-15 12:52:40 Etc/GMT',
10
10
  'expires_date_pst' => '2014-04-15 05:52:40 America/Los_Angeles',
11
+ 'purchase_date' => '2014-04-14 12:52:40 Etc/GMT',
11
12
  'is_trial_period' => 'false'
12
13
  }, {
13
14
  'expires_date' => '2015-04-15 12:52:40 Etc/GMT',
14
15
  'expires_date_pst' => '2015-04-15 05:52:40 America/Los_Angeles',
16
+ 'purchase_date' => '2015-04-14 12:52:40 Etc/GMT',
15
17
  'is_trial_period' => 'false'
16
18
  }]
17
19
  end
@@ -34,6 +36,34 @@ describe CandyCheck::AppStore::ReceiptCollection do
34
36
  expected = DateTime.new(2015, 4, 15, 12, 52, 40)
35
37
  subject.expires_at.must_equal expected
36
38
  end
39
+
40
+ it 'is expired? at same pointin time' do
41
+ Timecop.freeze(Time.utc(2015, 4, 15, 12, 52, 40)) do
42
+ subject.expired?.must_be_true
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'unordered receipts' do
48
+ let(:attributes) do
49
+ [{
50
+ 'expires_date' => '2015-04-15 12:52:40 Etc/GMT',
51
+ 'expires_date_pst' => '2015-04-15 05:52:40 America/Los_Angeles',
52
+ 'purchase_date' => '2015-04-14 12:52:40 Etc/GMT',
53
+ 'is_trial_period' => 'false'
54
+ }, {
55
+ 'expires_date' => '2014-04-15 12:52:40 Etc/GMT',
56
+ 'expires_date_pst' => '2014-04-15 05:52:40 America/Los_Angeles',
57
+ 'purchase_date' => '2014-04-14 12:52:40 Etc/GMT',
58
+ 'is_trial_period' => 'false'
59
+ }]
60
+ end
61
+
62
+ it 'the expires date is the latest one in time' do
63
+ expected = DateTime.new(2015, 4, 15, 12, 52, 40)
64
+ subject.expires_at.must_equal expected
65
+ end
66
+
37
67
  end
38
68
 
39
69
  describe 'unexpired trial subscription' do
@@ -42,10 +72,12 @@ describe CandyCheck::AppStore::ReceiptCollection do
42
72
  let(:attributes) do
43
73
  [{
44
74
  'expires_date' => '2016-04-15 12:52:40 Etc/GMT',
75
+ 'purchase_date' => '2016-04-15 12:52:40 Etc/GMT',
45
76
  'is_trial_period' => 'true'
46
77
  }, {
47
78
  'expires_date' =>
48
79
  two_days_from_now.strftime('%Y-%m-%d %H:%M:%S Etc/GMT'),
80
+ 'purchase_date' => '2016-04-15 12:52:40 Etc/GMT',
49
81
  'is_trial_period' => 'true'
50
82
  }]
51
83
  end
@@ -62,4 +94,5 @@ describe CandyCheck::AppStore::ReceiptCollection do
62
94
  subject.overdue_days.must_equal(-2)
63
95
  end
64
96
  end
97
+
65
98
  end