mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
Merging #3756 into `main` Co-authored-by: Nick Kohrn <nickkohrn@users.noreply.github.com>
136 lines
4.8 KiB
Swift
136 lines
4.8 KiB
Swift
//
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the MIT License (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://opensource.org/licenses/MIT
|
|
//
|
|
// PurchasesReceiptParser.swift
|
|
//
|
|
// Created by Andrés Boedo on 7/22/20.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// A type that can parse Apple receipts from a device.
|
|
/// This implements parsing based on [Apple's documentation](https://rev.cat/apple-receipt-fields).
|
|
///
|
|
/// To use this class you must access ``PurchasesReceiptParser/default``:
|
|
/// ```swift
|
|
/// let parser = PurchasesReceiptParser.default
|
|
/// let receipt = try parser.parse(from: data)
|
|
/// ```
|
|
public class PurchasesReceiptParser: NSObject {
|
|
|
|
private let logger: LoggerType
|
|
private let containerBuilder: ASN1ContainerBuilder
|
|
private let receiptBuilder: AppleReceiptBuilder
|
|
|
|
internal init(logger: LoggerType,
|
|
containerBuilder: ASN1ContainerBuilder = ASN1ContainerBuilder(),
|
|
receiptBuilder: AppleReceiptBuilder = AppleReceiptBuilder()) {
|
|
self.logger = logger
|
|
self.containerBuilder = containerBuilder
|
|
self.receiptBuilder = receiptBuilder
|
|
}
|
|
|
|
/// Returns the result of parsing the receipt from `receiptData`
|
|
/// - Throws: ``PurchasesReceiptParser/Error``.
|
|
public func parse(from receiptData: Data) throws -> AppleReceipt {
|
|
#if DEBUG
|
|
Self.ensureRunningOutsideOfMainThread()
|
|
#endif
|
|
|
|
self.logger.info(ReceiptStrings.parsing_receipt)
|
|
|
|
let asn1Container = try self.containerBuilder.build(fromPayload: ArraySlice(receiptData))
|
|
guard let receiptASN1Container = try self.findASN1Container(withObjectId: ASN1ObjectIdentifier.data,
|
|
inContainer: asn1Container) else {
|
|
self.logger.error(ReceiptStrings.data_object_identifier_not_found_receipt)
|
|
throw Error.dataObjectIdentifierMissing
|
|
}
|
|
|
|
let receipt = try self.receiptBuilder.build(fromContainer: receiptASN1Container)
|
|
self.logger.info(ReceiptStrings.parsing_receipt_success)
|
|
return receipt
|
|
}
|
|
|
|
}
|
|
|
|
public extension PurchasesReceiptParser {
|
|
|
|
/// Returns the result of parsing the receipt from a base64 encoded string.
|
|
/// - Throws: ``PurchasesReceiptParser/Error``.
|
|
func parse(base64String string: String) throws -> AppleReceipt {
|
|
guard let data = Data(base64Encoded: string) else {
|
|
throw Error.failedToDecodeBase64String
|
|
}
|
|
|
|
return try self.parse(from: data)
|
|
}
|
|
|
|
}
|
|
|
|
// @unchecked because:
|
|
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
|
|
extension PurchasesReceiptParser: @unchecked Sendable {}
|
|
|
|
// MARK: - Internal
|
|
|
|
extension PurchasesReceiptParser {
|
|
|
|
@objc
|
|
func receiptHasTransactions(receiptData: Data) -> Bool {
|
|
if let receipt = try? self.parse(from: receiptData) {
|
|
return !receipt.inAppPurchases.isEmpty
|
|
}
|
|
|
|
self.logger.warn(ReceiptStrings.parsing_receipt_failed(fileName: #fileID, functionName: #function))
|
|
return true
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private extension PurchasesReceiptParser {
|
|
|
|
func findASN1Container(withObjectId objectId: ASN1ObjectIdentifier,
|
|
inContainer container: ASN1Container) throws -> ASN1Container? {
|
|
if container.encodingType == .constructed {
|
|
for (index, internalContainer) in container.internalContainers.enumerated() {
|
|
if internalContainer.containerIdentifier == .objectIdentifier {
|
|
let objectIdentifier = try ASN1ObjectIdentifierBuilder.build(
|
|
fromPayload: internalContainer.internalPayload)
|
|
if objectIdentifier == objectId && index < container.internalContainers.count - 1 {
|
|
// the container that holds the data comes right after the one with the object identifier
|
|
return container.internalContainers[index + 1]
|
|
}
|
|
} else {
|
|
let receipt = try self.findASN1Container(withObjectId: objectId, inContainer: internalContainer)
|
|
if receipt != nil {
|
|
return receipt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
#if DEBUG
|
|
static func ensureRunningOutsideOfMainThread() {
|
|
// Only checking on integration tests.
|
|
// Unit tests might run on the main thread when testing this class directly.
|
|
if ProcessInfo.processInfo.environment["RCRunningIntegrationTests"] == "1" {
|
|
precondition(
|
|
!Thread.isMainThread,
|
|
"Receipt parsing should not run on the main thread."
|
|
)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
}
|