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
@@ -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