Files
purchases-ios/Sources/LocalReceiptParsing/Builders/InAppPurchaseBuilder.swift
T
NachoSoto b3875a9380 Exposed PurchasesReceiptParser and AppleReceipt (#2153)
Initial step for [CSDK-17]. Fixes [CSDK-523].

### Changes:
- Renamed `ReceiptParser` to `PurchasesReceiptParser` to avoid name
collision with future SPM `ReceiptParser`
- Exposed `ReceiptParser`, `AppleReceipt`,
`PurchasesReceiptParser.Error`
- Added new types to `SwiftAPITester`
- Added docs to these types
- Moved `LoggerType` to be available to `ReceiptParser`
- Moved `DateFormatter+Extensions` to be available to `ReceiptParser`
- Moved `Codable+Extensions` to be available to `ReceiptParser`
- Created `ReceiptParserLogger` for `ReceiptParser`


[CSDK-17]:
https://revenuecats.atlassian.net/browse/CSDK-17?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

[CSDK-523]:
https://revenuecats.atlassian.net/browse/CSDK-523?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-12-22 10:09:12 -08:00

128 lines
5.5 KiB
Swift

//
// Created by Andrés Boedo on 7/29/20.
// Copyright (c) 2020 Purchases. All rights reserved.
//
import Foundation
class InAppPurchaseBuilder {
typealias InAppPurchase = AppleReceipt.InAppPurchase
// swiftlint:disable:next line_length
// https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
enum AttributeType: Int {
case quantity = 1701,
productId = 1702,
transactionId = 1703,
purchaseDate = 1704,
originalTransactionId = 1705,
originalPurchaseDate = 1706,
productType = 1707,
expiresDate = 1708,
webOrderLineItemId = 1711,
cancellationDate = 1712,
isInTrialPeriod = 1713,
isInIntroOfferPeriod = 1719,
promotionalOfferIdentifier = 1721
}
private let containerBuilder: ASN1ContainerBuilder
private let typeContainerIndex = 0
private let versionContainerIndex = 1 // unused
private let attributeTypeContainerIndex = 2
private let expectedInternalContainersCount = 3 // type + version + attribute
init() {
self.containerBuilder = ASN1ContainerBuilder()
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
func build(fromContainer container: ASN1Container) throws -> InAppPurchase {
var quantity: Int?
var productId: String?
var transactionId: String?
var originalTransactionId: String?
var productType: InAppPurchase.ProductType = .unknown
var purchaseDate: Date?
var originalPurchaseDate: Date?
var expiresDate: Date?
var cancellationDate: Date?
var isInTrialPeriod: Bool?
var isInIntroOfferPeriod: Bool?
var webOrderLineItemId: Int64?
var promotionalOfferIdentifier: String?
for internalContainer in container.internalContainers {
guard internalContainer.internalContainers.count == expectedInternalContainersCount else {
throw PurchasesReceiptParser.Error.inAppPurchaseParsingError
}
let typeContainer = internalContainer.internalContainers[typeContainerIndex]
let valueContainer = internalContainer.internalContainers[attributeTypeContainerIndex]
guard let attributeType = AttributeType(rawValue: typeContainer.internalPayload.toInt())
else { continue }
let internalContainer = try containerBuilder.build(fromPayload: valueContainer.internalPayload)
guard internalContainer.length.value > 0 else { continue }
switch attributeType {
case .quantity:
quantity = internalContainer.internalPayload.toInt()
case .webOrderLineItemId:
webOrderLineItemId = internalContainer.internalPayload.toInt64()
case .productType:
productType = .init(rawValue: internalContainer.internalPayload.toInt()) ?? .unknown
case .isInIntroOfferPeriod:
isInIntroOfferPeriod = internalContainer.internalPayload.toBool()
case .isInTrialPeriod:
isInTrialPeriod = internalContainer.internalPayload.toBool()
case .productId:
productId = internalContainer.internalPayload.toString()
case .transactionId:
transactionId = internalContainer.internalPayload.toString()
case .originalTransactionId:
originalTransactionId = internalContainer.internalPayload.toString()
case .promotionalOfferIdentifier:
promotionalOfferIdentifier = internalContainer.internalPayload.toString()
case .cancellationDate:
cancellationDate = internalContainer.internalPayload.toDate()
case .expiresDate:
expiresDate = internalContainer.internalPayload.toDate()
case .originalPurchaseDate:
originalPurchaseDate = internalContainer.internalPayload.toDate()
case .purchaseDate:
purchaseDate = internalContainer.internalPayload.toDate()
}
}
guard let nonOptionalQuantity = quantity,
let nonOptionalProductId = productId,
let nonOptionalTransactionId = transactionId,
let nonOptionalPurchaseDate = purchaseDate else {
throw PurchasesReceiptParser.Error.inAppPurchaseParsingError
}
return InAppPurchase(quantity: nonOptionalQuantity,
productId: nonOptionalProductId,
transactionId: nonOptionalTransactionId,
originalTransactionId: originalTransactionId,
productType: productType,
purchaseDate: nonOptionalPurchaseDate,
originalPurchaseDate: originalPurchaseDate,
expiresDate: expiresDate,
cancellationDate: cancellationDate,
isInTrialPeriod: isInTrialPeriod,
isInIntroOfferPeriod: isInIntroOfferPeriod,
webOrderLineItemId: webOrderLineItemId,
promotionalOfferIdentifier: promotionalOfferIdentifier)
}
}
// @unchecked because:
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
extension InAppPurchaseBuilder: @unchecked Sendable {}