238 lines
8.1 KiB
Swift
238 lines
8.1 KiB
Swift
|
|
//
|
|||
|
|
// 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
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|