This commit is contained in:
2025-12-16 13:10:50 +08:00
parent 444877fb73
commit fd0ddfd45a
17 changed files with 4751 additions and 3 deletions

View File

@@ -0,0 +1,475 @@
//
// TransactionConverter.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
/// Transaction
/// Transaction Dictionary/JSON
public struct TransactionConverter {
/// Transaction Dictionary JSON
/// - Parameter transaction: Transaction
/// - Returns: Dictionary
public static func toDictionary(_ transaction: Transaction) -> [String: Any] {
var dict: [String: Any] = [:]
//
dict["id"] = String(transaction.id)
dict["productID"] = transaction.productID
//
dict["productType"] = productTypeToString(transaction.productType)
//
if let price = transaction.price {
dict["price"] = Double(String(format: "%.2f", NSDecimalNumber(decimal: price).doubleValue)) ?? NSDecimalNumber(decimal: price).doubleValue
} else {
dict["price"] = 0.00
}
// iOS 16.0+
if #available(iOS 16.0, *) {
if let currency = transaction.currency {
//
dict["currency"] = String(describing: currency)
} else {
dict["currency"] = NSNull()
}
} else {
dict["currency"] = NSNull()
}
//
dict["ownershipType"] = ownershipTypeToString(transaction.ownershipType)
// ID
dict["originalID"] = String(transaction.originalID)
//
dict["originalPurchaseDate"] = dateToTimestamp(transaction.originalPurchaseDate)
dict["purchaseDate"] = dateToTimestamp(transaction.purchaseDate)
//
dict["purchasedQuantity"] = transaction.purchasedQuantity
// iOS 17.0+
if #available(iOS 17.0, *) {
dict["purchaseReason"] = transactionReasonToString(transaction.reason)
} else {
dict["purchaseReason"] = ""
}
// ID
if let subscriptionGroupID = transaction.subscriptionGroupID {
dict["subscriptionGroupID"] = String(describing: subscriptionGroupID)
} else {
dict["subscriptionGroupID"] = NSNull()
}
//
if let expirationDate = transaction.expirationDate {
dict["expirationDate"] = dateToTimestamp(expirationDate)
} else {
dict["expirationDate"] = NSNull()
}
//
dict["isUpgraded"] = transaction.isUpgraded
//
if let revocationDate = transaction.revocationDate {
dict["hasRevocation"] = true
dict["revocationDate"] = dateToTimestamp(revocationDate)
} else {
dict["hasRevocation"] = false
dict["revocationDate"] = NSNull()
}
//
if let revocationReason = transaction.revocationReason {
dict["revocationReason"] = revocationReasonToString(revocationReason)
} else {
dict["revocationReason"] = NSNull()
}
// iOS 16.0+
if #available(iOS 16.0, *) {
dict["environment"] = environmentToString(transaction.environment)
} else {
dict["environment"] = "unknown"
}
//
if let appAccountToken = transaction.appAccountToken {
dict["appAccountToken"] = appAccountToken.uuidString
} else {
dict["appAccountToken"] = ""
}
// IDiOS 18.4+
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
dict["appBundleID"] = String(describing: transaction.appBundleID)
dict["appTransactionID"] = transaction.appTransactionID
} else {
dict["appTransactionID"] = NSNull()
dict["appBundleID"] = NSNull()
}
//
dict["signedDate"] = dateToTimestamp(transaction.signedDate)
// iOS 17.0+
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *) {
let storefront = transaction.storefront
dict["storefrontId"] = storefront.id
dict["storefrontCountryCode"] = storefront.countryCode
if let currency = storefront.currency {
dict["storefrontCurrency"] = String(describing: currency)
} else {
dict["storefrontCurrency"] = ""
}
} else {
dict["storefrontId"] = ""
dict["storefrontCountryCode"] = ""
dict["storefrontCurrency"] = ""
}
// WebID
if let webOrderLineItemID = transaction.webOrderLineItemID {
dict["webOrderLineItemID"] = webOrderLineItemID
} else {
dict["webOrderLineItemID"] = ""
}
//
dict["deviceVerification"] = transaction.deviceVerification.base64EncodedString()
// Nonce
dict["deviceVerificationNonce"] = transaction.deviceVerificationNonce.uuidString
//
dict["offer"] = offerToDictionary(from: transaction)
// iOS 18.4+
// Transaction.AdvancedCommerceInfo API
//
return dict
}
/// Transaction Dictionary
/// - Parameter transactions: Transaction
/// - Returns: Dictionary
public static func toDictionaryArray(_ transactions: [Transaction]) -> [[String: Any]] {
return transactions.map { toDictionary($0) }
}
/// Transaction JSON
/// - Parameter transaction: Transaction
/// - Returns: JSON
public static func toJSONString(_ transaction: Transaction) -> String? {
let dict = toDictionary(transaction)
return dictionaryToJSONString(dict)
}
/// Transaction JSON
/// - Parameter transactions: Transaction
/// - Returns: JSON
public static func toJSONString(_ transactions: [Transaction]) -> String? {
let array = toDictionaryArray(transactions)
return arrayToJSONString(array)
}
// MARK: -
///
private static func dateToTimestamp(_ date: Date) -> Int64 {
return Int64(date.timeIntervalSince1970 * 1000)
}
///
private static func productTypeToString(_ type: Product.ProductType) -> String {
switch type {
case .consumable:
return "consumable"
case .nonConsumable:
return "nonConsumable"
case .autoRenewable:
return "autoRenewable"
case .nonRenewable:
return "nonRenewable"
default:
return "unknown"
}
}
///
private static func ownershipTypeToString(_ type: Transaction.OwnershipType) -> String {
switch type {
case .purchased:
return "purchased"
case .familyShared:
return "familyShared"
default:
return "unknown"
}
}
///
@available(iOS 16.0, *)
private static func environmentToString(_ environment: AppStore.Environment) -> String {
switch environment {
case .production:
return "production"
case .sandbox:
return "sandbox"
case .xcode:
return "xcode"
default:
return "unknown"
}
}
///
@available(iOS 17.0, *)
private static func transactionReasonToString(_ reason: Transaction.Reason) -> String {
switch reason {
case .purchase:
return "purchase"
case .renewal:
return "renewal"
default:
return "unknown"
}
}
///
private static func revocationReasonToString(_ reason: Transaction.RevocationReason) -> String {
return extractEnumValueName(from: reason)
}
///
/// - Parameter value:
/// - Returns:
private static func extractEnumValueName<T>(from value: T) -> String {
let valueString = String(describing: value)
// "Transaction.OfferType.introductory" -> "introductory"
if let lastDot = valueString.lastIndex(of: ".") {
return String(valueString[valueString.index(after: lastDot)...])
}
return valueString
}
/// iOS 15.0-17.1
@available(iOS 15.0, *)
private static func transactionOfferTypeDeprecatedToString(_ type: Transaction.OfferType) -> String {
switch type {
case .introductory:
return "introductory"
case .promotional:
return "promotional"
case .code:
return "code"
default:
return "unknown"
}
}
/// Product.SubscriptionOffer.PaymentMode
private static func paymentModeToString(_ mode: Product.SubscriptionOffer.PaymentMode) -> String {
switch mode {
case .freeTrial:
return "freeTrial"
case .payAsYouGo:
return "payAsYouGo"
case .payUpFront:
return "payUpFront"
default:
return "unknown"
}
}
// MARK: - Offer
/// Transaction.OfferiOS 17.2+
@available(iOS 17.2, *)
private static func transactionOfferTypeToString(_ type: Transaction.OfferType) -> String {
// 使 if-else switch
if type == .introductory {
return "introductory"
} else if type == .promotional {
return "promotional"
} else if type == .code {
return "code"
} else {
// iOS 18.0+ winBack
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
if type == .winBack {
return "winBack"
}
}
return "unknown"
}
}
/// Transaction.Offer.PaymentMode
@available(iOS 17.2, *)
private static func transactionOfferPaymentModeToString(_ mode: Transaction.Offer.PaymentMode) -> String {
switch mode {
case .freeTrial:
return "freeTrial"
case .payAsYouGo:
return "payAsYouGo"
case .payUpFront:
return "payUpFront"
default:
return "unknown"
}
}
/// Transaction Dictionary
/// - Parameter transaction: Transaction
/// - Returns: NSNull
private static func offerToDictionary(from transaction: Transaction) -> Any {
// iOS 17.2+ 使 offer
if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *) {
return modernOfferToDictionary(from: transaction)
}
// iOS 15.0 - iOS 17.1 使
else if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
return deprecatedOfferToDictionary(from: transaction)
}
// iOS 15.0
else {
return NSNull()
}
}
/// 使 offer iOS 17.2+
@available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *)
private static func modernOfferToDictionary(from transaction: Transaction) -> Any {
guard let offer = transaction.offer else {
return NSNull()
}
var offerDict: [String: Any] = [:]
//
offerDict["type"] = transactionOfferTypeToString(offer.type)
// ID
if let offerID = offer.id {
offerDict["id"] = offerID
} else {
offerDict["id"] = NSNull()
}
// 使
if let paymentMode = offer.paymentMode {
offerDict["paymentMode"] = transactionOfferPaymentModeToString(paymentMode)
} else {
offerDict["paymentMode"] = NSNull()
}
// iOS 18.4+
offerDict["periodCount"] = 0
offerDict["periodUnit"] = ""
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
if let period = offer.period {
offerDict["periodCount"] = period.value
offerDict["periodUnit"] = subscriptionPeriodUnitToString(period.unit)
}
}
return offerDict
}
/// 使iOS 15.0 - iOS 17.1
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private static func deprecatedOfferToDictionary(from transaction: Transaction) -> Any {
guard let offerType = transaction.offerType else {
return NSNull()
}
var offerDict: [String: Any] = [:]
//
offerDict["type"] = transactionOfferTypeDeprecatedToString(offerType)
// ID
if let offerID = transaction.offerID {
offerDict["id"] = String(describing: offerID)
} else {
offerDict["id"] = NSNull()
}
//
if let paymentMode = transaction.offerPaymentModeStringRepresentation {
offerDict["paymentMode"] = paymentMode
} else {
offerDict["paymentMode"] = NSNull()
}
// iOS 15.0 - iOS 18.3 使iOS 18.4+
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
// iOS 18.4+ offerPeriodStringRepresentation NSNull
offerDict["period"] = NSNull()
} else {
// iOS 15.0 - iOS 18.3 使
if let period = transaction.offerPeriodStringRepresentation {
offerDict["period"] = period
} else {
offerDict["period"] = NSNull()
}
}
return offerDict
}
/// Dictionary
private static func subscriptionPeriodToDictionary(_ period: Product.SubscriptionPeriod) -> [String: Any] {
var dict: [String: Any] = [:]
dict["value"] = period.value
dict["unit"] = subscriptionPeriodUnitToString(period.unit)
return dict
}
///
private static func subscriptionPeriodUnitToString(_ unit: Product.SubscriptionPeriod.Unit) -> String {
switch unit {
case .day:
return "day"
case .week:
return "week"
case .month:
return "month"
case .year:
return "year"
default:
return "unknown"
}
}
// Transaction.AdvancedCommerceProduct
// 使 jsonRepresentation
/// Dictionary JSON
private static func dictionaryToJSONString(_ dict: [String: Any]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return nil
}
return jsonString
}
/// Array JSON
private static func arrayToJSONString(_ array: [[String: Any]]) -> String? {
guard let jsonData = try? JSONSerialization.data(withJSONObject: array, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return nil
}
return jsonString
}
}