iap_verifier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b3c46ebc360b35276b2ef9a7d66f55e38a5a999d
4
+ data.tar.gz: 8888b7705e0a972d34571b50b676c3fd8e92000c
5
+ SHA512:
6
+ metadata.gz: d9cb9d90072e7f2319d368b4fd1a0e17e20133338caa61ad97e52102be1d6e8838e3082faeb98805e06754e549041014fe6d6608f5e5868e5d606f6e278b7c1f
7
+ data.tar.gz: 3ce5852c0950034c91bb9e69250a4fd3b36ffeea0f4fafded4bff20dca00683b7b6594747693ec1ff62b704a5fc9a60e7c3d9351d66c33fc9c89f2c312ea7b7c
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - head
4
+ - 2.1.2
5
+ - rbx-2.1.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # CHANGELOG
2
+
3
+ ## master
4
+
5
+ ## v0.0.1
6
+
7
+ Initial release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in iap_verifier.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jun Lin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # IAPVerifier
2
+
3
+ In-App Purchase receipt verification.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'iap_verifier'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install iap_verifier
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ receipt = IAPVerifier.verify_receipt(receipt: 'base64_encoded_receipt_string')
23
+ receipt.original_application_version # 1234
24
+ receipt.original_purchase_date_pst # 2014-08-31 11:24:13 America/Los_Angeles
25
+ ```
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( https://github.com/linjunpop/iap_verifier/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test" # here is the test_helper
7
+ t.test_files = FileList['test/*_test.rb']
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'iap_verifier/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "iap_verifier"
8
+ spec.version = IAPVerifier::VERSION
9
+ spec.authors = ["Jun Lin"]
10
+ spec.email = ["linjunpop@gmail.com"]
11
+ spec.summary = %q{In-App Purchase receipt verification.}
12
+ spec.description = %q{In-App Purchase receipt verification.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "pry"
24
+ spec.add_development_dependency "mocha"
25
+ spec.add_development_dependency "minitest-reporters"
26
+ spec.add_development_dependency "webmock"
27
+ spec.add_development_dependency "vcr"
28
+ end
@@ -0,0 +1,55 @@
1
+ module IAPVerifier
2
+ module Error
3
+ class Standard < StandardError; end
4
+
5
+ class EmptyReceipt < Standard
6
+ def message
7
+ 'Receipt cannot be empty.'
8
+ end
9
+ end
10
+
11
+ class NetworkDown < Standard
12
+ def initialize(response)
13
+ @response = response
14
+ end
15
+
16
+ def message
17
+ "Server returns unexpected code: #{@response.code}."
18
+ end
19
+ end
20
+
21
+ class MalformedReceiptData < Standard
22
+ def initialize(data)
23
+ @data = data
24
+ end
25
+
26
+ def message
27
+ "Malformed Receipt data: #{@data}"
28
+ end
29
+ end
30
+
31
+ class InvalidResponseData < Standard
32
+ MESSAGES = {
33
+ 21000 => "App store could not read",
34
+ 21002 => "Data was malformed",
35
+ 21003 => "Receipt not authenticated",
36
+ 21004 => "Shared secret does not match",
37
+ 21005 => "Receipt server unavailable",
38
+ 21006 => "Receipt valid but sub expired",
39
+ 21007 => "Sandbox receipt sent to Production environment",
40
+ 21008 => "Production receipt sent to Sandbox environment"
41
+ }
42
+
43
+ def initialize(code)
44
+ @code = code
45
+ end
46
+
47
+ def message
48
+ {
49
+ error_code: @code,
50
+ message: MESSAGES.fetch(@code, 'Unknown error code')
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,19 @@
1
+ module IAPVerifier
2
+ class Receipt
3
+ def initialize(response_data)
4
+ @receipt_data = response_data['receipt']
5
+
6
+ unless @receipt_data.is_a?(Hash)
7
+ raise Error::MalformedReceiptData
8
+ end
9
+
10
+ @receipt_data.each do |key, value|
11
+ define_singleton_method key.to_s, -> { value }
12
+ end
13
+ end
14
+
15
+ def to_h
16
+ @receipt_data
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ require 'json'
2
+ require 'net/https'
3
+ require 'uri'
4
+
5
+ module IAPVerifier
6
+ class Request
7
+ PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt"
8
+ SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
9
+
10
+ def initialize(receipt:)
11
+ @request_data = RequestData.new(receipt)
12
+ unless receipt.length > 0
13
+ raise Error::EmptyReceipt.new
14
+ end
15
+ end
16
+
17
+ def response
18
+ verify_with_retry(@request_data)
19
+ end
20
+
21
+ private
22
+
23
+ def verify_with_retry(request_data)
24
+ response_data = verify(request_data, PRODUCTION_URL)
25
+
26
+ if response_data.sandbox?
27
+ response_data = verify(request_data, SANDBOX_URL)
28
+ end
29
+
30
+ response_data
31
+ end
32
+
33
+ def verify(request_data, url)
34
+ uri = URI(url)
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+
37
+ http.use_ssl = true
38
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
39
+
40
+ request = Net::HTTP::Post.new(uri.request_uri)
41
+ request['Content-Type'] = "application/json"
42
+ request['Accept'] = "application/json"
43
+ request.body = request_data.to_json
44
+
45
+ response = http.request(request)
46
+
47
+ if response.instance_of?(Net::HTTPOK)
48
+ ResponseData.new(JSON.parse(response.body))
49
+ else
50
+ raise Error::NetworkDown.new(response)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ module IAPVerifier
2
+ class RequestData
3
+ def initialize(receipt)
4
+ @receipt = receipt
5
+ end
6
+
7
+ def to_h
8
+ {
9
+ 'receipt-data' => @receipt
10
+ }
11
+ end
12
+
13
+ def to_json
14
+ to_h.to_json
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module IAPVerifier
2
+ class ResponseData
3
+ SUCCESS_RESPONSE_CODE = 0
4
+ SANDBOX_RECEIPT_CODE = 21007
5
+
6
+ def initialize(response_data)
7
+ @response_data = response_data
8
+ end
9
+
10
+ def valid?
11
+ status_code == SUCCESS_RESPONSE_CODE
12
+ end
13
+
14
+ def sandbox?
15
+ status_code == SANDBOX_RECEIPT_CODE
16
+ end
17
+
18
+ def receipt
19
+ if valid?
20
+ Receipt.new(@response_data)
21
+ else
22
+ raise Error::InvalidResponseData.new(status_code)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def status_code
29
+ @response_data['status']
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module IAPVerifier
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,22 @@
1
+ require "iap_verifier/version"
2
+ require 'iap_verifier/error'
3
+ require 'iap_verifier/request'
4
+ require 'iap_verifier/request_data'
5
+ require 'iap_verifier/response_data'
6
+ require 'iap_verifier/receipt'
7
+
8
+ module IAPVerifier
9
+ class << self
10
+ def verify_receipt!(receipt:)
11
+ IAPVerifier::Request.new(receipt: receipt).response.receipt
12
+ end
13
+
14
+ def verify_receipt(receipt:)
15
+ begin
16
+ verify_receipt!(receipt: receipt)
17
+ rescue IAPVerifier::Error::Standard
18
+ return nil
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1 @@
1
+ ewoJInNpZ25hdHVyZSIgPSAiQXBNVUJDODZBbHpOaWtWNVl0clpBTWlKUWJLOEVkZVhrNjNrV0JBWHpsQzhkWEd1anE0N1puSVlLb0ZFMW9OL0ZTOGNYbEZmcDlZWHQ5aU1CZEwyNTBsUlJtaU5HYnloaXRyeVlWQVFvcmkzMlc5YVIwVDhML2FZVkJkZlcrT3kvUXlQWkVtb05LeGhudDJXTlNVRG9VaFo4Wis0cFA3MHBlNWtVUWxiZElWaEFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5USXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1YvcnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRkS1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNqQndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBnRVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERnUVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lCM1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNlVFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIwN2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FRVnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xvSHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5qK2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2JwMGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREV5TFRBMExUTXdJREE0T2pBMU9qVTFJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkltOXlhV2RwYm1Gc0xYUnlZVzV6WVdOMGFXOXVMV2xrSWlBOUlDSXhNREF3TURBd01EUTJNVGM0T0RFM0lqc0tDU0ppZG5KeklpQTlJQ0l5TURFeU1EUXlOeUk3Q2draWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqRXdNREF3TURBd05EWXhOemc0TVRjaU93b0pJbkYxWVc1MGFYUjVJaUE5SUNJeElqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbExXMXpJaUE5SUNJeE16TTFOems0TXpVMU9EWTRJanNLQ1NKd2NtOWtkV04wTFdsa0lpQTlJQ0pqYjIwdWJXbHVaRzF2WW1Gd2NDNWtiM2R1Ykc5aFpDSTdDZ2tpYVhSbGJTMXBaQ0lnUFNBaU5USXhNVEk1T0RFeUlqc0tDU0ppYVdRaUlEMGdJbU52YlM1dGFXNWtiVzlpWVhCd0xrMXBibVJOYjJJaU93b0pJbkIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpFek16VTNPVGd6TlRVNE5qZ2lPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVWlJRDBnSWpJd01USXRNRFF0TXpBZ01UVTZNRFU2TlRVZ1JYUmpMMGROVkNJN0Nna2ljSFZ5WTJoaGMyVXRaR0YwWlMxd2MzUWlJRDBnSWpJd01USXRNRFF0TXpBZ01EZzZNRFU2TlRVZ1FXMWxjbWxqWVM5TWIzTmZRVzVuWld4bGN5STdDZ2tpYjNKcFoybHVZV3d0Y0hWeVkyaGhjMlV0WkdGMFpTSWdQU0FpTWpBeE1pMHdOQzB6TUNBeE5Ub3dOVG8xTlNCRmRHTXZSMDFVSWpzS2ZRPT0iOwoJImVudmlyb25tZW50IiA9ICJTYW5kYm94IjsKCSJwb2QiID0gIjEwMCI7Cgkic2lnbmluZy1zdGF0dXMiID0gIjAiOwp9
@@ -0,0 +1,150 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://buy.itunes.apple.com/verifyReceipt
6
+ body:
7
+ encoding: UTF-8
8
+ string: '{"receipt-data":"ewoJInNpZ25hdHVyZSIgPSAiQXBNVUJDODZBbHpOaWtWNVl0clpBTWlKUWJLOEVkZVhrNjNrV0JBWHpsQzhkWEd1anE0N1puSVlLb0ZFMW9OL0ZTOGNYbEZmcDlZWHQ5aU1CZEwyNTBsUlJtaU5HYnloaXRyeVlWQVFvcmkzMlc5YVIwVDhML2FZVkJkZlcrT3kvUXlQWkVtb05LeGhudDJXTlNVRG9VaFo4Wis0cFA3MHBlNWtVUWxiZElWaEFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5USXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1YvcnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRkS1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNqQndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBnRVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERnUVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lCM1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNlVFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIwN2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FRVnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xvSHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5qK2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2JwMGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREV5TFRBMExUTXdJREE0T2pBMU9qVTFJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkltOXlhV2RwYm1Gc0xYUnlZVzV6WVdOMGFXOXVMV2xrSWlBOUlDSXhNREF3TURBd01EUTJNVGM0T0RFM0lqc0tDU0ppZG5KeklpQTlJQ0l5TURFeU1EUXlOeUk3Q2draWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqRXdNREF3TURBd05EWXhOemc0TVRjaU93b0pJbkYxWVc1MGFYUjVJaUE5SUNJeElqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbExXMXpJaUE5SUNJeE16TTFOems0TXpVMU9EWTRJanNLQ1NKd2NtOWtkV04wTFdsa0lpQTlJQ0pqYjIwdWJXbHVaRzF2WW1Gd2NDNWtiM2R1Ykc5aFpDSTdDZ2tpYVhSbGJTMXBaQ0lnUFNBaU5USXhNVEk1T0RFeUlqc0tDU0ppYVdRaUlEMGdJbU52YlM1dGFXNWtiVzlpWVhCd0xrMXBibVJOYjJJaU93b0pJbkIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpFek16VTNPVGd6TlRVNE5qZ2lPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVWlJRDBnSWpJd01USXRNRFF0TXpBZ01UVTZNRFU2TlRVZ1JYUmpMMGROVkNJN0Nna2ljSFZ5WTJoaGMyVXRaR0YwWlMxd2MzUWlJRDBnSWpJd01USXRNRFF0TXpBZ01EZzZNRFU2TlRVZ1FXMWxjbWxqWVM5TWIzTmZRVzVuWld4bGN5STdDZ2tpYjNKcFoybHVZV3d0Y0hWeVkyaGhjMlV0WkdGMFpTSWdQU0FpTWpBeE1pMHdOQzB6TUNBeE5Ub3dOVG8xTlNCRmRHTXZSMDFVSWpzS2ZRPT0iOwoJImVudmlyb25tZW50IiA9ICJTYW5kYm94IjsKCSJwb2QiID0gIjEwMCI7Cgkic2lnbmluZy1zdGF0dXMiID0gIjAiOwp9"}'
9
+ headers:
10
+ Accept-Encoding:
11
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
12
+ Accept:
13
+ - application/json
14
+ User-Agent:
15
+ - Ruby
16
+ Content-Type:
17
+ - application/json
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: Apple WebObjects
22
+ headers:
23
+ X-Apple-Jingle-Correlation-Key:
24
+ - 2YZEYGN63OI7E
25
+ Pod:
26
+ - '53'
27
+ X-Apple-Translated-Wo-Url:
28
+ - "/WebObjects/MZFinance.woa/wa/verifyReceipt"
29
+ X-Apple-Orig-Url:
30
+ - http://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/verifyReceipt
31
+ X-Apple-Application-Site:
32
+ - ST13
33
+ Edge-Control:
34
+ - cache-maxage=0
35
+ - no-store
36
+ Date:
37
+ - Mon, 01 Sep 2014 23:29:26 GMT
38
+ Set-Cookie:
39
+ - NSC_nagjobodf-bopo-qppm*0=ffffffff12a5a8e645525d5f4f58455e445a4a423660;path=/;secure;httponly
40
+ - Pod=53; version="1"; expires=Wed, 01-Oct-2014 23:29:26 GMT; path=/; domain=.apple.com
41
+ - itspod=53; version="1"; expires=Wed, 01-Oct-2014 23:29:26 GMT; path=/; domain=.apple.com
42
+ - mzf_dr=0; version="1"; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=/WebObjects;
43
+ domain=.apple.com
44
+ - mzf_in=532400; version="1"; path=/WebObjects; domain=.apple.com; secure
45
+ - ns-mzf-inst=182-247-80-217-23-8155-532400-53-st13; version=1; Max-Age=1800;
46
+ path=/; domain=.apple.com; httponly
47
+ Apple-Timing-App:
48
+ - 533 ms
49
+ Cache-Control:
50
+ - max-age=0
51
+ - must-revalidate
52
+ - no-cache
53
+ - no-store
54
+ - no-transform
55
+ - private
56
+ Expires:
57
+ - Mon, 01 Sep 2014 23:29:26 GMT
58
+ X-Apple-Lokamai-No-Cache:
59
+ - 'true'
60
+ X-Apple-Application-Instance:
61
+ - '532400'
62
+ X-Frame-Options:
63
+ - SAMEORIGIN
64
+ Itspod:
65
+ - '53'
66
+ X-Webobjects-Loadaverage:
67
+ - '24'
68
+ Connection:
69
+ - keep-alive
70
+ Content-Length:
71
+ - '36'
72
+ body:
73
+ encoding: UTF-8
74
+ string: '{"status":21007}'
75
+ http_version:
76
+ recorded_at: Mon, 01 Sep 2014 23:29:26 GMT
77
+ - request:
78
+ method: post
79
+ uri: https://sandbox.itunes.apple.com/verifyReceipt
80
+ body:
81
+ encoding: UTF-8
82
+ string: '{"receipt-data":"ewoJInNpZ25hdHVyZSIgPSAiQXBNVUJDODZBbHpOaWtWNVl0clpBTWlKUWJLOEVkZVhrNjNrV0JBWHpsQzhkWEd1anE0N1puSVlLb0ZFMW9OL0ZTOGNYbEZmcDlZWHQ5aU1CZEwyNTBsUlJtaU5HYnloaXRyeVlWQVFvcmkzMlc5YVIwVDhML2FZVkJkZlcrT3kvUXlQWkVtb05LeGhudDJXTlNVRG9VaFo4Wis0cFA3MHBlNWtVUWxiZElWaEFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NHVVVrVTNaV0FTMU1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEE1TURZeE5USXlNRFUxTmxvWERURTBNRFl4TkRJeU1EVTFObG93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNclJqRjJjdDRJclNkaVRDaGFJMGc4cHd2L2NtSHM4cC9Sd1YvcnQvOTFYS1ZoTmw0WElCaW1LalFRTmZnSHNEczZ5anUrK0RyS0pFN3VLc3BoTWRkS1lmRkU1ckdYc0FkQkVqQndSSXhleFRldngzSExFRkdBdDFtb0t4NTA5ZGh4dGlJZERnSnYyWWFWczQ5QjB1SnZOZHk2U01xTk5MSHNETHpEUzlvWkhBZ01CQUFHamNqQndNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOaDNvNHAyQzBnRVl0VEpyRHRkREM1RllRem93RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZERnUVdCQlNwZzRQeUdVakZQaEpYQ0JUTXphTittVjhrOVRBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQUVhU2JQanRtTjRDL0lCM1FFcEszMlJ4YWNDRFhkVlhBZVZSZVM1RmFaeGMrdDg4cFFQOTNCaUF4dmRXLzNlVFNNR1k1RmJlQVlMM2V0cVA1Z204d3JGb2pYMGlreVZSU3RRKy9BUTBLRWp0cUIwN2tMczlRVWU4Y3pSOFVHZmRNMUV1bVYvVWd2RGQ0TndOWXhMUU1nNFdUUWZna1FRVnk4R1had1ZIZ2JFL1VDNlk3MDUzcEdYQms1MU5QTTN3b3hoZDNnU1JMdlhqK2xvSHNTdGNURXFlOXBCRHBtRzUrc2s0dHcrR0szR01lRU41LytlMVFUOW5wL0tsMW5qK2FCdzdDMHhzeTBiRm5hQWQxY1NTNnhkb3J5L0NVdk02Z3RLc21uT09kcVRlc2JwMGJzOHNuNldxczBDOWRnY3hSSHVPTVoydG04bnBMVW03YXJnT1N6UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREV5TFRBMExUTXdJREE0T2pBMU9qVTFJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkltOXlhV2RwYm1Gc0xYUnlZVzV6WVdOMGFXOXVMV2xrSWlBOUlDSXhNREF3TURBd01EUTJNVGM0T0RFM0lqc0tDU0ppZG5KeklpQTlJQ0l5TURFeU1EUXlOeUk3Q2draWRISmhibk5oWTNScGIyNHRhV1FpSUQwZ0lqRXdNREF3TURBd05EWXhOemc0TVRjaU93b0pJbkYxWVc1MGFYUjVJaUE5SUNJeElqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbExXMXpJaUE5SUNJeE16TTFOems0TXpVMU9EWTRJanNLQ1NKd2NtOWtkV04wTFdsa0lpQTlJQ0pqYjIwdWJXbHVaRzF2WW1Gd2NDNWtiM2R1Ykc5aFpDSTdDZ2tpYVhSbGJTMXBaQ0lnUFNBaU5USXhNVEk1T0RFeUlqc0tDU0ppYVdRaUlEMGdJbU52YlM1dGFXNWtiVzlpWVhCd0xrMXBibVJOYjJJaU93b0pJbkIxY21Ob1lYTmxMV1JoZEdVdGJYTWlJRDBnSWpFek16VTNPVGd6TlRVNE5qZ2lPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVWlJRDBnSWpJd01USXRNRFF0TXpBZ01UVTZNRFU2TlRVZ1JYUmpMMGROVkNJN0Nna2ljSFZ5WTJoaGMyVXRaR0YwWlMxd2MzUWlJRDBnSWpJd01USXRNRFF0TXpBZ01EZzZNRFU2TlRVZ1FXMWxjbWxqWVM5TWIzTmZRVzVuWld4bGN5STdDZ2tpYjNKcFoybHVZV3d0Y0hWeVkyaGhjMlV0WkdGMFpTSWdQU0FpTWpBeE1pMHdOQzB6TUNBeE5Ub3dOVG8xTlNCRmRHTXZSMDFVSWpzS2ZRPT0iOwoJImVudmlyb25tZW50IiA9ICJTYW5kYm94IjsKCSJwb2QiID0gIjEwMCI7Cgkic2lnbmluZy1zdGF0dXMiID0gIjAiOwp9"}'
83
+ headers:
84
+ Accept-Encoding:
85
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
86
+ Accept:
87
+ - application/json
88
+ User-Agent:
89
+ - Ruby
90
+ Content-Type:
91
+ - application/json
92
+ response:
93
+ status:
94
+ code: 200
95
+ message: Apple WebObjects
96
+ headers:
97
+ X-Apple-Jingle-Correlation-Key:
98
+ - I7RNKZTLOR2JI
99
+ Pod:
100
+ - '100'
101
+ X-Apple-Translated-Wo-Url:
102
+ - "/WebObjects/MZFinance.woa/wa/verifyReceipt"
103
+ X-Apple-Orig-Url:
104
+ - http://sandbox.itunes.apple.com/WebObjects/MZFinance.woa/wa/verifyReceipt
105
+ X-Apple-Application-Site:
106
+ - SB
107
+ Edge-Control:
108
+ - cache-maxage=0
109
+ - no-store
110
+ Set-Cookie:
111
+ - Pod=100; version="1"; expires=Wed, 01-Oct-2014 23:29:27 GMT; path=/; domain=.apple.com
112
+ - itspod=100; version="1"; expires=Wed, 01-Oct-2014 23:29:27 GMT; path=/; domain=.apple.com
113
+ - mzf_dr=0; version="1"; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=/WebObjects;
114
+ domain=.apple.com
115
+ - mzf_in=990140; version="1"; path=/WebObjects; domain=.apple.com; secure
116
+ Apple-Timing-App:
117
+ - 11 ms
118
+ Cache-Control:
119
+ - max-age=0
120
+ - must-revalidate
121
+ - no-cache
122
+ - no-store
123
+ - no-transform
124
+ - private
125
+ Expires:
126
+ - Mon, 01 Sep 2014 23:29:27 GMT
127
+ X-Apple-Lokamai-No-Cache:
128
+ - 'true'
129
+ X-Apple-Application-Instance:
130
+ - '990140'
131
+ X-Frame-Options:
132
+ - SAMEORIGIN
133
+ Itspod:
134
+ - '100'
135
+ X-Webobjects-Loadaverage:
136
+ - '0'
137
+ Connection:
138
+ - keep-alive
139
+ Content-Length:
140
+ - '259'
141
+ Date:
142
+ - Mon, 01 Sep 2014 23:29:27 GMT
143
+ body:
144
+ encoding: UTF-8
145
+ string: |-
146
+ {
147
+ "receipt":{"original_purchase_date_pst":"2012-04-30 08:05:55 America/Los_Angeles", "original_transaction_id":"1000000046178817", "original_purchase_date_ms":"1335798355868", "transaction_id":"1000000046178817", "quantity":"1", "product_id":"com.mindmobapp.download", "bvrs":"20120427", "purchase_date_ms":"1335798355868", "purchase_date":"2012-04-30 15:05:55 Etc/GMT", "original_purchase_date":"2012-04-30 15:05:55 Etc/GMT", "purchase_date_pst":"2012-04-30 08:05:55 America/Los_Angeles", "bid":"com.mindmobapp.MindMob", "item_id":"521129812"}, "status":0}
148
+ http_version:
149
+ recorded_at: Mon, 01 Sep 2014 23:29:27 GMT
150
+ recorded_with: VCR 2.9.2
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+ require 'iap_verifier/receipt'
3
+
4
+ class ReceiptTest < MiniTest::Test
5
+ def test_method_missing
6
+ receipt = IAPVerifier::Receipt.new({ 'receipt' => { 'foo' => 'bar' } })
7
+
8
+ assert_equal 'bar', receipt.foo
9
+ assert_raises(NoMethodError) do
10
+ receipt.bar
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+ require 'iap_verifier/request_data'
3
+
4
+ class RequestDataTest < MiniTest::Test
5
+ def test_base64_receipt
6
+ receipt = File.read(File.expand_path("../fixtures/base64_receipt", __FILE__)).chop
7
+
8
+ expected = {
9
+ 'receipt-data' => receipt
10
+ }
11
+ assert_equal expected, IAPVerifier::RequestData.new(receipt).to_h
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ require 'test_helper'
2
+ require 'iap_verifier/request'
3
+
4
+ class RequestTest < MiniTest::Test
5
+ def test_empty_receipt
6
+ assert_raises(IAPVerifier::Error::EmptyReceipt) do
7
+ IAPVerifier::Request.new(receipt: '')
8
+ end
9
+ end
10
+
11
+ def test_response
12
+ VCR.use_cassette('valid_receipt') do
13
+ receipt = File.read(File.expand_path("../fixtures/base64_receipt", __FILE__)).chop
14
+ response_data = IAPVerifier::Request.new(receipt: receipt).response
15
+
16
+ assert_equal true, response_data.valid?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ require 'test_helper'
2
+ require 'iap_verifier/response_data'
3
+ require 'iap_verifier/receipt'
4
+ require 'iap_verifier/error'
5
+
6
+ class ResponseDataTest < MiniTest::Test
7
+ def test_success_response_json
8
+ success_response_json = { 'status' => 0, 'receipt' => { } }
9
+ response_data = IAPVerifier::ResponseData.new(success_response_json)
10
+
11
+ assert_equal true, response_data.valid?
12
+ end
13
+
14
+ def test_error_response_json
15
+ error_response_json = { 'status' => 21002 }
16
+ response_data = IAPVerifier::ResponseData.new(error_response_json)
17
+
18
+ assert_equal false, response_data.valid?
19
+ end
20
+
21
+ def test_sandbox_response_json
22
+ # sandbox receipt sent to production server
23
+ sandbox_response_json = { 'status' => 21007 }
24
+ response_data = IAPVerifier::ResponseData.new(sandbox_response_json)
25
+
26
+ assert_equal false, response_data.valid?
27
+ assert_equal true, response_data.sandbox?
28
+ end
29
+
30
+ def test_receipt
31
+ response_data = IAPVerifier::ResponseData.new({ 'receipt' => { } })
32
+ response_data.stubs(:valid?).returns(true)
33
+
34
+ assert_equal true, response_data.receipt.instance_of?(IAPVerifier::Receipt)
35
+ end
36
+
37
+ def test_invalid_response_data
38
+ assert_raises(IAPVerifier::Error::InvalidResponseData) do
39
+ response_data = IAPVerifier::ResponseData.new({ 'receipt' => { } })
40
+ response_data.stubs(:valid?).returns(false)
41
+
42
+ response_data.receipt
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ require 'minitest/autorun'
2
+ require "minitest/reporters"
3
+ require 'mocha/mini_test'
4
+ require 'webmock/minitest'
5
+ require 'vcr'
6
+
7
+ Minitest::Reporters.use!
8
+
9
+ VCR.configure do |c|
10
+ c.cassette_library_dir = File.expand_path( '../fixtures/vcr_cassettes', __FILE__)
11
+ c.hook_into :webmock # or :fakeweb
12
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iap_verifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jun Lin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mocha
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-reporters
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: In-App Purchase receipt verification.
112
+ email:
113
+ - linjunpop@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
+ - CHANGELOG.md
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - iap_verifier.gemspec
126
+ - lib/iap_verifier.rb
127
+ - lib/iap_verifier/error.rb
128
+ - lib/iap_verifier/receipt.rb
129
+ - lib/iap_verifier/request.rb
130
+ - lib/iap_verifier/request_data.rb
131
+ - lib/iap_verifier/response_data.rb
132
+ - lib/iap_verifier/version.rb
133
+ - test/fixtures/base64_receipt
134
+ - test/fixtures/vcr_cassettes/valid_receipt.yml
135
+ - test/receipt_test.rb
136
+ - test/request_data_test.rb
137
+ - test/request_test.rb
138
+ - test/response_data_test.rb
139
+ - test/test_helper.rb
140
+ homepage: ''
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubyforge_project:
160
+ rubygems_version: 2.2.2
161
+ signing_key:
162
+ specification_version: 4
163
+ summary: In-App Purchase receipt verification.
164
+ test_files:
165
+ - test/fixtures/base64_receipt
166
+ - test/fixtures/vcr_cassettes/valid_receipt.yml
167
+ - test/receipt_test.rb
168
+ - test/request_data_test.rb
169
+ - test/request_test.rb
170
+ - test/response_data_test.rb
171
+ - test/test_helper.rb