Files
2024-01-30 10:50:59 +01:00

318 lines
8.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// InAppReceipt.swift
// TPReceiptValidator
//
// Created by Pavel Tikhonenko on 28/09/16.
// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
import ASN1Swift
public struct InAppReceiptField
{
static let environment: Int32 = 0 // Sandbox, Production, ProductionSandbox
static let bundleIdentifier: Int32 = 2
static let appVersion: Int32 = 3
static let opaqueValue: Int32 = 4
static let receiptHash: Int32 = 5 // SHA-1 Hash
static let ageRating: Int32 = 10 // SHA-1 Hash
static let receiptCreationDate: Int32 = 12
static let inAppPurchaseReceipt: Int32 = 17 // The receipt for an in-app purchase.
static let originalAppPurchaseDate: Int32 = 18
static let originalAppVersion: Int32 = 19
static let expirationDate: Int32 = 21
static let quantity: Int32 = 1701
static let productIdentifier: Int32 = 1702
static let transactionIdentifier: Int32 = 1703
static let purchaseDate: Int32 = 1704
static let originalTransactionIdentifier: Int32 = 1705
static let originalPurchaseDate: Int32 = 1706
static let productType: Int32 = 1707
static let subscriptionExpirationDate: Int32 = 1708
static let webOrderLineItemID: Int32 = 1711
static let cancellationDate: Int32 = 1712
static let subscriptionTrialPeriod: Int32 = 1713
static let subscriptionIntroductoryPricePeriod: Int32 = 1719
static let promotionalOfferIdentifier: Int32 = 1721
}
public class InAppReceipt
{
/// PKCS7 container
internal var receipt: _InAppReceipt
/// Payload of the receipt.
/// Payload object contains all meta information.
internal var payload: InAppReceiptPayload { receipt.payload }
/// root certificate path, used to check signature
/// added for testing purpose , as unit test can't read main bundle
internal var rootCertificatePath: String?
/// Raw data
private var rawData: Data
/// Initialize a `InAppReceipt` using local receipt
public convenience init() throws
{
let data = try Bundle.main.appStoreReceiptData()
try self.init(receiptData: data)
}
///
///
/// Initialize a `InAppReceipt` with asn1 payload
///
/// - parameter receiptData: `Data` object that represents receipt
public init(receiptData: Data, rootCertPath: String? = nil) throws
{
self.receipt = try _InAppReceipt(rawData: receiptData)
self.rawData = receiptData
#if DEBUG
let certificateName = "StoreKitTestCertificate"
#else
let certificateName = "AppleIncRootCertificate"
#endif
self.rootCertificatePath = rootCertPath ?? Bundle.lookUp(forResource: certificateName, ofType: "cer")
}
}
public extension InAppReceipt
{
/// The apps bundle identifier
var bundleIdentifier: String
{
return payload.bundleIdentifier
}
/// The apps version number
var appVersion: String
{
return payload.appVersion
}
/// The version of the app that was originally purchased.
var originalAppVersion: String
{
return payload.originalAppVersion
}
/// The date of the app that was originally purchased.
var originalPurchaseDate: Date?
{
return payload.originalPurchaseDate
}
/// In-app purchase's receipts
var purchases: [InAppPurchase]
{
return payload.purchases
}
/// Returns all auto renewable `InAppPurchase`s,
var autoRenewablePurchases: [InAppPurchase]
{
return purchases.filter({ $0.isRenewableSubscription })
}
/// Returns all ACTIVE auto renewable `InAppPurchase`s,
///
var activeAutoRenewableSubscriptionPurchases: [InAppPurchase]
{
return purchases.filter({ $0.isRenewableSubscription && $0.isActiveAutoRenewableSubscription(forDate: Date()) })
}
/// The date that the app receipt expires
var expirationDate: Date?
{
return payload.expirationDate
}
/// Returns `true` if any purchases exist, `false` otherwise
var hasPurchases: Bool
{
return purchases.count > 0
}
/// Returns `true` if any Active Auto Renewable purchases exist, `false` otherwise
var hasActiveAutoRenewablePurchases: Bool
{
return activeAutoRenewableSubscriptionPurchases.count > 0
}
var creationDate: Date
{
return payload.creationDate
}
var ageRating: String
{
return payload.ageRating
}
/// In App Receipt in base64
var base64: String
{
return rawData.base64EncodedString()
}
/// Return original transaction identifier if there is a purchase for a specific product identifier
///
/// - parameter productIdentifier: Product name
func originalTransactionIdentifier(ofProductIdentifier productIdentifier: String) -> String?
{
return purchases(ofProductIdentifier: productIdentifier).first?.originalTransactionIdentifier
}
/// Returns `true` if there is a purchase for a specific product identifier, `false` otherwise
///
/// - parameter productIdentifier: Product name
func containsPurchase(ofProductIdentifier productIdentifier: String) -> Bool
{
for item in purchases
{
if item.productIdentifier == productIdentifier
{
return true
}
}
return false
}
/// Returns `[InAppPurchase]` if there are purchases for a specific product identifier,
/// empty array otherwise
///
/// - parameter productIdentifier: Product name
/// - parameter sort: Sorting block
func purchases(ofProductIdentifier productIdentifier: String,
sortedBy sort: ((InAppPurchase, InAppPurchase) -> Bool)? = nil) -> [InAppPurchase]
{
let filtered: [InAppPurchase] = purchases.filter({
return $0.productIdentifier == productIdentifier
})
if let sort = sort
{
return filtered.sorted(by: {
return sort($0, $1)
})
}else{
return filtered.sorted(by: {
return $0.purchaseDate > $1.purchaseDate
})
}
}
/// Returns `InAppPurchase` if there is a purchase for a specific product identifier,
/// `nil` otherwise
///
/// - parameter productIdentifier: Product name
func activeAutoRenewableSubscriptionPurchases(ofProductIdentifier productIdentifier: String, forDate date: Date) -> InAppPurchase?
{
let filtered = purchases(ofProductIdentifier: productIdentifier)
for purchase in filtered
{
if purchase.isActiveAutoRenewableSubscription(forDate: date)
{
return purchase
}
}
return nil
}
/// Returns the last `InAppPurchase` if there is one for a specific product identifier,
/// `nil` otherwise
///
/// - parameter productIdentifier: Product name
func lastAutoRenewableSubscriptionPurchase(ofProductIdentifier productIdentifier: String) -> InAppPurchase?
{
var purchase: InAppPurchase? = nil
let filtered = purchases(ofProductIdentifier: productIdentifier)
var lastInterval: TimeInterval = 0
for iap in filtered
{
if let thisInterval = iap.subscriptionExpirationDate?.timeIntervalSince1970
{
if purchase == nil || thisInterval > lastInterval
{
purchase = iap
lastInterval = thisInterval
}
}
}
return purchase
}
/// Returns true if there is an active subscription for a specific product identifier on the date specified,
/// false otherwise
///
/// - parameter productIdentifier: Product name
/// - parameter date: Date to check subscription against
func hasActiveAutoRenewableSubscription(ofProductIdentifier productIdentifier: String, forDate date: Date) -> Bool
{
return activeAutoRenewableSubscriptionPurchases(ofProductIdentifier: productIdentifier, forDate: date) != nil
}
}
internal extension InAppReceipt
{
/// Used to validate the receipt
var bundleIdentifierData: Data
{
return payload.bundleIdentifierData
}
/// An opaque value used, with other data, to compute the SHA-1 hash during validation.
var opaqueValue: Data
{
return payload.opaqueValue
}
/// A SHA-1 hash, used to validate the receipt.
var receiptHash: Data
{
return payload.receiptHash
}
}
public extension InAppReceipt
{
/// signature for validation
var signature: Data?
{
return receipt.signatureData
}
var worldwideDeveloperCertificateData: Data?
{
return receipt.worldwideDeveloperCertificateData
}
var iTunesCertificateData: Data?
{
return receipt.iTunesCertificateData
}
var iTunesPublicKeyData: Data?
{
return receipt.iTunesPublicKeyData
}
var payloadRawData: Data
{
return payload.rawData
}
}