1
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// ProductConverter.swift
|
||||
// StoreKit2Manager
|
||||
//
|
||||
// Created by xiaopin on 2025/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
/// Product 转换器
|
||||
/// 将 Product 对象转换为可序列化的基础数据类型(Dictionary/JSON)
|
||||
public struct ProductConverter {
|
||||
|
||||
/// 将 Product 转换为 Dictionary(可序列化为 JSON)
|
||||
/// - Parameter product: Product 对象
|
||||
/// - Returns: Dictionary 对象,包含所有产品信息
|
||||
public static func toDictionary(_ product: Product) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
// 基本信息(确保都是字符串类型)
|
||||
dict["id"] = product.id
|
||||
dict["displayName"] = product.displayName
|
||||
dict["description"] = product.description
|
||||
|
||||
// 价格信息
|
||||
let priceDecimal = product.price
|
||||
let priceDouble = NSDecimalNumber(decimal: priceDecimal).doubleValue
|
||||
dict["price"] = Double(String(format: "%.2f", priceDouble)) ?? priceDouble
|
||||
dict["displayPrice"] = product.displayPrice
|
||||
|
||||
// 产品类型
|
||||
dict["type"] = productTypeToString(product.type)
|
||||
|
||||
// 家庭共享
|
||||
dict["isFamilyShareable"] = product.isFamilyShareable
|
||||
|
||||
// JSON 表示(可选,可能很大,通常用于服务器验证)
|
||||
// 注意:jsonRepresentation 是 Data 类型,转换为 Base64 字符串以便 JSON 序列化
|
||||
let jsonData = product.jsonRepresentation
|
||||
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
dict["jsonRepresentation"] = jsonString
|
||||
} else {
|
||||
dict["jsonRepresentation"] = ""
|
||||
}
|
||||
|
||||
// 订阅信息(如果有)
|
||||
if let subscription = product.subscription {
|
||||
dict["subscription"] = SubscriptionConverter.subscriptionInfoToDictionary(subscription, product: product)
|
||||
} else {
|
||||
dict["subscription"] = NSNull()
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
/// 将 Product 数组转换为 Dictionary 数组
|
||||
/// - Parameter products: Product 数组
|
||||
/// - Returns: Dictionary 数组
|
||||
public static func toDictionaryArray(_ products: [Product]) -> [[String: Any]] {
|
||||
return products.map { toDictionary($0) }
|
||||
}
|
||||
|
||||
/// 将 Product 转换为 JSON 字符串
|
||||
/// - Parameter product: Product 对象
|
||||
/// - Returns: JSON 字符串
|
||||
public static func toJSONString(_ product: Product) -> String? {
|
||||
let dict = toDictionary(product)
|
||||
return dictionaryToJSONString(dict)
|
||||
}
|
||||
|
||||
/// 将 Product 数组转换为 JSON 字符串
|
||||
/// - Parameter products: Product 数组
|
||||
/// - Returns: JSON 字符串
|
||||
public static func toJSONString(_ products: [Product]) -> String? {
|
||||
let array = toDictionaryArray(products)
|
||||
return arrayToJSONString(array)
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
/// 产品类型转字符串
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
/// Dictionary 转 JSON 字符串
|
||||
internal 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 字符串
|
||||
internal 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// StoreKitConverter.swift
|
||||
// StoreKit2Manager
|
||||
//
|
||||
// Created by xiaopin on 2025/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
/// StoreKit 统一转换器
|
||||
/// 提供统一的转换接口,方便外部调用
|
||||
public struct StoreKitConverter {
|
||||
|
||||
// MARK: - Product 转换
|
||||
|
||||
/// 将 Product 转换为 Dictionary
|
||||
public static func productToDictionary(_ product: Product) -> [String: Any] {
|
||||
return ProductConverter.toDictionary(product)
|
||||
}
|
||||
|
||||
/// 将 Product 数组转换为 Dictionary 数组
|
||||
public static func productsToDictionaryArray(_ products: [Product]) -> [[String: Any]] {
|
||||
return ProductConverter.toDictionaryArray(products)
|
||||
}
|
||||
|
||||
/// 将 Product 转换为 JSON 字符串
|
||||
public static func productToJSONString(_ product: Product) -> String? {
|
||||
return ProductConverter.toJSONString(product)
|
||||
}
|
||||
|
||||
/// 将 Product 数组转换为 JSON 字符串
|
||||
public static func productsToJSONString(_ products: [Product]) -> String? {
|
||||
return ProductConverter.toJSONString(products)
|
||||
}
|
||||
|
||||
// MARK: - Transaction 转换
|
||||
|
||||
/// 将 Transaction 转换为 Dictionary
|
||||
public static func transactionToDictionary(_ transaction: Transaction) -> [String: Any] {
|
||||
return TransactionConverter.toDictionary(transaction)
|
||||
}
|
||||
|
||||
/// 将 Transaction 数组转换为 Dictionary 数组
|
||||
public static func transactionsToDictionaryArray(_ transactions: [Transaction]) -> [[String: Any]] {
|
||||
return TransactionConverter.toDictionaryArray(transactions)
|
||||
}
|
||||
|
||||
/// 将 Transaction 转换为 JSON 字符串
|
||||
public static func transactionToJSONString(_ transaction: Transaction) -> String? {
|
||||
return TransactionConverter.toJSONString(transaction)
|
||||
}
|
||||
|
||||
/// 将 Transaction 数组转换为 JSON 字符串
|
||||
public static func transactionsToJSONString(_ transactions: [Transaction]) -> String? {
|
||||
return TransactionConverter.toJSONString(transactions)
|
||||
}
|
||||
|
||||
// MARK: - StoreKitState 转换
|
||||
|
||||
/// 将 StoreKitState 转换为 Dictionary
|
||||
public static func stateToDictionary(_ state: StoreKitState) -> [String: Any] {
|
||||
return StoreKitStateConverter.toDictionary(state)
|
||||
}
|
||||
|
||||
/// 将 StoreKitState 转换为 JSON 字符串
|
||||
public static func stateToJSONString(_ state: StoreKitState) -> String? {
|
||||
return StoreKitStateConverter.toJSONString(state)
|
||||
}
|
||||
|
||||
// MARK: - RenewalInfo 转换
|
||||
|
||||
/// 将 RenewalInfo 转换为 Dictionary
|
||||
public static func renewalInfoToDictionary(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> [String: Any] {
|
||||
return SubscriptionConverter.renewalInfoToDictionary(renewalInfo)
|
||||
}
|
||||
|
||||
/// 将 RenewalInfo 转换为 JSON 字符串
|
||||
public static func renewalInfoToJSONString(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> String? {
|
||||
return SubscriptionConverter.renewalInfoToJSONString(renewalInfo)
|
||||
}
|
||||
|
||||
// MARK: - RenewalState 转换
|
||||
|
||||
/// 将 RenewalState 转换为字符串
|
||||
public static func renewalStateToString(_ state: Product.SubscriptionInfo.RenewalState) -> String {
|
||||
return SubscriptionConverter.renewalStateToString(state)
|
||||
}
|
||||
|
||||
// MARK: - SubscriptionInfo 转换
|
||||
|
||||
/// 将 SubscriptionInfo 转换为 Dictionary
|
||||
public static func subscriptionInfoToDictionary(_ subscription: Product.SubscriptionInfo, product: Product? = nil) -> [String: Any] {
|
||||
return SubscriptionConverter.subscriptionInfoToDictionary(subscription, product: product)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// StoreKitStateConverter.swift
|
||||
// StoreKit2Manager
|
||||
//
|
||||
// Created by xiaopin on 2025/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
/// StoreKitState 转换器
|
||||
/// 将 StoreKitState 对象转换为可序列化的基础数据类型(Dictionary/JSON)
|
||||
public struct StoreKitStateConverter {
|
||||
|
||||
/// 将 StoreKitState 转换为 Dictionary(可序列化为 JSON)
|
||||
/// - Parameter state: StoreKitState 对象
|
||||
/// - Returns: Dictionary 对象,包含状态信息
|
||||
public static func toDictionary(_ state: StoreKitState) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
switch state {
|
||||
case .idle:
|
||||
dict["type"] = "idle"
|
||||
|
||||
case .loadingProducts:
|
||||
dict["type"] = "loadingProducts"
|
||||
|
||||
// case .productsLoaded(let products):
|
||||
// dict["type"] = "productsLoaded"
|
||||
// dict["products"] = ProductConverter.toDictionaryArray(products)
|
||||
|
||||
case .loadingPurchases:
|
||||
dict["type"] = "loadingPurchases"
|
||||
|
||||
case .purchasesLoaded:
|
||||
dict["type"] = "purchasesLoaded"
|
||||
|
||||
case .purchasing(let productId):
|
||||
dict["type"] = "purchasing"
|
||||
dict["productId"] = productId
|
||||
|
||||
case .purchaseSuccess(let productId):
|
||||
dict["type"] = "purchaseSuccess"
|
||||
dict["productId"] = productId
|
||||
|
||||
case .purchasePending(let productId):
|
||||
dict["type"] = "purchasePending"
|
||||
dict["productId"] = productId
|
||||
|
||||
case .purchaseCancelled(let productId):
|
||||
dict["type"] = "purchaseCancelled"
|
||||
dict["productId"] = productId
|
||||
|
||||
case .purchaseFailed(let productId, let error):
|
||||
dict["type"] = "purchaseFailed"
|
||||
dict["productId"] = productId
|
||||
dict["error"] = String(describing: error)
|
||||
|
||||
// case .subscriptionStatusChanged(let renewalState):
|
||||
// dict["type"] = "subscriptionStatusChanged"
|
||||
// dict["renewalState"] = renewalStateToString(renewalState)
|
||||
|
||||
case .restoringPurchases:
|
||||
dict["type"] = "restoringPurchases"
|
||||
|
||||
case .restorePurchasesSuccess:
|
||||
dict["type"] = "restorePurchasesSuccess"
|
||||
|
||||
case .restorePurchasesFailed(let error):
|
||||
dict["type"] = "restorePurchasesFailed"
|
||||
dict["error"] = String(describing: error)
|
||||
|
||||
case .purchaseRefunded(let productId):
|
||||
dict["type"] = "purchaseRefunded"
|
||||
dict["productId"] = productId
|
||||
|
||||
case .purchaseRevoked(let productId):
|
||||
dict["type"] = "purchaseRevoked"
|
||||
dict["productId"] = productId
|
||||
|
||||
case .subscriptionCancelled(let productId, let isFreeTrialCancelled):
|
||||
dict["type"] = "subscriptionCancelled"
|
||||
dict["productId"] = productId
|
||||
dict["isFreeTrialCancelled"] = isFreeTrialCancelled
|
||||
|
||||
case .error(let error):
|
||||
dict["type"] = "error"
|
||||
dict["error"] = String(describing: error)
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
/// 将 StoreKitState 转换为 JSON 字符串
|
||||
/// - Parameter state: StoreKitState 对象
|
||||
/// - Returns: JSON 字符串
|
||||
public static func toJSONString(_ state: StoreKitState) -> String? {
|
||||
let dict = toDictionary(state)
|
||||
return dictionaryToJSONString(dict)
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
/// 续订状态转字符串
|
||||
private static func renewalStateToString(_ state: Product.SubscriptionInfo.RenewalState) -> String {
|
||||
switch state {
|
||||
case .subscribed:
|
||||
return "subscribed"
|
||||
case .expired:
|
||||
return "expired"
|
||||
case .inBillingRetryPeriod:
|
||||
return "inBillingRetryPeriod"
|
||||
case .inGracePeriod:
|
||||
return "inGracePeriod"
|
||||
case .revoked:
|
||||
return "revoked"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//
|
||||
// SubscriptionConverter.swift
|
||||
// StoreKit2Manager
|
||||
//
|
||||
// Created by xiaopin on 2025/12/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
/// 订阅相关转换器
|
||||
/// 将订阅相关的对象转换为可序列化的基础数据类型(Dictionary/JSON)
|
||||
public struct SubscriptionConverter {
|
||||
|
||||
// MARK: - SubscriptionInfo
|
||||
|
||||
/// 将 SubscriptionInfo 转换为 Dictionary(同步版本,不包含异步属性)
|
||||
/// - Parameters:
|
||||
/// - subscription: SubscriptionInfo 对象
|
||||
/// - product: 关联的 Product 对象(可选)
|
||||
/// - Returns: Dictionary 对象
|
||||
public static func subscriptionInfoToDictionary(_ subscription: Product.SubscriptionInfo, product: Product? = nil) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
// 订阅组ID
|
||||
dict["subscriptionGroupID"] = subscription.subscriptionGroupID
|
||||
|
||||
// 订阅周期
|
||||
dict["subscriptionPeriodCount"] = subscription.subscriptionPeriod.value
|
||||
dict["subscriptionPeriodUnit"] = subscriptionPeriodUnitToString(subscription.subscriptionPeriod.unit)
|
||||
|
||||
// 介绍性优惠(如果有)
|
||||
if let introOffer = subscription.introductoryOffer {
|
||||
dict["introductoryOffer"] = subscriptionOfferToDictionary(introOffer)
|
||||
} else {
|
||||
dict["introductoryOffer"] = NSNull()
|
||||
}
|
||||
|
||||
// 促销优惠列表
|
||||
dict["promotionalOffers"] = subscription.promotionalOffers.map { subscriptionOfferToDictionary($0) }
|
||||
|
||||
// 赢回优惠列表(iOS 18.0+)
|
||||
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
|
||||
dict["winBackOffers"] = subscription.winBackOffers.map { subscriptionOfferToDictionary($0) }
|
||||
} else {
|
||||
dict["winBackOffers"] = []
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
// MARK: - RenewalInfo
|
||||
|
||||
/// 将 RenewalInfo 转换为 Dictionary
|
||||
/// - Parameter renewalInfo: RenewalInfo 对象
|
||||
/// - Returns: Dictionary 对象
|
||||
public static func renewalInfoToDictionary(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
// 是否自动续订
|
||||
dict["willAutoRenew"] = renewalInfo.willAutoRenew
|
||||
|
||||
// 续订日期(如果有)
|
||||
if let renewalDate = renewalInfo.renewalDate {
|
||||
dict["renewalDate"] = dateToTimestamp(renewalDate)
|
||||
} else {
|
||||
dict["renewalDate"] = NSNull()
|
||||
}
|
||||
|
||||
// 过期原因(如果有)
|
||||
if let expirationReason = renewalInfo.expirationReason {
|
||||
dict["expirationReason"] = expirationReasonToString(expirationReason)
|
||||
} else {
|
||||
dict["expirationReason"] = NSNull()
|
||||
}
|
||||
|
||||
// 注意:过期日期(expirationDate)不在 RenewalInfo 中,需要从 Transaction 中获取
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
/// 将 RenewalInfo 转换为 JSON 字符串
|
||||
/// - Parameter renewalInfo: RenewalInfo 对象
|
||||
/// - Returns: JSON 字符串
|
||||
public static func renewalInfoToJSONString(_ renewalInfo: Product.SubscriptionInfo.RenewalInfo) -> String? {
|
||||
let dict = renewalInfoToDictionary(renewalInfo)
|
||||
return dictionaryToJSONString(dict)
|
||||
}
|
||||
|
||||
// MARK: - RenewalState
|
||||
|
||||
/// 将 RenewalState 转换为字符串
|
||||
/// - Parameter state: RenewalState 对象
|
||||
/// - Returns: 字符串
|
||||
public static func renewalStateToString(_ state: Product.SubscriptionInfo.RenewalState) -> String {
|
||||
switch state {
|
||||
case .subscribed:
|
||||
return "subscribed"
|
||||
case .expired:
|
||||
return "expired"
|
||||
case .inBillingRetryPeriod:
|
||||
return "inBillingRetryPeriod"
|
||||
case .inGracePeriod:
|
||||
return "inGracePeriod"
|
||||
case .revoked:
|
||||
return "revoked"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SubscriptionPeriod
|
||||
|
||||
/// 将 SubscriptionPeriod 转换为 Dictionary
|
||||
/// - Parameter period: SubscriptionPeriod 对象
|
||||
/// - Returns: Dictionary 对象
|
||||
public static func subscriptionPeriodToDictionary(_ period: Product.SubscriptionPeriod) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
dict["value"] = period.value
|
||||
dict["unit"] = subscriptionPeriodUnitToString(period.unit)
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
// MARK: - SubscriptionOffer
|
||||
|
||||
/// 将 SubscriptionOffer 转换为 Dictionary
|
||||
/// - Parameter offer: SubscriptionOffer 对象
|
||||
/// - Returns: Dictionary 对象
|
||||
private static func subscriptionOfferToDictionary(_ offer: Product.SubscriptionOffer) -> [String: Any] {
|
||||
var dict: [String: Any] = [:]
|
||||
|
||||
// 优惠ID(介绍性优惠为 nil,确保是字符串类型)
|
||||
if let offerID = offer.id {
|
||||
dict["id"] = offerID
|
||||
} else {
|
||||
dict["id"] = NSNull()
|
||||
}
|
||||
|
||||
// 优惠类型
|
||||
dict["type"] = subscriptionOfferTypeToString(offer.type)
|
||||
|
||||
// 价格信息(确保是字符串类型)
|
||||
dict["displayPrice"] = String(describing: offer.displayPrice)
|
||||
dict["price"] = Double(String(format: "%.2f", NSDecimalNumber(decimal: offer.price).doubleValue)) ?? NSDecimalNumber(decimal: offer.price).doubleValue
|
||||
|
||||
// 支付模式
|
||||
dict["paymentMode"] = paymentModeToString(offer.paymentMode)
|
||||
|
||||
// 优惠周期
|
||||
dict["periodCount"] = offer.period.value
|
||||
dict["periodUnit"] = subscriptionPeriodUnitToString(offer.period.unit)
|
||||
|
||||
// 周期数量
|
||||
dict["offerPeriodCount"] = offer.periodCount
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
/// 日期转时间戳(毫秒)
|
||||
private static func dateToTimestamp(_ date: Date) -> Int64 {
|
||||
return Int64(date.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
|
||||
/// 订阅周期单位转字符串
|
||||
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"
|
||||
@unknown default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
/// 优惠类型转字符串
|
||||
private static func subscriptionOfferTypeToString(_ type: Product.SubscriptionOffer.OfferType) -> String {
|
||||
switch type {
|
||||
case .introductory:
|
||||
return "introductory"
|
||||
case .promotional:
|
||||
return "promotional"
|
||||
default:
|
||||
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
|
||||
if type == .winBack {
|
||||
return "winBack"
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
/// 支付模式转字符串
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
/// 过期原因转字符串
|
||||
private static func expirationReasonToString(_ reason: Product.SubscriptionInfo.RenewalInfo.ExpirationReason) -> String {
|
||||
// ExpirationReason 的具体枚举值可能因 iOS 版本而异
|
||||
// 使用 String(describing:) 作为后备方案
|
||||
let reasonString = String(describing: reason)
|
||||
// 移除命名空间前缀,只保留枚举值名称
|
||||
if let lastDot = reasonString.lastIndex(of: ".") {
|
||||
let value = String(reasonString[reasonString.index(after: lastDot)...])
|
||||
return value
|
||||
}
|
||||
return reasonString
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] = ""
|
||||
}
|
||||
|
||||
// 应用交易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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user