candy_check 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +6 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +157 -0
- data/Rakefile +15 -0
- data/bin/cc_appstore +90 -0
- data/bin/cc_playstore +119 -0
- data/candy_check.gemspec +31 -0
- data/lib/candy_check/app_store/client.rb +57 -0
- data/lib/candy_check/app_store/config.rb +30 -0
- data/lib/candy_check/app_store/receipt.rb +83 -0
- data/lib/candy_check/app_store/verification.rb +49 -0
- data/lib/candy_check/app_store/verification_failure.rb +60 -0
- data/lib/candy_check/app_store/verifier.rb +69 -0
- data/lib/candy_check/app_store.rb +12 -0
- data/lib/candy_check/play_store/client.rb +102 -0
- data/lib/candy_check/play_store/config.rb +51 -0
- data/lib/candy_check/play_store/discovery_repository.rb +33 -0
- data/lib/candy_check/play_store/receipt.rb +81 -0
- data/lib/candy_check/play_store/verification.rb +46 -0
- data/lib/candy_check/play_store/verification_failure.rb +30 -0
- data/lib/candy_check/play_store/verifier.rb +52 -0
- data/lib/candy_check/play_store.rb +15 -0
- data/lib/candy_check/utils/attribute_reader.rb +30 -0
- data/lib/candy_check/utils/config.rb +40 -0
- data/lib/candy_check/utils.rb +2 -0
- data/lib/candy_check/version.rb +4 -0
- data/lib/candy_check.rb +8 -0
- data/spec/app_store/client_spec.rb +55 -0
- data/spec/app_store/config_spec.rb +41 -0
- data/spec/app_store/receipt_spec.rb +92 -0
- data/spec/app_store/verifcation_failure_spec.rb +28 -0
- data/spec/app_store/verification_spec.rb +66 -0
- data/spec/app_store/verifier_spec.rb +110 -0
- data/spec/candy_check_spec.rb +9 -0
- data/spec/fixtures/api_cache.dump +1 -0
- data/spec/fixtures/play_store/api_cache.dump +1 -0
- data/spec/fixtures/play_store/auth_failure.txt +18 -0
- data/spec/fixtures/play_store/auth_success.txt +20 -0
- data/spec/fixtures/play_store/discovery.txt +2841 -0
- data/spec/fixtures/play_store/dummy.p12 +0 -0
- data/spec/fixtures/play_store/empty.txt +17 -0
- data/spec/fixtures/play_store/products_failure.txt +29 -0
- data/spec/fixtures/play_store/products_success.txt +22 -0
- data/spec/play_store/client_spec.rb +104 -0
- data/spec/play_store/config_spec.rb +96 -0
- data/spec/play_store/discovery_respository_spec.rb +31 -0
- data/spec/play_store/receipt_spec.rb +88 -0
- data/spec/play_store/verification_failure_spec.rb +35 -0
- data/spec/play_store/verification_spec.rb +80 -0
- data/spec/play_store/verifier_spec.rb +95 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/with_fixtures.rb +9 -0
- data/spec/support/with_temp_file.rb +23 -0
- metadata +270 -0
data/candy_check.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'candy_check/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'candy_check'
|
8
|
+
spec.version = CandyCheck::VERSION
|
9
|
+
spec.authors = ['Jonas Thiel']
|
10
|
+
spec.email = ['jonas@thiel.io']
|
11
|
+
spec.summary = 'Check and verify in-app receipts'
|
12
|
+
spec.homepage = 'https://github.com/jnbt/candy_check'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'multi_json', '~> 1.10'
|
21
|
+
spec.add_dependency 'google-api-client', '~> 0.8'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'rubocop', '~> 0.28'
|
24
|
+
spec.add_development_dependency 'inch', '~> 0.5'
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
27
|
+
spec.add_development_dependency 'coveralls', '~> 0.7'
|
28
|
+
spec.add_development_dependency 'minitest', '~> 5.5'
|
29
|
+
spec.add_development_dependency 'minitest-around', '~> 0.3'
|
30
|
+
spec.add_development_dependency 'webmock', '~> 1.20'
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module CandyCheck
|
4
|
+
module AppStore
|
5
|
+
# Simple HTTP client to load the receipt's data from Apple's verification
|
6
|
+
# servers (either sandbox or production).
|
7
|
+
class Client
|
8
|
+
# Mimetype for JSON objects
|
9
|
+
JSON_MIME_TYPE = 'application/json'
|
10
|
+
|
11
|
+
# Initialize a new client bound to an endpoint
|
12
|
+
# @param endpoint_url [String]
|
13
|
+
def initialize(endpoint_url)
|
14
|
+
@uri = URI(endpoint_url)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Contacts the configured endpoint and requests the receipt's data
|
18
|
+
# @param receipt_data [String] base64 encoded data string from the app
|
19
|
+
# @param secret [String] the password for auto-renewable subscriptions
|
20
|
+
# @return [Hash]
|
21
|
+
def verify(receipt_data, secret = nil)
|
22
|
+
request = build_request(build_request_parameters(receipt_data, secret))
|
23
|
+
response = perform_request(request)
|
24
|
+
MultiJson.load(response.body)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def perform_request(request)
|
30
|
+
build_http_connector.request(request)
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_http_connector
|
34
|
+
Net::HTTP.new(@uri.host, @uri.port).tap do |net|
|
35
|
+
net.use_ssl = true
|
36
|
+
net.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_request(parameters)
|
41
|
+
Net::HTTP::Post.new(@uri.request_uri).tap do |post|
|
42
|
+
post['Accept'] = JSON_MIME_TYPE
|
43
|
+
post['Content-Type'] = JSON_MIME_TYPE
|
44
|
+
post.body = MultiJson.dump(parameters)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_request_parameters(receipt_data, secret)
|
49
|
+
{
|
50
|
+
'receipt-data' => receipt_data
|
51
|
+
}.tap do |h|
|
52
|
+
h['password'] = secret if secret
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Configure the verifier
|
4
|
+
class Config < Utils::Config
|
5
|
+
# @return [Symbol] the used environment
|
6
|
+
attr_reader :environment
|
7
|
+
|
8
|
+
# Initializes a new configuration from a hash
|
9
|
+
# @param attributes [Hash]
|
10
|
+
# @example
|
11
|
+
# Config.new(
|
12
|
+
# environment: :production # or :sandbox
|
13
|
+
# )
|
14
|
+
def initialize(attributes)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Boolean] if it is production environment
|
19
|
+
def production?
|
20
|
+
environment == :production
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate!
|
26
|
+
validates_inclusion(:environment, :production, :sandbox)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Describes a successful response from the AppStore 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
|
+
# Initializes a new instance which bases on a JSON result
|
11
|
+
# from Apple's verification server
|
12
|
+
# @param attributes [Hash]
|
13
|
+
def initialize(attributes)
|
14
|
+
@attributes = attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
# In most cases a receipt is a valid transaction except when the
|
18
|
+
# transaction was canceled.
|
19
|
+
# @return [Boolean]
|
20
|
+
def valid?
|
21
|
+
!has?('cancellation_date')
|
22
|
+
end
|
23
|
+
|
24
|
+
# The receipt's transaction id
|
25
|
+
# @return [String]
|
26
|
+
def transaction_id
|
27
|
+
read('transaction_id')
|
28
|
+
end
|
29
|
+
|
30
|
+
# The receipt's original transaction id which might differ from
|
31
|
+
# the transaction id for restored products
|
32
|
+
# @return [String]
|
33
|
+
def original_transaction_id
|
34
|
+
read('original_transaction_id')
|
35
|
+
end
|
36
|
+
|
37
|
+
# The version number for the app
|
38
|
+
# @return [String]
|
39
|
+
def app_version
|
40
|
+
read('bvrs')
|
41
|
+
end
|
42
|
+
|
43
|
+
# The app's bundle identifier
|
44
|
+
# @return [String]
|
45
|
+
def bundle_identifier
|
46
|
+
read('bid')
|
47
|
+
end
|
48
|
+
|
49
|
+
# The app's item id of the product
|
50
|
+
# @return [String]
|
51
|
+
def item_id
|
52
|
+
read('item_id')
|
53
|
+
end
|
54
|
+
|
55
|
+
# The quantity of the product
|
56
|
+
# @return [Fixnum]
|
57
|
+
def quantity
|
58
|
+
read_integer('quantity')
|
59
|
+
end
|
60
|
+
|
61
|
+
# The purchase date
|
62
|
+
# @return [DateTime]
|
63
|
+
def purchase_date
|
64
|
+
read_datetime_from_string('purchase_date')
|
65
|
+
end
|
66
|
+
|
67
|
+
# The original purchase date which might differ from the
|
68
|
+
# actual purchase date for restored products
|
69
|
+
# @return [DateTime]
|
70
|
+
def original_purchase_date
|
71
|
+
read_datetime_from_string('original_purchase_date')
|
72
|
+
end
|
73
|
+
|
74
|
+
# The date of when Apple has canceled this transaction.
|
75
|
+
# From Apple's documentation: "Treat a canceled receipt
|
76
|
+
# the same as if no purchase had ever been made."
|
77
|
+
# @return [DateTime]
|
78
|
+
def cancellation_date
|
79
|
+
read_datetime_from_string('cancellation_date')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Verifies a receipt block against a verification server.
|
4
|
+
# The call return either an {Receipt} or a {VerificationFailure}
|
5
|
+
class Verification
|
6
|
+
# @return [String] the verification URL to use
|
7
|
+
attr_reader :endpoint_url
|
8
|
+
# @return [String] the raw data to be verified
|
9
|
+
attr_reader :receipt_data
|
10
|
+
# @return [String] the optional shared secret
|
11
|
+
attr_reader :secret
|
12
|
+
|
13
|
+
# Constant for successful responses
|
14
|
+
STATUS_OK = 0
|
15
|
+
|
16
|
+
# Builds a fresh verification run
|
17
|
+
# @param endpoint_url [String] the verification URL to use
|
18
|
+
# @param receipt_data [String] the raw data to be verified
|
19
|
+
# @param secret [String] the optional shared secret
|
20
|
+
def initialize(endpoint_url, receipt_data, secret = nil)
|
21
|
+
@endpoint_url, @receipt_data = endpoint_url, receipt_data
|
22
|
+
@secret = secret
|
23
|
+
end
|
24
|
+
|
25
|
+
# Performs the verification against the remote server
|
26
|
+
# @return [Receipt] if successful
|
27
|
+
# @return [VerificationFailure] otherwise
|
28
|
+
def call!
|
29
|
+
verify!
|
30
|
+
if valid?
|
31
|
+
Receipt.new(@response['receipt'])
|
32
|
+
else
|
33
|
+
VerificationFailure.fetch(@response['status'])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def valid?
|
40
|
+
@response && @response['status'] == STATUS_OK && @response['receipt']
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify!
|
44
|
+
client = Client.new(endpoint_url)
|
45
|
+
@response = client.verify(receipt_data, secret)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Represents a failing call against the verification server
|
4
|
+
class VerificationFailure < Struct.new(:code, :message)
|
5
|
+
# @!attribute code
|
6
|
+
# @return [Fixnum] the code of the failure
|
7
|
+
# @!attribute message
|
8
|
+
# @return [String] the message of the failure
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Gets a known failure or build an unknown failure
|
12
|
+
# without description
|
13
|
+
# @param code [Fixnum]
|
14
|
+
# @return [VerificationFailure]
|
15
|
+
def fetch(code)
|
16
|
+
known.fetch(code) do
|
17
|
+
fallback(code)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fallback(code)
|
24
|
+
new(code || -1, 'Unknown error')
|
25
|
+
end
|
26
|
+
|
27
|
+
def known
|
28
|
+
@known ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(code, name)
|
32
|
+
known[code] = new(code, name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def freeze!
|
36
|
+
known.freeze
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
add 21_000, 'The App Store could not read the JSON object you provided.'
|
41
|
+
add 21_002, 'The data in the receipt-data property was malformed' \
|
42
|
+
' or missing.'
|
43
|
+
add 21_003, 'The receipt could not be authenticated.'
|
44
|
+
add 21_004, 'The shared secret you provided does not match the shared' \
|
45
|
+
' secret on file for your account.'
|
46
|
+
add 21_005, 'The receipt server is not currently available.'
|
47
|
+
add 21_006, 'This receipt is valid but the subscription has expired.' \
|
48
|
+
' When this status code is returned to your server, the' \
|
49
|
+
' receipt data is also decoded and returned as part of'\
|
50
|
+
' the response.'
|
51
|
+
add 21_007, 'This receipt is from the test environment, but it was' \
|
52
|
+
' sent to the production environment for verification.' \
|
53
|
+
' Send it to the test environment instead.'
|
54
|
+
add 21_008, 'This receipt is from the production environment, but it' \
|
55
|
+
' was sent to the test environment for verification.' \
|
56
|
+
' Send it to the production environment instead.'
|
57
|
+
freeze!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module CandyCheck
|
2
|
+
module AppStore
|
3
|
+
# Verifies receipts against the verification servers.
|
4
|
+
# The call return either an {Receipt} or a {VerificationFailure}
|
5
|
+
class Verifier
|
6
|
+
# HTTPS endpoint for production receipts
|
7
|
+
PRODUCTION_ENDPOINT = 'https://buy.itunes.apple.com/verifyReceipt'
|
8
|
+
# HTTPS endpoint for sandbox receipts
|
9
|
+
SANDBOX_ENDPOINT = 'https://sandbox.itunes.apple.com/verifyReceipt'
|
10
|
+
# Status code from production endpoint when receiving a sandbox
|
11
|
+
# receipt which occurs during the app's review process
|
12
|
+
REDIRECT_TO_SANDBOX_CODE = 21_007
|
13
|
+
# Status code from the sandbox endpoint when receiving a production
|
14
|
+
# receipt
|
15
|
+
REDIRECT_TO_PRODUCTION_CODE = 21_008
|
16
|
+
|
17
|
+
# @return [Config] the current configuration
|
18
|
+
attr_reader :config
|
19
|
+
|
20
|
+
# Initializes a new verifier for the application which is bound
|
21
|
+
# to a configuration
|
22
|
+
# @param config [Config]
|
23
|
+
def initialize(config)
|
24
|
+
@config = config
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calls a verification for the given input
|
28
|
+
# @param receipt_data [String] the raw data to be verified
|
29
|
+
# @param secret [String] the optional shared secret
|
30
|
+
# @return [Receipt] if successful
|
31
|
+
# @return [VerificationFailure] otherwise
|
32
|
+
def verify(receipt_data, secret = nil)
|
33
|
+
default_endpoint, opposite_endpoint = endpoints
|
34
|
+
result = call_for(default_endpoint, receipt_data, secret)
|
35
|
+
if should_retry?(result)
|
36
|
+
return call_for(opposite_endpoint, receipt_data, secret)
|
37
|
+
end
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def call_for(endpoint_url, receipt_data, secret)
|
44
|
+
Verification.new(endpoint_url, receipt_data, secret).call!
|
45
|
+
end
|
46
|
+
|
47
|
+
def should_retry?(result)
|
48
|
+
result.is_a?(VerificationFailure) && redirect?(result)
|
49
|
+
end
|
50
|
+
|
51
|
+
def endpoints
|
52
|
+
if config.production?
|
53
|
+
[PRODUCTION_ENDPOINT, SANDBOX_ENDPOINT]
|
54
|
+
else
|
55
|
+
[SANDBOX_ENDPOINT, PRODUCTION_ENDPOINT]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def redirect_code
|
60
|
+
config.production? ? REDIRECT_TO_SANDBOX_CODE :
|
61
|
+
REDIRECT_TO_PRODUCTION_CODE
|
62
|
+
end
|
63
|
+
|
64
|
+
def redirect?(failure)
|
65
|
+
failure.code == redirect_code
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'candy_check/app_store/client'
|
2
|
+
require 'candy_check/app_store/config'
|
3
|
+
require 'candy_check/app_store/receipt'
|
4
|
+
require 'candy_check/app_store/verification'
|
5
|
+
require 'candy_check/app_store/verification_failure'
|
6
|
+
require 'candy_check/app_store/verifier'
|
7
|
+
|
8
|
+
module CandyCheck
|
9
|
+
# Module to request and verify a AppStore receipt
|
10
|
+
module AppStore
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,102 @@
|
|
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'
|
19
|
+
# API scope for Android services
|
20
|
+
API_SCOPE = 'https://www.googleapis.com/auth/androidpublisher'
|
21
|
+
# API discovery namespace
|
22
|
+
API_DISCOVER = 'androidpublisher'
|
23
|
+
# API version
|
24
|
+
API_VERSION = 'v2'
|
25
|
+
|
26
|
+
# Initializes a client using a configuration.
|
27
|
+
# @param config [ClientConfig]
|
28
|
+
def initialize(config)
|
29
|
+
self.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
|
+
self.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
|
+
api_client.execute(
|
52
|
+
api_method: rpc.purchases.products.get,
|
53
|
+
parameters: {
|
54
|
+
'packageName' => package,
|
55
|
+
'productId' => product_id,
|
56
|
+
'token' => token
|
57
|
+
}
|
58
|
+
).data.to_hash
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
attr_accessor :config, :api_client, :rpc
|
64
|
+
|
65
|
+
def discover!
|
66
|
+
self.rpc = load_discover_dump || request_discover
|
67
|
+
validate_rpc!
|
68
|
+
write_discover_dump
|
69
|
+
end
|
70
|
+
|
71
|
+
def request_discover
|
72
|
+
api_client.discovered_api(API_DISCOVER, API_VERSION)
|
73
|
+
end
|
74
|
+
|
75
|
+
def authorize!
|
76
|
+
api_client.authorization = Signet::OAuth2::Client.new(
|
77
|
+
token_credential_uri: API_URL,
|
78
|
+
audience: API_URL,
|
79
|
+
scope: API_SCOPE,
|
80
|
+
issuer: config.issuer,
|
81
|
+
signing_key: config.api_key
|
82
|
+
)
|
83
|
+
api_client.authorization.fetch_access_token!
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_rpc!
|
87
|
+
return if rpc.purchases.products.get
|
88
|
+
fail DiscoveryError, 'Unable to get the API discovery'
|
89
|
+
rescue NoMethodError
|
90
|
+
raise DiscoveryError, 'Unable to get the API discovery'
|
91
|
+
end
|
92
|
+
|
93
|
+
def load_discover_dump
|
94
|
+
DiscoveryRepository.new(config.cache_file).load
|
95
|
+
end
|
96
|
+
|
97
|
+
def write_discover_dump
|
98
|
+
DiscoveryRepository.new(config.cache_file).save(rpc)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|