├── .rspec ├── lib ├── monza │ ├── version.rb │ ├── config.rb │ ├── bool_typecasting.rb │ ├── client.rb │ ├── transaction_receipt.rb │ ├── verification_response.rb │ └── receipt.rb └── monza.rb ├── spec ├── spec_helper.rb ├── monza_spec.rb ├── client_spec.rb ├── response.json ├── bad_response.json ├── receipt_spec.rb ├── cancellation_response.json └── verification_response_spec.rb ├── Gemfile ├── Rakefile ├── bin ├── setup └── console ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── CODE_OF_CONDUCT.md ├── monza.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/monza/version.rb: -------------------------------------------------------------------------------- 1 | module Monza 2 | VERSION = "1.1.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'monza' 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in monza.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.iml 11 | .DS_Store 12 | .idea 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | 5 | rvm: 6 | - 2.2.7 7 | - 2.3.4 8 | - 2.4.1 9 | - ruby-head 10 | 11 | before_install: gem install bundler -v 1.10.6 12 | -------------------------------------------------------------------------------- /lib/monza.rb: -------------------------------------------------------------------------------- 1 | require "monza/version" 2 | require "monza/client" 3 | require "monza/verification_response" 4 | require "monza/receipt" 5 | require "monza/transaction_receipt" 6 | require "monza/bool_typecasting" 7 | require "monza/config" 8 | -------------------------------------------------------------------------------- /lib/monza/config.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'active_support/core_ext/time' 3 | 4 | module Monza 5 | 6 | # Set default for Time zone if none has been set 7 | # Default is UTC 8 | Time.zone = Time.zone ? Time.zone : "UTC" 9 | 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/monza_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Monza do 4 | it 'has a version number' do 5 | expect(Monza::VERSION).not_to be nil 6 | end 7 | 8 | # it 'does something useful' do 9 | # expect(false).to eq(true) 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "monza" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Monza::Client do 4 | describe 'verification_url' do 5 | context 'development' do 6 | it 'should have correct url' do 7 | expect(Monza::Client.development.verification_url).to eq 'https://sandbox.itunes.apple.com/verifyReceipt' 8 | end 9 | end 10 | 11 | context 'production' do 12 | it 'should have correct url' do 13 | expect(Monza::Client.production.verification_url).to eq 'https://buy.itunes.apple.com/verifyReceipt' 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/monza/bool_typecasting.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def to_bool 3 | return true if self == true || self =~ (/^(true|t|yes|y|1)$/i) 4 | return false if self == false || self.empty? || self =~ (/^(false|f|no|n|0)$/i) 5 | raise ArgumentError.new("invalid value for Boolean: \"#{self}\"") 6 | end 7 | # if using Rails empty? can be changed for blank? 8 | end 9 | 10 | if RUBY_VERSION >= "2.4" 11 | integer_class = Object.const_get("Integer") 12 | else 13 | integer_class = Object.const_get("Fixnum") 14 | end 15 | integer_class::class_eval do 16 | def to_bool 17 | return true if self == 1 18 | return false if self == 0 19 | raise ArgumentError.new("invalid value for Boolean: \"#{self}\"") 20 | end 21 | end 22 | 23 | class TrueClass 24 | def to_i; 1; end 25 | def to_bool; self; end 26 | end 27 | 28 | class FalseClass 29 | def to_i; 0; end 30 | def to_bool; self; end 31 | end 32 | 33 | class NilClass 34 | def to_bool; false; end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Gabriel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /monza.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'monza/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "monza" 8 | spec.version = Monza::VERSION 9 | spec.authors = ["Gabriel Garza"] 10 | spec.email = ["garzagabriel@gmail.com"] 11 | 12 | spec.summary = %q{Validate iTunes In-App purchase receipts, including auto-renewable subscriptions, with the App Store.} 13 | spec.description = %q{Validate iTunes In-App purchase receipts, including auto-renewable subscriptions, with the App Store.} 14 | spec.homepage = "https://github.com/gabrielgarza/monza" 15 | spec.license = "MIT" 16 | 17 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 18 | # delete this section to allow pushing this gem to any host. 19 | # if spec.respond_to?(:metadata) 20 | # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" 21 | # else 22 | # raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 23 | # end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "json" 31 | spec.add_dependency "activesupport" 32 | 33 | spec.add_development_dependency "bundler", "~> 1.10" 34 | spec.add_development_dependency "rake", "~> 10.0" 35 | spec.add_development_dependency "rspec" 36 | end 37 | -------------------------------------------------------------------------------- /lib/monza/client.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'net/https' 3 | require 'uri' 4 | 5 | module Monza 6 | class Client 7 | attr_accessor :verification_url 8 | attr_writer :shared_secret 9 | 10 | PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt" 11 | DEVELOPMENT_URL = "https://sandbox.itunes.apple.com/verifyReceipt" 12 | 13 | def self.development 14 | client = self.new 15 | client.verification_url = DEVELOPMENT_URL 16 | client 17 | end 18 | 19 | def self.production 20 | client = self.new 21 | client.verification_url = PRODUCTION_URL 22 | client 23 | end 24 | 25 | def initialize 26 | end 27 | 28 | def verify(data, options = {}) 29 | # Post to apple and receive json_response 30 | json_response = post_receipt_verification(data, options) 31 | # Get status code of response 32 | status = json_response['status'].to_i 33 | 34 | case status 35 | when 0 36 | begin 37 | return VerificationResponse.new(json_response) 38 | rescue 39 | nil 40 | end 41 | else 42 | raise VerificationResponse::VerificationError.new(status) 43 | end 44 | 45 | end 46 | 47 | private 48 | 49 | def post_receipt_verification(data, options = {}) 50 | parameters = { 51 | 'receipt-data' => data 52 | } 53 | 54 | parameters['password'] = options[:shared_secret] if options[:shared_secret] 55 | 56 | uri = URI(@verification_url) 57 | http = Net::HTTP.new(uri.host, uri.port) 58 | http.use_ssl = true 59 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 60 | 61 | request = Net::HTTP::Post.new(uri.request_uri) 62 | request['Accept'] = "application/json" 63 | request['Content-Type'] = "application/json" 64 | request.body = parameters.to_json 65 | 66 | response = http.request(request) 67 | 68 | JSON.parse(response.body) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 0, 3 | "environment": "Sandbox", 4 | "receipt": { 5 | "receipt_type": "ProductionSandbox", 6 | "adam_id": 0, 7 | "app_item_id": 0, 8 | "bundle_id": "com.example.app", 9 | "application_version": "58", 10 | "download_id": 100, 11 | "version_external_identifier": 0, 12 | "receipt_creation_date": "2016-06-17 01:54:26 Etc/GMT", 13 | "receipt_creation_date_ms": "1466128466000", 14 | "receipt_creation_date_pst": "2016-06-16 18:54:26 America/Los_Angeles", 15 | "request_date": "2016-06-17 17:34:41 Etc/GMT", 16 | "request_date_ms": "1466184881174", 17 | "request_date_pst": "2016-06-17 10:34:41 America/Los_Angeles", 18 | "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 19 | "original_purchase_date_ms": "1375340400000", 20 | "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 21 | "original_application_version": "1.0", 22 | "in_app": [ 23 | { 24 | "quantity": "1", 25 | "product_id": "com.example.product_id", 26 | "transaction_id": "1000000218147651", 27 | "original_transaction_id": "1000000218147500", 28 | "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 29 | "purchase_date_ms": "1466127148000", 30 | "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 31 | "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 32 | "original_purchase_date_ms": "1466127033000", 33 | "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 34 | "expires_date": "2016-06-17 01:37:28 Etc/GMT", 35 | "expires_date_ms": "1466127448000", 36 | "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 37 | "web_order_line_item_id": "1000000032727764", 38 | "is_trial_period": "false" 39 | } 40 | ] 41 | }, 42 | "latest_receipt_info": [ 43 | { 44 | "quantity": "1", 45 | "product_id": "com.example.product_id", 46 | "transaction_id": "1000000218147500", 47 | "original_transaction_id": "1000000218147500", 48 | "purchase_date": "2016-06-17 01:27:28 Etc/GMT", 49 | "purchase_date_ms": "1466126848000", 50 | "purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 51 | "original_purchase_date": "2016-06-17 01:27:28 Etc/GMT", 52 | "original_purchase_date_ms": "1466126848000", 53 | "original_purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 54 | "expires_date": "2016-06-17 01:32:28 Etc/GMT", 55 | "expires_date_ms": "1466127148000", 56 | "expires_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 57 | "web_order_line_item_id": "1000000032727765", 58 | "is_trial_period": "true" 59 | } 60 | ], 61 | "latest_receipt": "base 64 string" 62 | } 63 | -------------------------------------------------------------------------------- /spec/bad_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 21003, 3 | "environment": "Sandbox", 4 | "receipt": { 5 | "receipt_type": "ProductionSandbox", 6 | "adam_id": 0, 7 | "app_item_id": 0, 8 | "bundle_id": "com.example.bad_app", 9 | "application_version": "58", 10 | "download_id": 100, 11 | "version_external_identifier": 0, 12 | "receipt_creation_date": "2016-06-17 01:54:26 Etc/GMT", 13 | "receipt_creation_date_ms": "1466128466000", 14 | "receipt_creation_date_pst": "2016-06-16 18:54:26 America/Los_Angeles", 15 | "request_date": "2016-06-17 17:34:41 Etc/GMT", 16 | "request_date_ms": "1466184881174", 17 | "request_date_pst": "2016-06-17 10:34:41 America/Los_Angeles", 18 | "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 19 | "original_purchase_date_ms": "1375340400000", 20 | "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 21 | "original_application_version": "1.0", 22 | "in_app": [ 23 | { 24 | "quantity": "1", 25 | "product_id": "com.example.bad_product_id", 26 | "transaction_id": "1000000218147651", 27 | "original_transaction_id": "1000000218147500", 28 | "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 29 | "purchase_date_ms": "1466127148000", 30 | "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 31 | "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 32 | "original_purchase_date_ms": "1466127033000", 33 | "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 34 | "expires_date": "2016-06-17 01:37:28 Etc/GMT", 35 | "expires_date_ms": "1466127448000", 36 | "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 37 | "web_order_line_item_id": "1000000032727764", 38 | "is_trial_period": "false" 39 | } 40 | ] 41 | }, 42 | "latest_receipt_info": [ 43 | { 44 | "quantity": "1", 45 | "product_id": "com.example.bad_product_id", 46 | "transaction_id": "1000000218147500", 47 | "original_transaction_id": "1000000218147500", 48 | "purchase_date": "2016-06-17 01:27:28 Etc/GMT", 49 | "purchase_date_ms": "1466126848000", 50 | "purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 51 | "original_purchase_date": "2016-06-17 01:27:28 Etc/GMT", 52 | "original_purchase_date_ms": "1466126848000", 53 | "original_purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 54 | "expires_date": "2016-06-17 01:32:28 Etc/GMT", 55 | "expires_date_ms": "1466127148000", 56 | "expires_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 57 | "web_order_line_item_id": "1000000032727765", 58 | "is_trial_period": "true" 59 | } 60 | ], 61 | "latest_receipt": "base 64 string" 62 | } 63 | -------------------------------------------------------------------------------- /spec/receipt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Monza::Receipt do 4 | context 'verification receipt' do 5 | let(:response) { JSON.parse File.open("#{Dir.pwd}/spec/response.json", 'rb').read } 6 | let(:receipt) { described_class.new(response['receipt']) } 7 | 8 | # TODO: verify DateTime for: 9 | # + :@receipt_creation_date, 10 | # + :@receipt_creation_date_ms, 11 | # + :@receipt_creation_date_pst, 12 | # + :@request_date, 13 | # + :@request_date_ms, 14 | # + :@request_date_pst, 15 | # + :@original_purchase_date, 16 | # + :@original_purchase_date_ms, 17 | # + :@original_purchase_date_pst, 18 | 19 | it { expect(receipt.version_external_identifier).to eq 0 } 20 | it { expect(receipt.app_item_id).to eq 0 } 21 | it { expect(receipt.download_id).to eq 100 } 22 | it { expect(receipt.application_version).to eq '58' } 23 | it { expect(receipt.original_application_version).to eq '1.0' } 24 | it { expect(receipt.receipt_type).to eq 'ProductionSandbox' } 25 | it { expect(receipt.bundle_id).to eq 'com.example.app' } 26 | it 'time zone' do 27 | expect(receipt.receipt_creation_date).to eq DateTime.parse('2016-06-17 01:54:26 Etc/GMT') 28 | expect(receipt.receipt_creation_date_ms).to eq Time.zone.at("1466128466000".to_i / 1000) 29 | 30 | expect(receipt.request_date).to eq DateTime.parse('2016-06-17 17:34:41 Etc/GMT') 31 | expect(receipt.request_date_ms).to eq Time.zone.at("1466184881174".to_i / 1000) 32 | 33 | expect(receipt.original_purchase_date).to eq DateTime.parse('2013-08-01 07:00:00 Etc/GMT') 34 | expect(receipt.original_purchase_date_ms).to eq Time.zone.at("1375340400000".to_i / 1000) 35 | end 36 | 37 | it 'receipt' do 38 | in_app = receipt.in_app.first 39 | 40 | # TODO: vefify purchase_date and original_purchase_date 41 | 42 | expect(in_app).not_to be_nil 43 | expect(in_app.quantity).to eq 1 44 | expect(in_app.transaction_id).to eq '1000000218147651' 45 | expect(in_app.original_transaction_id).to eq '1000000218147500' 46 | expect(in_app.product_id).to eq 'com.example.product_id' 47 | 48 | expect(in_app.purchase_date).to eq DateTime.parse('2016-06-17 01:32:28 Etc/GMT') 49 | expect(in_app.purchase_date_ms).to eq Time.zone.at("1466127148000".to_i / 1000) 50 | 51 | expect(in_app.original_purchase_date).to eq DateTime.parse('2016-06-17 01:30:33 Etc/GMT') 52 | expect(in_app.original_purchase_date_ms).to eq Time.zone.at("1466127033000".to_i / 1000) 53 | 54 | expect(in_app.expires_date).to eq DateTime.parse('2016-06-17 01:37:28 Etc/GMT') 55 | expect(in_app.expires_date_ms).to eq Time.zone.at("1466127448000".to_i / 1000) 56 | 57 | expect(in_app.is_trial_period).to eq false 58 | expect(in_app.cancellation_date).to be_nil 59 | end 60 | 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/cancellation_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 0, 3 | "environment": "Sandbox", 4 | "receipt": { 5 | "receipt_type": "ProductionSandbox", 6 | "adam_id": 0, 7 | "app_item_id": 0, 8 | "bundle_id": "com.example.app", 9 | "application_version": "58", 10 | "download_id": 100, 11 | "version_external_identifier": 0, 12 | "receipt_creation_date": "2016-06-17 01:54:26 Etc/GMT", 13 | "receipt_creation_date_ms": "1466128466000", 14 | "receipt_creation_date_pst": "2016-06-16 18:54:26 America/Los_Angeles", 15 | "request_date": "2016-06-17 17:34:41 Etc/GMT", 16 | "request_date_ms": "1466184881174", 17 | "request_date_pst": "2016-06-17 10:34:41 America/Los_Angeles", 18 | "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 19 | "original_purchase_date_ms": "1375340400000", 20 | "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 21 | "original_application_version": "1.0", 22 | "in_app": [ 23 | { 24 | "quantity": "1", 25 | "product_id": "com.example.product_id", 26 | "transaction_id": "1000000218147651", 27 | "original_transaction_id": "1000000218147500", 28 | "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 29 | "purchase_date_ms": "1466127148000", 30 | "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 31 | "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 32 | "original_purchase_date_ms": "1466127033000", 33 | "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 34 | "expires_date": "2016-06-17 01:37:28 Etc/GMT", 35 | "expires_date_ms": "1466127448000", 36 | "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 37 | "web_order_line_item_id": "1000000032727764", 38 | "is_trial_period": "false", 39 | "cancellation_date": "2016-06-17 01:37:28 Etc/GMT" 40 | } 41 | ] 42 | }, 43 | "latest_receipt_info": [ 44 | { 45 | "quantity": "1", 46 | "product_id": "com.example.product_id", 47 | "transaction_id": "1000000218147500", 48 | "original_transaction_id": "1000000218147500", 49 | "purchase_date": "2016-06-17 01:27:28 Etc/GMT", 50 | "purchase_date_ms": "1466126848000", 51 | "purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 52 | "original_purchase_date": "2016-06-17 01:27:28 Etc/GMT", 53 | "original_purchase_date_ms": "1466126848000", 54 | "original_purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 55 | "expires_date": "2016-06-17 01:32:28 Etc/GMT", 56 | "expires_date_ms": "1466127148000", 57 | "expires_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 58 | "web_order_line_item_id": "1000000032727765", 59 | "is_trial_period": "true", 60 | "cancellation_date": "2016-06-17 01:37:28 Etc/GMT" 61 | } 62 | ], 63 | "latest_receipt": "base 64 string" 64 | } 65 | -------------------------------------------------------------------------------- /lib/monza/transaction_receipt.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'active_support/core_ext/time' 3 | 4 | module Monza 5 | class TransactionReceipt 6 | # Receipt Fields Documentation 7 | # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1 8 | 9 | attr_reader :quantity 10 | attr_reader :product_id 11 | attr_reader :transaction_id 12 | attr_reader :original_transaction_id 13 | attr_reader :purchase_date 14 | attr_reader :purchase_date_ms 15 | attr_reader :purchase_date_pst 16 | attr_reader :original_purchase_date 17 | attr_reader :original_purchase_date_ms 18 | attr_reader :original_purchase_date_pst 19 | attr_reader :web_order_line_item_id 20 | 21 | attr_reader :expires_date 22 | attr_reader :expires_date_ms 23 | attr_reader :expires_date_pst 24 | attr_reader :is_trial_period 25 | attr_reader :cancellation_date 26 | 27 | def initialize(attributes) 28 | @quantity = attributes['quantity'].to_i 29 | @product_id = attributes['product_id'] 30 | @transaction_id = attributes['transaction_id'] 31 | @original_transaction_id = attributes['original_transaction_id'] 32 | @purchase_date = DateTime.parse(attributes['purchase_date']) if attributes['purchase_date'] 33 | @purchase_date_ms = Time.zone.at(attributes['purchase_date_ms'].to_i / 1000) 34 | @purchase_date_pst = DateTime.parse(attributes['purchase_date_pst'].gsub("America/Los_Angeles","PST")) if attributes['purchase_date_pst'] 35 | @original_purchase_date = DateTime.parse(attributes['original_purchase_date']) if attributes['original_purchase_date'] 36 | @original_purchase_date_ms = Time.zone.at(attributes['original_purchase_date_ms'].to_i / 1000) 37 | @original_purchase_date_pst = DateTime.parse(attributes['original_purchase_date_pst'].gsub("America/Los_Angeles","PST")) if attributes['original_purchase_date_pst'] 38 | @web_order_line_item_id = attributes['web_order_line_item_id'] 39 | 40 | if attributes['expires_date'] 41 | @expires_date = DateTime.parse(attributes['expires_date']) 42 | end 43 | if attributes['expires_date_ms'] 44 | @expires_date_ms = Time.zone.at(attributes['expires_date_ms'].to_i / 1000) 45 | end 46 | if attributes['expires_date_pst'] 47 | @expires_date_pst = DateTime.parse(attributes['expires_date_pst'].gsub("America/Los_Angeles","PST")) 48 | end 49 | if attributes['is_trial_period'] 50 | @is_trial_period = attributes['is_trial_period'].to_bool 51 | end 52 | if attributes['cancellation_date'] 53 | @cancellation_date = DateTime.parse(attributes['cancellation_date']) 54 | end 55 | end # end initialize 56 | 57 | # 58 | # Depcrecating - don't use these 59 | # These will only work if the user never cancels and then resubscribes 60 | # The original_transaction_id does not reset after the user resubscribes 61 | # 62 | # def is_renewal? 63 | # !is_first_transaction? 64 | # end 65 | # 66 | # def is_first_transaction? 67 | # @original_transaction_id == @transaction_id 68 | # end 69 | end # end class 70 | end # end module 71 | 72 | # 73 | # Sample JSON Object 74 | # 75 | # { 76 | # "quantity": "1", 77 | # "product_id": "product_id", 78 | # "transaction_id": "1000000218147651", 79 | # "original_transaction_id": "1000000218147500", 80 | # "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 81 | # "purchase_date_ms": "1466127148000", 82 | # "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 83 | # "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 84 | # "original_purchase_date_ms": "1466127033000", 85 | # "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 86 | # "expires_date": "2016-06-17 01:37:28 Etc/GMT", 87 | # "expires_date_ms": "1466127448000", 88 | # "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 89 | # "web_order_line_item_id": "1000000032727764", 90 | # "is_trial_period": "false" 91 | # } 92 | -------------------------------------------------------------------------------- /spec/verification_response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Monza::VerificationResponse do 4 | context 'verification example' do 5 | let(:response) { JSON.parse File.open("#{Dir.pwd}/spec/response.json", 'rb').read } 6 | let(:verify) { described_class.new(response) } 7 | let(:cancellation_response) { JSON.parse File.open("#{Dir.pwd}/spec/cancellation_response.json", 'rb').read } 8 | let(:cancellation_receipt) { described_class.new(cancellation_response['receipt']) } 9 | 10 | it { expect(verify.status).to eq 0 } 11 | it { expect(verify.environment).to eq 'Sandbox' } 12 | it { expect(verify.receipt.class).to eq Monza::Receipt } 13 | it { expect(verify.latest_receipt_info).not_to be_nil } 14 | it { expect(verify.latest_receipt).to eq 'base 64 string' } 15 | end 16 | 17 | context 'vefification response error' do 18 | let(:response) { JSON.parse File.open("#{Dir.pwd}/spec/bad_response.json", 'rb').read } 19 | let(:verify) { described_class.new(response) } 20 | 21 | it { expect(verify.status).to eq 21_003 } 22 | it do 23 | error = described_class::VerificationError.new(response['status']) 24 | 25 | expect(error.message).to eq 'The receipt could not be authenticated.' 26 | end 27 | end 28 | 29 | context 'latest receipt info' do 30 | let(:response) { JSON.parse File.open("#{Dir.pwd}/spec/response.json", 'rb').read } 31 | let(:verify) { described_class.new(response) } 32 | 33 | it 'latest_receipt_info' do 34 | latest_transaction = verify.latest_receipt_info.last 35 | 36 | expect(latest_transaction).not_to be_nil 37 | expect(latest_transaction.quantity).to eq 1 38 | expect(latest_transaction.transaction_id).to eq '1000000218147500' 39 | expect(latest_transaction.original_transaction_id).to eq '1000000218147500' 40 | expect(latest_transaction.product_id).to eq 'com.example.product_id' 41 | 42 | expect(latest_transaction.purchase_date).to eq DateTime.parse('2016-06-17 01:27:28 Etc/GMT') 43 | expect(latest_transaction.purchase_date_ms).to eq Time.zone.at("1466126848000".to_i / 1000) 44 | 45 | expect(latest_transaction.original_purchase_date).to eq DateTime.parse('2016-06-17 01:27:28 Etc/GMT') 46 | expect(latest_transaction.original_purchase_date_ms).to eq Time.zone.at("1466126848000".to_i / 1000) 47 | 48 | expect(latest_transaction.expires_date).to eq DateTime.parse('2016-06-17 01:32:28 Etc/GMT') 49 | expect(latest_transaction.expires_date_ms).to eq Time.zone.at("1466127148000".to_i / 1000) 50 | 51 | expect(latest_transaction.is_trial_period).to eq true 52 | expect(latest_transaction.cancellation_date).to be_nil 53 | end 54 | end 55 | 56 | context 'latest receipt info with cancellation' do 57 | let(:cancellation_response) { JSON.parse File.open("#{Dir.pwd}/spec/cancellation_response.json", 'rb').read } 58 | let(:verify) { described_class.new(cancellation_response) } 59 | 60 | it 'latest_receipt_info' do 61 | latest_transaction = verify.latest_receipt_info.last 62 | 63 | expect(latest_transaction).not_to be_nil 64 | expect(latest_transaction.quantity).to eq 1 65 | expect(latest_transaction.transaction_id).to eq '1000000218147500' 66 | expect(latest_transaction.original_transaction_id).to eq '1000000218147500' 67 | expect(latest_transaction.product_id).to eq 'com.example.product_id' 68 | 69 | expect(latest_transaction.purchase_date).to eq DateTime.parse('2016-06-17 01:27:28 Etc/GMT') 70 | expect(latest_transaction.purchase_date_ms).to eq Time.zone.at("1466126848000".to_i / 1000) 71 | 72 | expect(latest_transaction.original_purchase_date).to eq DateTime.parse('2016-06-17 01:27:28 Etc/GMT') 73 | expect(latest_transaction.original_purchase_date_ms).to eq Time.zone.at("1466126848000".to_i / 1000) 74 | 75 | expect(latest_transaction.expires_date).to eq DateTime.parse('2016-06-17 01:32:28 Etc/GMT') 76 | expect(latest_transaction.expires_date_ms).to eq Time.zone.at("1466127148000".to_i / 1000) 77 | 78 | expect(latest_transaction.is_trial_period).to eq true 79 | expect(latest_transaction.cancellation_date).to eq DateTime.parse('2016-06-17 01:37:28 Etc/GMT') 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/monza/verification_response.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module Monza 4 | class VerificationResponse 5 | # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html 6 | 7 | attr_reader :status 8 | attr_reader :environment 9 | attr_reader :receipt 10 | attr_reader :latest_receipt_info 11 | attr_reader :latest_receipt 12 | attr_reader :original_json_response 13 | 14 | 15 | def initialize(attributes) 16 | @original_json_response = attributes 17 | @status = attributes['status'] 18 | @environment = attributes['environment'] 19 | @receipt = Receipt.new(attributes['receipt']) 20 | @latest_receipt_info = [] 21 | if attributes['latest_receipt_info'] 22 | attributes['latest_receipt_info'].each do |transaction_receipt_attributes| 23 | @latest_receipt_info << TransactionReceipt.new(transaction_receipt_attributes) 24 | end 25 | end 26 | @latest_receipt = attributes['latest_receipt'] 27 | end 28 | 29 | def is_subscription_active? 30 | if @latest_receipt_info.last 31 | @latest_receipt_info.last.expires_date_ms >= Time.zone.now 32 | else 33 | false 34 | end 35 | end 36 | 37 | def latest_expiry_date 38 | @latest_receipt_info.last.expires_date_ms if @latest_receipt_info.last 39 | end 40 | 41 | class VerificationError < StandardError 42 | attr_accessor :code 43 | 44 | def initialize(code) 45 | @code = Integer(code) 46 | end 47 | 48 | def message 49 | case @code 50 | when 21000 51 | "The App Store could not read the JSON object you provided." 52 | when 21002 53 | "The data in the receipt-data property was malformed." 54 | when 21003 55 | "The receipt could not be authenticated." 56 | when 21004 57 | "The shared secret you provided does not match the shared secret on file for your account." 58 | when 21005 59 | "The receipt server is not currently available." 60 | when 21006 61 | "This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response." 62 | when 21007 63 | "This receipt is a sandbox receipt, but it was sent to the production service for verification." 64 | when 21008 65 | "This receipt is a production receipt, but it was sent to the sandbox service for verification." 66 | else 67 | "Unknown Error: #{@code}" 68 | end 69 | end 70 | end # end VerificationError 71 | end # class 72 | end # module 73 | 74 | # Sample JSON Response 75 | # 76 | # { 77 | # "status": 0, 78 | # "environment": "Sandbox", 79 | # "receipt": { 80 | # "receipt_type": "ProductionSandbox", 81 | # "adam_id": 0, 82 | # "app_item_id": 0, 83 | # "bundle_id": "your_product_id", 84 | # "application_version": "58", 85 | # "download_id": 0, 86 | # "version_external_identifier": 0, 87 | # "receipt_creation_date": "2016-06-17 01:54:26 Etc/GMT", 88 | # "receipt_creation_date_ms": "1466128466000", 89 | # "receipt_creation_date_pst": "2016-06-16 18:54:26 America/Los_Angeles", 90 | # "request_date": "2016-06-17 17:34:41 Etc/GMT", 91 | # "request_date_ms": "1466184881174", 92 | # "request_date_pst": "2016-06-17 10:34:41 America/Los_Angeles", 93 | # "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 94 | # "original_purchase_date_ms": "1375340400000", 95 | # "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 96 | # "original_application_version": "1.0", 97 | # "in_app": [ 98 | # { 99 | # "quantity": "1", 100 | # "product_id": "product_id", 101 | # "transaction_id": "1000000218147651", 102 | # "original_transaction_id": "1000000218147500", 103 | # "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 104 | # "purchase_date_ms": "1466127148000", 105 | # "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 106 | # "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 107 | # "original_purchase_date_ms": "1466127033000", 108 | # "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 109 | # "expires_date": "2016-06-17 01:37:28 Etc/GMT", 110 | # "expires_date_ms": "1466127448000", 111 | # "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 112 | # "web_order_line_item_id": "1000000032727764", 113 | # "is_trial_period": "false" 114 | # } 115 | # ] 116 | # }, 117 | # "latest_receipt_info": [ 118 | # { 119 | # "quantity": "1", 120 | # "product_id": "product_id", 121 | # "transaction_id": "1000000218147500", 122 | # "original_transaction_id": "1000000218147500", 123 | # "purchase_date": "2016-06-17 01:27:28 Etc/GMT", 124 | # "purchase_date_ms": "1466126848000", 125 | # "purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 126 | # "original_purchase_date": "2016-06-17 01:27:28 Etc/GMT", 127 | # "original_purchase_date_ms": "1466126848000", 128 | # "original_purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 129 | # "expires_date": "2016-06-17 01:32:28 Etc/GMT", 130 | # "expires_date_ms": "1466127148000", 131 | # "expires_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 132 | # "web_order_line_item_id": "1000000032727765", 133 | # "is_trial_period": "true" 134 | # } 135 | # ], 136 | # "latest_receipt": "base 64" 137 | # } 138 | -------------------------------------------------------------------------------- /lib/monza/receipt.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'active_support/core_ext/time' 3 | 4 | module Monza 5 | class Receipt 6 | # Receipt Fields Documentation 7 | # https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1 8 | 9 | attr_reader :receipt_type 10 | attr_reader :adam_id 11 | attr_reader :bundle_id 12 | attr_reader :application_version 13 | attr_reader :download_id 14 | attr_reader :receipt_creation_date 15 | attr_reader :receipt_creation_date_ms 16 | attr_reader :receipt_creation_date_pst 17 | attr_reader :request_date 18 | attr_reader :request_date_ms 19 | attr_reader :request_date_pst 20 | attr_reader :original_purchase_date 21 | attr_reader :original_purchase_date_ms 22 | attr_reader :original_purchase_date_pst 23 | attr_reader :original_application_version 24 | attr_reader :in_app 25 | 26 | # This key is present only for apps purchased through the Volume Purchase Program. If this key is not present, the receipt does not expire. 27 | attr_reader :expiration_date 28 | 29 | # This field is not present for Mac apps 30 | attr_reader :app_item_id 31 | 32 | # This key is not present for receipts created in the test environment. 33 | attr_reader :version_external_identifier 34 | 35 | def initialize(attributes) 36 | @receipt_type = attributes['receipt_type'] 37 | @adam_id = attributes['adam_id'] 38 | @bundle_id = attributes['bundle_id'] 39 | @application_version = attributes['application_version'] 40 | @download_id = attributes['download_id'] 41 | @receipt_creation_date = DateTime.parse(attributes['receipt_creation_date']) 42 | @receipt_creation_date_ms = Time.zone.at(attributes['receipt_creation_date_ms'].to_i / 1000) 43 | @receipt_creation_date_pst = DateTime.parse(attributes['receipt_creation_date_pst'].gsub("America/Los_Angeles","PST")) 44 | @request_date = DateTime.parse(attributes['request_date']) 45 | @request_date_ms = Time.zone.at(attributes['request_date_ms'].to_i / 1000) 46 | @request_date_pst = DateTime.parse(attributes['request_date_pst'].gsub("America/Los_Angeles","PST")) 47 | @original_purchase_date = DateTime.parse(attributes['original_purchase_date']) 48 | @original_purchase_date_ms = Time.zone.at(attributes['original_purchase_date_ms'].to_i / 1000) 49 | @original_purchase_date_pst = DateTime.parse(attributes['original_purchase_date_pst'].gsub("America/Los_Angeles","PST")) 50 | @original_application_version = attributes['original_application_version'] 51 | 52 | if attributes['version_external_identifier'] 53 | @version_external_identifier = attributes['version_external_identifier'] 54 | end 55 | if attributes['app_item_id'] 56 | @app_item_id = attributes['app_item_id'] 57 | end 58 | if attributes['expiration_date'] 59 | @expires_at = Time.zone.at(attributes['expiration_date'].to_i / 1000) 60 | end 61 | 62 | @in_app = [] 63 | if attributes['in_app'] 64 | attributes['in_app'].each do |transaction_receipt_attributes| 65 | @in_app << TransactionReceipt.new(transaction_receipt_attributes) 66 | end 67 | end 68 | end # end initialize 69 | 70 | def self.verify(data, options = {}) 71 | client = Client.production 72 | 73 | begin 74 | client.verify(data, options) 75 | rescue VerificationResponse::VerificationError => error 76 | case error.code 77 | when 21007 # This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead. 78 | client = Client.development 79 | retry 80 | when 21008 # This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead. 81 | client = Client.production 82 | retry 83 | else 84 | raise error 85 | end 86 | end 87 | end 88 | 89 | end # end class 90 | end # end module 91 | 92 | # 93 | # Sample JSON Object 94 | # 95 | # "receipt": { 96 | # "receipt_type": "ProductionSandbox", 97 | # "adam_id": 0, 98 | # "app_item_id": 0, 99 | # "bundle_id": "your_product_id", 100 | # "application_version": "58", 101 | # "download_id": 0, 102 | # "version_external_identifier": 0, 103 | # "receipt_creation_date": "2016-06-17 01:54:26 Etc/GMT", 104 | # "receipt_creation_date_ms": "1466128466000", 105 | # "receipt_creation_date_pst": "2016-06-16 18:54:26 America/Los_Angeles", 106 | # "request_date": "2016-06-17 17:34:41 Etc/GMT", 107 | # "request_date_ms": "1466184881174", 108 | # "request_date_pst": "2016-06-17 10:34:41 America/Los_Angeles", 109 | # "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 110 | # "original_purchase_date_ms": "1375340400000", 111 | # "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 112 | # "original_application_version": "1.0", 113 | # "in_app": [ 114 | # { 115 | # "quantity": "1", 116 | # "product_id": "com.everlance.everlance.pro.monthly.test", 117 | # "transaction_id": "1000000218147651", 118 | # "original_transaction_id": "1000000218147500", 119 | # "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 120 | # "purchase_date_ms": "1466127148000", 121 | # "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 122 | # "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 123 | # "original_purchase_date_ms": "1466127033000", 124 | # "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 125 | # "expires_date": "2016-06-17 01:37:28 Etc/GMT", 126 | # "expires_date_ms": "1466127448000", 127 | # "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 128 | # "web_order_line_item_id": "1000000032727764", 129 | # "is_trial_period": "false" 130 | # } 131 | # ] 132 | # } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/gabrielgarza/monza.svg?branch=master)](https://travis-ci.org/gabrielgarza/monza) 2 | 3 | ![monza_asset](https://cloud.githubusercontent.com/assets/1076706/16257552/1baa36f8-380e-11e6-8730-cbbd1fe73c6c.png) 4 | 5 | #### Monza is a ruby gem that makes In-App Purchase receipt and Auto-Renewable subscription validation easy. 6 | 7 | You should always validate receipts on the server, in [Apple's words](https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1): 8 | > Use a trusted server to communicate with the App Store. Using your own server lets you design your app to recognize and trust only your server, and lets you ensure that your server connects with the App Store server. It is not possible to build a trusted connection between a user’s device and the App Store directly because you don’t control either end of that connection. 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'monza' 16 | ``` 17 | 18 | And then execute: 19 | 20 | $ bundle install 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install monza 25 | 26 | ## Usage 27 | 28 | ##### Basic Usage: 29 | ```ruby 30 | 31 | data = "base64 receipt data string" 32 | options = { shared_secret: "your shared secret" } 33 | response = Monza::Receipt.verify(data, options) 34 | 35 | ``` 36 | 37 | ##### Useful Methods 38 | ```ruby 39 | # Check if subscription is active 40 | # this checks if latest transaction receipt expiry_date is in today or the future 41 | response.is_subscription_active? # => true or false 42 | 43 | # Check most recent expiry date 44 | # ActiveSupport::TimeWithZone 45 | response.latest_expiry_date # => Fri, 17 Jun 2016 01:57:28 UTC +00:00 46 | 47 | ``` 48 | 49 | ##### Response Objects 50 | ```ruby 51 | # Receipt object 52 | # See Receipt class or sample JSON below for full attributes 53 | response.receipt 54 | 55 | # Receipt In App Transactions 56 | # Returns array of TransactionReceipt objects, see TransactionReceipt class or sample JSON below for full attributes 57 | response.receipt.in_app 58 | 59 | # Receipt Latest Transactions List, use these instead if in_app to ensure you always have the latest 60 | # Returns array of TransactionReceipt objects, see TransactionReceipt class 61 | response.latest_receipt_info # => Array of TransactionReceipt objects 62 | 63 | # Expires date of a transaction 64 | # DateTime 65 | response.latest_receipt_info.last.expires_date => # Fri, 17 Jun 2016 01:57:28 +0000 66 | 67 | # Check if latest transaction was trial period 68 | response.latest_receipt_info.last.is_trial_period # => true or false 69 | 70 | # Latest receipt base64 string 71 | response.latest_receipt 72 | 73 | # original JSON response 74 | response.original_json_response 75 | ``` 76 | 77 | ##### Sample JSON Response Schema 78 | ```json 79 | 80 | { 81 | "status": 0, 82 | "environment": "Sandbox", 83 | "receipt": { 84 | "receipt_type": "ProductionSandbox", 85 | "adam_id": 0, 86 | "app_item_id": 0, 87 | "bundle_id": "your_product_id", 88 | "application_version": "58", 89 | "download_id": 0, 90 | "version_external_identifier": 0, 91 | "receipt_creation_date": "2016-06-17 01:54:26 Etc/GMT", 92 | "receipt_creation_date_ms": "1466128466000", 93 | "receipt_creation_date_pst": "2016-06-16 18:54:26 America/Los_Angeles", 94 | "request_date": "2016-06-17 17:34:41 Etc/GMT", 95 | "request_date_ms": "1466184881174", 96 | "request_date_pst": "2016-06-17 10:34:41 America/Los_Angeles", 97 | "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT", 98 | "original_purchase_date_ms": "1375340400000", 99 | "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles", 100 | "original_application_version": "1.0", 101 | "in_app": [ 102 | { 103 | "quantity": "1", 104 | "product_id": "product_id", 105 | "transaction_id": "1000000218147651", 106 | "original_transaction_id": "1000000218147500", 107 | "purchase_date": "2016-06-17 01:32:28 Etc/GMT", 108 | "purchase_date_ms": "1466127148000", 109 | "purchase_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 110 | "original_purchase_date": "2016-06-17 01:30:33 Etc/GMT", 111 | "original_purchase_date_ms": "1466127033000", 112 | "original_purchase_date_pst": "2016-06-16 18:30:33 America/Los_Angeles", 113 | "expires_date": "2016-06-17 01:37:28 Etc/GMT", 114 | "expires_date_ms": "1466127448000", 115 | "expires_date_pst": "2016-06-16 18:37:28 America/Los_Angeles", 116 | "web_order_line_item_id": "1000000032727764", 117 | "is_trial_period": "false" 118 | } 119 | ] 120 | }, 121 | "latest_receipt_info": [ 122 | { 123 | "quantity": "1", 124 | "product_id": "product_id", 125 | "transaction_id": "1000000218147500", 126 | "original_transaction_id": "1000000218147500", 127 | "purchase_date": "2016-06-17 01:27:28 Etc/GMT", 128 | "purchase_date_ms": "1466126848000", 129 | "purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 130 | "original_purchase_date": "2016-06-17 01:27:28 Etc/GMT", 131 | "original_purchase_date_ms": "1466126848000", 132 | "original_purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 133 | "expires_date": "2016-06-17 01:32:28 Etc/GMT", 134 | "expires_date_ms": "1466127148000", 135 | "expires_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 136 | "web_order_line_item_id": "1000000032727765", 137 | "is_trial_period": "true" 138 | } 139 | ], 140 | "latest_receipt": "base 64 string" 141 | } 142 | 143 | ``` 144 | 145 | ##### TransactionReceipt Object 146 | An array TransactionReceipt objects will come inside the `receipt.in_app` and `latest_receipt_info` keys of the `response` 147 | ```json 148 | { 149 | "quantity": "1", 150 | "product_id": "product_id", 151 | "transaction_id": "1000000218147500", 152 | "original_transaction_id": "1000000218147500", 153 | "purchase_date": "2016-06-17 01:27:28 Etc/GMT", 154 | "purchase_date_ms": "1466126848000", 155 | "purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 156 | "original_purchase_date": "2016-06-17 01:27:28 Etc/GMT", 157 | "original_purchase_date_ms": "1466126848000", 158 | "original_purchase_date_pst": "2016-06-16 18:27:28 America/Los_Angeles", 159 | "expires_date": "2016-06-17 01:32:28 Etc/GMT", 160 | "expires_date_ms": "1466127148000", 161 | "expires_date_pst": "2016-06-16 18:32:28 America/Los_Angeles", 162 | "web_order_line_item_id": "1000000032727765", 163 | "is_trial_period": "true" 164 | } 165 | 166 | ``` 167 | 168 | 169 | 170 | 171 | 172 | ## Development 173 | 174 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 175 | 176 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 177 | 178 | ## Contributing 179 | 180 | Bug reports and pull requests are welcome on GitHub at https://github.com/gabrielgarza/monza. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 181 | 182 | TODO: 183 | - Need help with testing using rspec 184 | 185 | 186 | ## License 187 | 188 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 189 | --------------------------------------------------------------------------------