476 lines
17 KiB
Swift
476 lines
17 KiB
Swift
//
|
||
// 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<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.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
|
||
}
|
||
}
|
||
|