// // 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"] = "" } // 应用交易ID(iOS 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"] = "" } // Web订单行项目ID(如果有) 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(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.Offer,iOS 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 } }