All Files
(85.49%
covered at
3.39
hits/line)
6 files in total.
193 relevant lines.
165 lines covered and
28 lines missed
-
1
require 'venice/version'
-
1
require 'venice/client'
-
1
require 'venice/in_app_receipt'
-
1
require 'venice/receipt'
-
1
require 'venice/pending_renewal_info'
-
1
require 'json'
-
1
require 'net/https'
-
1
require 'uri'
-
-
1
module Venice
-
1
ITUNES_PRODUCTION_RECEIPT_VERIFICATION_ENDPOINT = 'https://buy.itunes.apple.com/verifyReceipt'
-
1
ITUNES_DEVELOPMENT_RECEIPT_VERIFICATION_ENDPOINT = 'https://sandbox.itunes.apple.com/verifyReceipt'
-
-
1
class Client
-
1
attr_accessor :verification_url
-
1
attr_writer :shared_secret
-
-
1
class << self
-
1
def development
-
client = new
-
client.verification_url = ITUNES_DEVELOPMENT_RECEIPT_VERIFICATION_ENDPOINT
-
client
-
end
-
-
1
def production
-
1
client = new
-
1
client.verification_url = ITUNES_PRODUCTION_RECEIPT_VERIFICATION_ENDPOINT
-
1
client
-
end
-
end
-
-
1
def initialize
-
6
@verification_url = ENV['IAP_VERIFICATION_ENDPOINT']
-
end
-
-
1
def verify!(data, options = {})
-
6
@verification_url ||= ITUNES_DEVELOPMENT_RECEIPT_VERIFICATION_ENDPOINT
-
6
@shared_secret = options[:shared_secret] if options[:shared_secret]
-
-
6
json = json_response_from_verifying_data(data)
-
6
status = json['status'].to_i
-
6
receipt_attributes = json['receipt'].dup
-
6
receipt_attributes['original_json_response'] = json if receipt_attributes
-
-
6
case status
-
when 0, 21006
-
6
receipt = Receipt.new(receipt_attributes)
-
-
# From Apple docs:
-
# > Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
-
# > The JSON representation of the receipt for the most recent renewal
-
6
if latest_receipt_info_attributes = json['latest_receipt_info']
-
# AppStore returns 'latest_receipt_info' even if we use over iOS 6. Besides, its format is an Array.
-
1
receipt.latest_receipt_info = []
-
1
latest_receipt_info_attributes.each do |latest_receipt_info_attribute|
-
# latest_receipt_info format is identical with in_app
-
1
receipt.latest_receipt_info << InAppReceipt.new(latest_receipt_info_attribute)
-
end
-
end
-
-
6
return receipt
-
else
-
raise Receipt::VerificationError.new(status, receipt)
-
end
-
end
-
-
1
private
-
-
1
def json_response_from_verifying_data(data)
-
3
parameters = {
-
'receipt-data' => data
-
}
-
-
3
parameters['password'] = @shared_secret if @shared_secret
-
-
3
uri = URI(@verification_url)
-
3
http = Net::HTTP.new(uri.host, uri.port)
-
3
http.use_ssl = true
-
3
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
-
-
3
request = Net::HTTP::Post.new(uri.request_uri)
-
3
request['Accept'] = 'application/json'
-
3
request['Content-Type'] = 'application/json'
-
3
request.body = parameters.to_json
-
-
3
response = http.request(request)
-
-
3
JSON.parse(response.body)
-
end
-
end
-
end
-
1
require 'time'
-
-
1
module Venice
-
1
class InAppReceipt
-
# For detailed explanations on these keys/values, see
-
# https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW12
-
-
# The number of items purchased. This value corresponds to the quantity property of
-
# the SKPayment object stored in the transaction’s payment property.
-
1
attr_reader :quantity
-
-
# The product identifier of the item that was purchased. This value corresponds to
-
# the productIdentifier property of the SKPayment object stored in the transaction’s
-
# payment property.
-
1
attr_reader :product_id
-
-
# The transaction identifier of the item that was purchased. This value corresponds
-
# to the transaction’s transactionIdentifier property.
-
1
attr_reader :transaction_id
-
-
# The date and time this transaction occurred. This value corresponds to the
-
# transaction’s transactionDate property.
-
1
attr_reader :purchased_at
-
-
# A string that the App Store uses to uniquely identify the application that created
-
# the payment transaction. If your server supports multiple applications, you can use
-
# this value to differentiate between them. Applications that are executing in the
-
# sandbox do not yet have an app-item-id assigned to them, so this key is missing from
-
# receipts created by the sandbox.
-
1
attr_reader :app_item_id
-
-
# An arbitrary number that uniquely identifies a revision of your application. This key
-
# is missing in receipts created by the sandbox.
-
1
attr_reader :version_external_identifier
-
-
# For a transaction that restores a previous transaction, this is the original receipt
-
1
attr_accessor :original
-
-
# For auto-renewable subscriptions, returns the date the subscription will expire
-
1
attr_reader :expires_at
-
-
# For a transaction that was canceled by Apple customer support, the time and date of the cancellation.
-
1
attr_reader :cancellation_at
-
-
1
def initialize(attributes = {})
-
24
@quantity = Integer(attributes['quantity']) if attributes['quantity']
-
24
@product_id = attributes['product_id']
-
24
@transaction_id = attributes['transaction_id']
-
24
@purchased_at = DateTime.parse(attributes['purchase_date']) if attributes['purchase_date']
-
24
@app_item_id = attributes['app_item_id']
-
24
@version_external_identifier = attributes['version_external_identifier']
-
-
# expires_date is in ms since the Epoch, Time.at expects seconds
-
24
@expires_at = Time.at(attributes['expires_date_ms'].to_i / 1000) if attributes['expires_date_ms']
-
-
# cancellation_date is in ms since the Epoch, Time.at expects seconds
-
24
@cancellation_at = Time.at(attributes['cancellation_date_ms'].to_i / 1000) if attributes['cancellation_date_ms']
-
-
24
if attributes['original_transaction_id'] || attributes['original_purchase_date']
-
12
original_attributes = {
-
'transaction_id' => attributes['original_transaction_id'],
-
'purchase_date' => attributes['original_purchase_date']
-
}
-
-
12
self.original = InAppReceipt.new(original_attributes)
-
end
-
end
-
-
1
def to_hash
-
{
-
quantity: @quantity,
-
product_id: @product_id,
-
transaction_id: @transaction_id,
-
2
purchase_date: (@purchased_at.httpdate rescue nil),
-
2
original_transaction_id: (@original.transaction_id rescue nil),
-
2
original_purchase_date: (@original.purchased_at.httpdate rescue nil),
-
app_item_id: @app_item_id,
-
version_external_identifier: @version_external_identifier,
-
2
expires_at: (@expires_at.httpdate rescue nil),
-
2
cancellation_at: (@cancellation_at.httpdate rescue nil)
-
2
}
-
end
-
1
alias_method :to_h, :to_hash
-
-
1
def to_json
-
to_hash.to_json
-
end
-
end
-
end
-
1
module Venice
-
1
class PendingRenewalInfo
-
# For an expired subscription, the reason for the subscription expiration.
-
# This key is only present for a receipt containing an expired auto-renewable subscription.
-
1
attr_reader :expiration_intent
-
-
# The current renewal status for the auto-renewable subscription.
-
# This key is only present for auto-renewable subscription receipts, for active or expired subscriptions
-
1
attr_reader :auto_renew_status
-
-
# The current renewal preference for the auto-renewable subscription.
-
# The value for this key corresponds to the productIdentifier property of the product that the customer’s subscription renews.
-
1
attr_reader :auto_renew_product_id
-
-
# For an expired subscription, whether or not Apple is still attempting to automatically renew the subscription.
-
# If the customer’s subscription failed to renew because the App Store was unable to complete the transaction, this value will reflect whether or not the App Store is still trying to renew the subscription.
-
1
attr_reader :is_in_billing_retry_period
-
-
# The product identifier of the item that was purchased.
-
# This value corresponds to the productIdentifier property of the SKPayment object stored in the transaction’s payment property.
-
1
attr_reader :product_id
-
-
# The current price consent status for a subscription price increase
-
# This key is only present for auto-renewable subscription receipts if the subscription price was increased without keeping the existing price for active subscribers
-
1
attr_reader :price_consent_status
-
-
# For a transaction that was cancelled, the reason for cancellation.
-
# Use this value along with the cancellation date to identify possible issues in your app that may lead customers to contact Apple customer support.
-
1
attr_reader :cancellation_reason
-
-
1
def initialize(attributes)
-
7
@expiration_intent = Integer(attributes['expiration_intent']) if attributes['expiration_intent']
-
7
@auto_renew_status = Integer(attributes['auto_renew_status']) if attributes['auto_renew_status']
-
7
@auto_renew_product_id = attributes['auto_renew_product_id']
-
-
7
if attributes['is_in_billing_retry_period']
-
7
@is_in_billing_retry_period = (attributes[is_in_billing_retry_period] == '1')
-
end
-
-
7
@product_id = attributes['product_id']
-
-
7
@price_consent_status = Integer(attributes['price_consent_status']) if attributes['price_consent_status']
-
7
@cancellation_reason = Integer(attributes['cancellation_reason']) if attributes['cancellation_reason']
-
end
-
-
1
def to_hash
-
{
-
expiration_intent: @expiration_intent,
-
auto_renew_status: @auto_renew_status,
-
auto_renew_product_id: @auto_renew_product_id,
-
is_in_billing_retry_period: @is_in_billing_retry_period,
-
product_id: @product_id,
-
price_consent_status: @price_consent_status,
-
cancellation_reason: @cancellation_reason
-
2
}
-
end
-
-
1
alias_method :to_h, :to_hash
-
-
1
def to_json
-
to_hash.to_json
-
end
-
end
-
end
-
1
require 'time'
-
-
1
module Venice
-
1
class Receipt
-
# For detailed explanations on these keys/values, see
-
# https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
-
-
# The app’s bundle identifier.
-
1
attr_reader :bundle_id
-
-
# The app’s version number.
-
1
attr_reader :application_version
-
-
# The receipt for an in-app purchase.
-
1
attr_reader :in_app
-
-
# The version of the app that was originally purchased.
-
1
attr_reader :original_application_version
-
-
# The original purchase date
-
1
attr_reader :original_purchase_date
-
-
# The date that the app receipt expires.
-
1
attr_reader :expires_at
-
-
# Non-Documented receipt keys/values
-
1
attr_reader :receipt_type
-
1
attr_reader :adam_id
-
1
attr_reader :download_id
-
1
attr_reader :requested_at
-
-
# Original json response from AppStore
-
1
attr_reader :original_json_response
-
-
1
attr_accessor :latest_receipt_info
-
-
# Information about the status of the customer's auto-renewable subscriptions
-
1
attr_reader :pending_renewal_info
-
-
1
def initialize(attributes = {})
-
8
@original_json_response = attributes['original_json_response']
-
-
8
@bundle_id = attributes['bundle_id']
-
8
@application_version = attributes['application_version']
-
8
@original_application_version = attributes['original_application_version']
-
8
if attributes['original_purchase_date']
-
6
@original_purchase_date = DateTime.parse(attributes['original_purchase_date'])
-
end
-
8
if attributes['expiration_date']
-
6
@expires_at = Time.at(attributes['expiration_date'].to_i / 1000).to_datetime
-
end
-
-
8
@receipt_type = attributes['receipt_type']
-
8
@adam_id = attributes['adam_id']
-
8
@download_id = attributes['download_id']
-
8
@requested_at = DateTime.parse(attributes['request_date']) if attributes['request_date']
-
-
8
@in_app = []
-
8
if attributes['in_app']
-
6
attributes['in_app'].each do |in_app_purchase_attributes|
-
6
@in_app << InAppReceipt.new(in_app_purchase_attributes)
-
end
-
end
-
-
8
@pending_renewal_info = []
-
8
if original_json_response && original_json_response['pending_renewal_info']
-
5
original_json_response['pending_renewal_info'].each do |pending_renewal_attributes|
-
5
@pending_renewal_info << PendingRenewalInfo.new(pending_renewal_attributes)
-
end
-
end
-
end
-
-
1
def to_hash
-
{
-
bundle_id: @bundle_id,
-
application_version: @application_version,
-
original_application_version: @original_application_version,
-
1
original_purchase_date: (@original_purchase_date.httpdate rescue nil),
-
1
expires_at: (@expires_at.httpdate rescue nil),
-
receipt_type: @receipt_type,
-
adam_id: @adam_id,
-
download_id: @download_id,
-
1
requested_at: (@requested_at.httpdate rescue nil),
-
in_app: @in_app.map(&:to_h),
-
pending_renewal_info: @pending_renewal_info.map(&:to_h),
-
latest_receipt_info: @latest_receipt_info
-
1
}
-
end
-
1
alias_method :to_h, :to_hash
-
-
1
def to_json
-
to_hash.to_json
-
end
-
-
1
class << self
-
1
def verify(data, options = {})
-
1
verify!(data, options) rescue false
-
end
-
-
1
def verify!(data, options = {})
-
1
client = Client.production
-
-
1
begin
-
1
client.verify!(data, options)
-
rescue VerificationError => error
-
case error.code
-
when 21007
-
client = Client.development
-
retry
-
when 21008
-
client = Client.production
-
retry
-
else
-
raise error
-
end
-
end
-
end
-
-
1
alias :validate :verify
-
1
alias :validate! :verify!
-
end
-
-
1
class VerificationError < StandardError
-
1
attr_accessor :code
-
1
attr_accessor :receipt
-
-
1
def initialize(code, receipt)
-
@code = Integer(code)
-
@receipt = receipt
-
end
-
-
1
def message
-
case @code
-
when 21000
-
'The App Store could not read the JSON object you provided.'
-
when 21002
-
'The data in the receipt-data property was malformed.'
-
when 21003
-
'The receipt could not be authenticated.'
-
when 21004
-
'The shared secret you provided does not match the shared secret on file for your account.'
-
when 21005
-
'The receipt server is not currently available.'
-
when 21006
-
'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.'
-
when 21007
-
'This receipt is a sandbox receipt, but it was sent to the production service for verification.'
-
when 21008
-
'This receipt is a production receipt, but it was sent to the sandbox service for verification.'
-
when 21010
-
'This receipt could not be authorized. Treat this the same as if a purchase was never made.'
-
when 21100..21199
-
'Internal data access error.'
-
else
-
"Unknown Error: #{@code}"
-
end
-
end
-
end
-
end
-
end
-
1
module Venice
-
1
VERSION = '0.4.1'
-
end