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
@@ -8,15 +8,17 @@ module CandyCheck
8
8
 
9
9
  # Initializes a new instance which bases on a JSON result
10
10
  # from Apple's verification server
11
- # @param attributes [Array<Hash>]
11
+ # @param attributes [Array<Hash>] raw data from Apple's server
12
12
  def initialize(attributes)
13
- @receipts = attributes.map { |r| Receipt.new(r) }
13
+ @receipts = attributes.map {|r| Receipt.new(r) }.sort{ |a, b|
14
+ a.purchase_date - b.purchase_date
15
+ }
14
16
  end
15
17
 
16
18
  # Check if the latest expiration date is passed
17
19
  # @return [bool]
18
20
  def expired?
19
- overdue_days > 0
21
+ expires_at.to_time <= Time.now.utc
20
22
  end
21
23
 
22
24
  # Check if in trial
@@ -3,13 +3,28 @@ module CandyCheck
3
3
  # Verifies a latest_receipt_info block against a verification server.
4
4
  # The call return either an {ReceiptCollection} or a {VerificationFailure}
5
5
  class SubscriptionVerification < CandyCheck::AppStore::Verification
6
+ # Builds a fresh verification run
7
+ # @param endpoint_url [String] the verification URL to use
8
+ # @param receipt_data [String] the raw data to be verified
9
+ # @param secret [String] optional: shared secret
10
+ # @param product_ids [Array<String>] optional: select specific products
11
+ def initialize(
12
+ endpoint_url,
13
+ receipt_data,
14
+ secret = nil,
15
+ product_ids = nil
16
+ )
17
+ super(endpoint_url, receipt_data, secret)
18
+ @product_ids = product_ids
19
+ end
20
+
6
21
  # Performs the verification against the remote server
7
22
  # @return [ReceiptCollection] if successful
8
23
  # @return [VerificationFailure] otherwise
9
24
  def call!
10
25
  verify!
11
26
  if valid?
12
- ReceiptCollection.new(@response['latest_receipt_info'])
27
+ build_collection(@response['latest_receipt_info'])
13
28
  else
14
29
  VerificationFailure.fetch(@response['status'])
15
30
  end
@@ -17,6 +32,15 @@ module CandyCheck
17
32
 
18
33
  private
19
34
 
35
+ def build_collection(latest_receipt_info)
36
+ unless @product_ids.nil?
37
+ latest_receipt_info = latest_receipt_info.select do |info|
38
+ @product_ids.include?(info['product_id'])
39
+ end
40
+ end
41
+ ReceiptCollection.new(latest_receipt_info)
42
+ end
43
+
20
44
  def valid?
21
45
  status_is_ok = @response['status'] == STATUS_OK
22
46
  @response && status_is_ok && @response['latest_receipt_info']
@@ -16,7 +16,7 @@ module CandyCheck
16
16
  # Builds a fresh verification run
17
17
  # @param endpoint_url [String] the verification URL to use
18
18
  # @param receipt_data [String] the raw data to be verified
19
- # @param secret [String] the optional shared secret
19
+ # @param secret [String] optional: shared secret
20
20
  def initialize(endpoint_url, receipt_data, secret = nil)
21
21
  @endpoint_url = endpoint_url
22
22
  @receipt_data = receipt_data
@@ -30,33 +30,33 @@ module CandyCheck
30
30
  # @return [Receipt] if successful
31
31
  # @return [VerificationFailure] otherwise
32
32
  def verify(receipt_data, secret = nil)
33
- @verifier = Verification
34
- fetch_receipt_information(receipt_data, secret)
33
+ fetch_receipt_information(Verification, [receipt_data, secret])
35
34
  end
36
35
 
37
36
  # Calls a subscription verification for the given input
38
37
  # @param receipt_data [String] the raw data to be verified
39
- # @param secret [string] the optional shared secret
38
+ # @param secret [String] optional: shared secret
39
+ # @param product_ids [Array<String>] optional: products to filter
40
40
  # @return [ReceiptCollection] if successful
41
41
  # @return [Verification] otherwise
42
- def verify_subscription(receipt_data, secret = nil)
43
- @verifier = SubscriptionVerification
44
- fetch_receipt_information(receipt_data, secret)
42
+ def verify_subscription(receipt_data, secret = nil, product_ids = nil)
43
+ args = [receipt_data, secret, product_ids]
44
+ fetch_receipt_information(SubscriptionVerification, args)
45
45
  end
46
46
 
47
47
  private
48
48
 
49
- def fetch_receipt_information(receipt_data, secret = nil)
49
+ def fetch_receipt_information(verifier_class, args)
50
50
  default_endpoint, opposite_endpoint = endpoints
51
- result = call_for(default_endpoint, receipt_data, secret)
51
+ result = call_for(verifier_class, args.dup.unshift(default_endpoint))
52
52
  if should_retry?(result)
53
- return call_for(opposite_endpoint, receipt_data, secret)
53
+ return call_for(verifier_class, args.dup.unshift(opposite_endpoint))
54
54
  end
55
55
  result
56
56
  end
57
57
 
58
- def call_for(endpoint_url, receipt_data, secret)
59
- @verifier.new(endpoint_url, receipt_data, secret).call!
58
+ def call_for(verifier_class, args)
59
+ verifier_class.new(*args).call!
60
60
  end
61
61
 
62
62
  def should_retry?(result)
@@ -1,4 +1,4 @@
1
- require 'thor'
1
+ require "thor"
2
2
 
3
3
  module CandyCheck
4
4
  module CLI
@@ -6,54 +6,37 @@ module CandyCheck
6
6
  # @example
7
7
  # $> candy_check help
8
8
  class App < Thor
9
- package_name 'CandyCheck'
9
+ package_name "CandyCheck"
10
10
 
11
- desc 'app_store RECEIPT_DATA', 'Verify a base64 encoded AppStore receipt'
11
+ desc "app_store RECEIPT_DATA", "Verify a base64 encoded AppStore receipt"
12
12
  method_option :environment,
13
- default: 'production',
13
+ default: "production",
14
14
  type: :string,
15
15
  enum: %w(production sandbox),
16
- aliases: '-e',
17
- desc: 'The environment to use for verfication'
16
+ aliases: "-e",
17
+ desc: "The environment to use for verfication"
18
18
  method_option :secret,
19
- aliases: '-s',
19
+ aliases: "-s",
20
20
  type: :string,
21
- desc: 'The shared secret for auto-renewable subscriptions'
21
+ desc: "The shared secret for auto-renewable subscriptions"
22
+
22
23
  def app_store(receipt)
23
24
  Commands::AppStore.run(receipt, options)
24
25
  end
25
26
 
26
- desc 'play_store PACKAGE PRODUCT_ID TOKEN', 'Verify PlayStore purchase'
27
- method_option :issuer,
28
- required: true,
29
- type: :string,
30
- aliases: '-i',
31
- desc: 'The issuer\'s email address for the API call'
32
- method_option :key_file,
27
+ desc "play_store PACKAGE PRODUCT_ID TOKEN", "Verify PlayStore purchase"
28
+ method_option :json_key_file,
33
29
  required: true,
34
30
  type: :string,
35
- aliases: '-k',
36
- desc: 'The key file to use for API authentication'
37
- method_option :key_secret,
38
- default: 'notasecret',
39
- type: :string,
40
- aliases: '-s',
41
- desc: 'The secret to decrypt the key_file'
42
- method_option :application_name,
43
- default: 'CandyCheck',
44
- type: :string,
45
- aliases: '-a',
46
- desc: 'Your application\'s name'
47
- method_option :application_version,
48
- default: CandyCheck::VERSION,
49
- type: :string,
50
- aliases: '-v',
51
- desc: 'Your application\'s version'
31
+ aliases: "-k",
32
+ desc: "The json key file to use for API authentication"
33
+
52
34
  def play_store(package, product_id, token)
53
35
  Commands::PlayStore.run(package, product_id, token, options)
54
36
  end
55
37
 
56
- desc 'version', 'Print the gem\'s version'
38
+ desc "version", 'Print the gem\'s version'
39
+
57
40
  def version
58
41
  Commands::Version.run
59
42
  end
@@ -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,27 @@
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/product_acknowledgements/acknowledgement"
8
+ require "candy_check/play_store/product_acknowledgements/response"
9
+ require "candy_check/play_store/subscription_purchases/subscription_verification"
10
+ require "candy_check/play_store/verification_failure"
11
+ require "candy_check/play_store/verifier"
12
+ require "candy_check/play_store/acknowledger"
12
13
 
13
14
  module CandyCheck
14
15
  # Module to request and verify a AppStore receipt
15
16
  module PlayStore
17
+ # Build an authorization object
18
+ # @param json_key_file [String]
19
+ # @return [Google::Auth::ServiceAccountCredentials]
20
+ def self.authorization(json_key_file)
21
+ Google::Auth::ServiceAccountCredentials.make_creds(
22
+ json_key_io: File.open(json_key_file),
23
+ scope: "https://www.googleapis.com/auth/androidpublisher",
24
+ )
25
+ end
16
26
  end
17
27
  end
@@ -0,0 +1,19 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ class Acknowledger
4
+ def initialize(authorization:)
5
+ @authorization = authorization
6
+ end
7
+
8
+ def acknowledge_product_purchase(package_name:, product_id:, token:)
9
+ acknowledger = CandyCheck::PlayStore::ProductAcknowledgements::Acknowledgement.new(
10
+ package_name: package_name,
11
+ product_id: product_id,
12
+ token: token,
13
+ authorization: @authorization,
14
+ )
15
+ acknowledger.call!
16
+ end
17
+ end
18
+ end
19
+ 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,45 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ module ProductAcknowledgements
4
+ # Verifies a purchase token against the PlayStore API
5
+
6
+ class Acknowledgement
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
+ def call!
26
+ acknowledge!
27
+
28
+ CandyCheck::PlayStore::ProductAcknowledgements::Response.new(
29
+ result: @response[:result], error_data: @response[:error_data])
30
+ end
31
+
32
+ private
33
+
34
+ def acknowledge!
35
+ service = CandyCheck::PlayStore::AndroidPublisherService.new
36
+
37
+ service.authorization = @authorization
38
+ service.acknowledge_purchase_product(package_name, product_id, token) do |result, error_data|
39
+ @response = { result: result, error_data: error_data }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module CandyCheck
2
+ module PlayStore
3
+ module ProductAcknowledgements
4
+ class Response
5
+ def initialize(result:, error_data:)
6
+ @result = result
7
+ @error_data = error_data
8
+ end
9
+
10
+ def acknowledged?
11
+ !!result
12
+ end
13
+
14
+ def error
15
+ return unless error_data
16
+
17
+ { status_code: error_data.status_code, body: error_data.body }
18
+ end
19
+
20
+ attr_reader :result, :error_data
21
+ end
22
+ end
23
+ end
24
+ 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