Files
keyboard/keyBoard/Class/Pay/StoreKit2Manager/Internal/StoreKitService.swift

1301 lines
57 KiB
Swift
Raw Normal View History

2025-12-16 13:10:50 +08:00
//
// StoreKitService.swift
// StoreKit2Manager
//
// Created by xiaopin on 2025/12/6.
//
import Foundation
import StoreKit
import Combine
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
/// StoreKit
/// StoreKit API
internal class StoreKitService: ObservableObject {
private let config: StoreKitConfig
weak var delegate: StoreKitServiceDelegate?
///
@Published private(set) var allProducts: [Product] = []
///
@Published private(set) var purchasedTransactions: [Transaction] = []
///
@Published private(set) var latestTransactions: [Transaction] = []
//
private var transactionListener: Task<Void, Error>?
private var subscriberTasks: [Task<Void, Never>] = []
private var cancellables = Set<AnyCancellable>()
//
private var isPurchasing = false
private let purchasingQueue = DispatchQueue(label: "com.storekit.purchasing")
// MARK: -
/// ID ->
///
///
/// - RenewalState//
/// -
/// -
private var lastSubscriptionStatus: [String: Product.SubscriptionInfo.RenewalState] = [:]
/// ID ->
///
///
/// - RenewalInfo willAutoRenewexpirationDate
/// - willAutoRenew true false
/// -
private var lastRenewalInfo: [String: Product.SubscriptionInfo.RenewalInfo] = [:]
/// 30
///
///
/// -
/// -
/// - 寿
private let subscriptionCheckInterval: TimeInterval = 30
//
private var currentState: StoreKitState = .idle {
didSet {
// 线 delegate
let state = currentState
Task { @MainActor [weak self] in
guard let self = self else { return }
self.notifyStateChanged(state)
}
}
}
init(config: StoreKitConfig, delegate: StoreKitServiceDelegate) {
self.config = config
self.delegate = delegate
setupSubscribers()
}
deinit {
stop()
}
// MARK: -
///
func start() {
guard transactionListener == nil else { return }
transactionListener = transactionStatusStream()
//
startSubscriptionStatusListener()
Task {
await clearUnfinishedTransactions()
await loadProducts()
await loadPurchasedTransactions()
//
await checkSubscriptionStatus()
}
}
///
func stop() {
transactionListener?.cancel()
transactionListener = nil
subscriberTasks.forEach { $0.cancel() }
subscriberTasks.removeAll()
cancellables.removeAll()
}
///
/// - Returns: nil
@MainActor
func loadProducts() async -> [Product]? {
currentState = .loadingProducts
do {
let storeProducts = try await Product.products(for: config.productIds)
var products: [Product] = []
for product in storeProducts {
products.append(product)
}
//
if config.autoSortProducts {
products = sortByPrice(products)
}
self.allProducts = products
return products
} catch {
currentState = .error(error)
return nil
}
}
///
@MainActor
func loadPurchasedTransactions() async {
currentState = .loadingPurchases
// 使 TaskGroup
var latestTransactions: [Transaction] = []
await withTaskGroup(of: Transaction?.self) { group in
// ID
for productId in config.productIds {
group.addTask {
if let latestTransaction = await Transaction.latest(for: productId) {
switch latestTransaction {
case .verified(let transaction):
return transaction
case .unverified:
return nil
}
}
return nil
}
}
//
for await transaction in group {
if let transaction = transaction {
latestTransactions.append(transaction)
}
}
}
self.latestTransactions = latestTransactions
// purchasedTransactions
var purchasedTransactions: [Transaction] = []
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
purchasedTransactions.append(transaction)
}
}
self.purchasedTransactions = purchasedTransactions
currentState = .purchasesLoaded
}
///
@MainActor
func clearUnfinishedTransactions() async {
for await result in Transaction.unfinished {
do {
// 使
let transaction = try verifyPurchase(result)
//
await transaction.finish()
print("未完成交易,完成交易处理: \(transaction.productID)")
} catch {
//
if case .unverified(let transaction, _) = result {
print("未完成交易交易验证失败产品ID: \(transaction.productID) 错误\(error.localizedDescription)")
//
currentState = .error(StoreKitError.verificationFailed)
}
// finish()
}
}
}
///
func purchase(_ product: Product) async throws {
//
return try await withCheckedThrowingContinuation { continuation in
purchasingQueue.async { [weak self] in
guard let self = self else {
continuation.resume(throwing: StoreKitError.unknownError)
return
}
guard !self.isPurchasing else {
continuation.resume(throwing: StoreKitError.purchaseInProgress)
return
}
self.isPurchasing = true
Task {
defer {
self.purchasingQueue.async {
self.isPurchasing = false
}
}
await self.performPurchase(product, continuation: continuation)
}
}
}
}
///
private func performPurchase(_ product: Product, continuation: CheckedContinuation<Void, Error>) async {
await MainActor.run {
currentState = .purchasing(product.id)
}
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
do {
let transaction = try verifyPurchase(verification)
await printProductDetails(product)
//
await printTransactionDetails(transaction)
//
await transaction.finish()
//
if product.type != .consumable {
await loadPurchasedTransactions()
}
await MainActor.run {
currentState = .purchaseSuccess(transaction.productID)
}
continuation.resume()
} catch {
await MainActor.run {
currentState = .purchaseFailed(product.id, error)
}
continuation.resume(throwing: error)
}
case .pending:
await MainActor.run {
currentState = .purchasePending(product.id)
}
continuation.resume()
case .userCancelled:
await MainActor.run {
currentState = .purchaseCancelled(product.id)
}
continuation.resume()
@unknown default:
let error = StoreKitError.unknownError
await MainActor.run {
currentState = .purchaseFailed(product.id, error)
}
continuation.resume(throwing: error)
}
} catch {
await MainActor.run {
currentState = .purchaseFailed(product.id, error)
}
continuation.resume(throwing: error)
}
}
///
@MainActor
func restorePurchases() async throws {
currentState = .restoringPurchases
do {
///
/// StoreKit 使使
/// -
/// - StoreKit App Store
try await AppStore.sync()
await loadPurchasedTransactions()
currentState = .restorePurchasesSuccess
} catch {
currentState = .restorePurchasesFailed(error)
throw StoreKitError.restorePurchasesFailed(error)
}
}
///
@MainActor
func refreshPurchasesSync() async {
// App Store
do {
try await AppStore.sync()
} catch {
print("同步 App Store 状态失败: \(error)")
}
//
await loadPurchasedTransactions()
}
/// ID
func getTransactionHistory(for productId: String? = nil) async -> [TransactionHistory] {
var histories: [TransactionHistory] = []
//
for await verificationResult in Transaction.all {
do {
let transaction = try verifyPurchase(verificationResult)
// ID
if let productId = productId, transaction.productID != productId {
continue
}
//
let product = allProducts.first(where: { $0.id == transaction.productID })
let history = TransactionHistory.from(transaction, product: product)
histories.append(history)
// 退
//
//
if transaction.revocationDate != nil {
await MainActor.run {
if transaction.productType == .autoRenewable {
// /退
// offer
// 退isFreeTrialCancelled true
let isFreeTrialCancelled = self.isFreeTrialTransaction(transaction)
// 使
// isFreeTrialCancelled
currentState = .subscriptionCancelled(transaction.productID, isFreeTrialCancelled: isFreeTrialCancelled)
} else {
// 退
currentState = .purchaseRefunded(transaction.productID)
}
}
}
} catch {
continue
}
}
//
return histories.sorted(by: { $0.purchaseDate > $1.purchaseDate })
}
///
func getConsumablePurchaseHistory(for productId: String) async -> [TransactionHistory] {
let allHistory = await getTransactionHistory(for: productId)
return allHistory.filter { history in
history.product?.type == .consumable
}
}
// MARK: -
///
private func setupSubscribers() {
//
$allProducts
.receive(on: DispatchQueue.main)
.sink { [weak self] products in
guard let self = self else { return }
Task { @MainActor in
self.notifyProductsLoaded(products)
}
}
.store(in: &cancellables)
//
$purchasedTransactions
.receive(on: DispatchQueue.main)
.sink { [weak self] transactions in
guard let self = self else { return }
Task { @MainActor in
self.notifyPurchasedTransactionsUpdated(transactions, self.latestTransactions)
}
}
.store(in: &cancellables)
}
///
private func verifyPurchase<T>(_ verificationResult: VerificationResult<T>) throws -> T {
switch verificationResult {
case .unverified(_, let error):
throw StoreKitError.verificationFailed
case .verified(let result):
return result
}
}
///
///
///
/// - offer使
/// - introductoryfreeTrial
///
/// 使
/// 1.
/// 2. /退
/// 3.
///
/// - Parameter transaction:
/// - Returns: 使 true false
///
/// - Note:
/// - 使
/// - 使使 true
/// - ""
private func isFreeTrialTransaction(_ transaction: Transaction) -> Bool {
// iOS 17.2+ 使 offer
if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *) {
if let offer = transaction.offer {
//
//
// 1. introductory
// 2. freeTrial
// 使
if offer.type == .introductory,
offer.paymentMode == .freeTrial {
return true
}
}
} else {
// iOS 15.0 - iOS 17.1 使
if let offerType = transaction.offerType,
let paymentMode = transaction.offerPaymentModeStringRepresentation {
//
// paymentMode "freeTrial"
if offerType == .introductory,
paymentMode == "freeTrial" {
return true
}
}
}
// false
//
// 1. 使
// 2. 使
// 3. 使
return false
}
///
private func transactionStatusStream() -> Task<Void, Error> {
return Task.detached { [weak self] in
guard let self = self else { return }
for await result in Transaction.updates {
do {
let transaction = try self.verifyPurchase(result)
await printTransactionDetails(transaction)
// 退
// revocationDate /退cancellation
// - revocation退 Transaction.updates
// - cancellation subscription.status
if transaction.revocationDate != nil {
await MainActor.run {
if transaction.productType == .autoRenewable {
// /退
// offer
// 退isFreeTrialCancelled true
let isFreeTrialCancelled = self.isFreeTrialTransaction(transaction)
// 使
// isFreeTrialCancelled
print("🔔 检测到订阅取消: \(transaction.productID), isFreeTrialCancelled: \(isFreeTrialCancelled)")
self.currentState = .subscriptionCancelled(transaction.productID, isFreeTrialCancelled: isFreeTrialCancelled)
} else {
// 退
// 退
print("🔔 检测到订阅退款: \(transaction.productID)")
self.currentState = .purchaseRefunded(transaction.productID)
}
}
}
await self.loadPurchasedTransactions()
await transaction.finish()
} catch {
print("交易处理失败: \(error)")
}
}
}
}
///
private func sortByPrice(_ products: [Product]) -> [Product] {
products.sorted(by: { $0.price < $1.price })
}
// MARK: -
///
///
///
/// -
/// - 30 subscriptionCheckInterval
/// -
/// 1. willAutoRenew true false
/// 2. RenewalState //
/// 3. revoked
/// -
/// -
private func startSubscriptionStatusListener() {
// 使 weak self
let task = Task { [weak self] in
guard let self = self else { return }
//
while !Task.isCancelled {
//
let now = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone.current
print("当前订阅检测时间: \(formatter.string(from: now))")
await self.checkSubscriptionStatus()
// 30
// 使 try?
try? await Task.sleep(nanoseconds: UInt64(self.subscriptionCheckInterval * 1_000_000_000))
}
}
// 便
subscriberTasks.append(task)
}
///
///
/// 1.
/// 2. willAutoRenew
/// 3. RenewalState
/// 4.
/// 5. subscriptionCancelledsubscriptionStatusChanged
@MainActor
private func checkSubscriptionStatus() async {
//
let purchasedSubscriptions = allProducts.filter { product in
product.type == .autoRenewable &&
purchasedTransactions.contains(where: { $0.productID == product.id })
}
//
guard !purchasedSubscriptions.isEmpty else { return }
// 使 TaskGroup
// (ID, , , , )
await withTaskGroup(of: (String, Product.SubscriptionInfo.RenewalState?, Product.SubscriptionInfo.RenewalInfo?, Date?, Bool?).self) { group in
//
for product in purchasedSubscriptions {
group.addTask { [weak self] in
guard let self = self else { return (product.id, nil, nil, nil, nil) }
guard let subscription = product.subscription else { return (product.id, nil, nil, nil, nil) }
do {
//
let statuses = try await subscription.status
guard let currentStatus = statuses.first else { return (product.id, nil, nil, nil, nil) }
let currentState = currentStatus.state
var renewalInfo: Product.SubscriptionInfo.RenewalInfo?
var expirationDate: Date?
var isFreeTrial: Bool? = nil
// willAutoRenewexpirationDate
if case .verified(let info) = currentStatus.renewalInfo {
renewalInfo = info
}
// Transaction
// subscription.status transaction
//
if case .verified(let transaction) = currentStatus.transaction {
expirationDate = transaction.expirationDate
// ========== ==========
//
// 1. offer
// 2. introductoryfreeTrial
// 3. 使
//
//
// - isFreeTrial true
// - isFreeTrial false
// -
// iOS 17.2+ 使 offer
if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *) {
if let offer = transaction.offer {
//
//
if offer.type == .introductory,
offer.paymentMode == .freeTrial {
isFreeTrial = true
} else {
//
isFreeTrial = false
}
} else {
//
isFreeTrial = false
}
} else {
// iOS 15.0 - iOS 17.1 使
if let offerType = transaction.offerType,
let paymentMode = transaction.offerPaymentModeStringRepresentation {
//
if offerType == .introductory,
paymentMode == "freeTrial" {
isFreeTrial = true
} else {
//
isFreeTrial = false
}
} else {
//
isFreeTrial = false
}
}
} else {
//
isFreeTrial = false
}
return (product.id, currentState, renewalInfo, expirationDate, isFreeTrial)
} catch {
print("获取订阅状态失败: \(product.id), 错误: \(error)")
return (product.id, nil, nil, nil, nil)
}
}
}
//
for await (productId, currentRenewalState, renewalInfo, expirationDate, isFreeTrial) in group {
//
guard let currentRenewalState = currentRenewalState else { continue }
//
let lastInfo = self.lastRenewalInfo[productId]
let lastState = self.lastSubscriptionStatus[productId]
// ========== ==========
// willAutoRenew true false
//
// 使
if let lastInfo = lastInfo,
let currentInfo = renewalInfo {
// willAutoRenew true false
if lastInfo.willAutoRenew == true && currentInfo.willAutoRenew == false {
// ========== ==========
//
// 1. isFreeTrial true 使
// 2. isFreeTrial true
// 3. isFreeTrial false
//
// 使
// - isFreeTrialCancelled = true
// * ""
// *
// *
// - isFreeTrialCancelled = false
// * "XX"
// *
// *
let isFreeTrialCancelled = isFreeTrial ?? false
//
if isFreeTrialCancelled {
print("🔔 检测到订阅取消(免费试用期): \(productId)")
print(" 说明:用户在免费试用期内取消了订阅,订阅将在试用期结束时失效")
} else {
print("🔔 检测到订阅取消(付费订阅期): \(productId)")
print(" 说明:用户在付费订阅期内取消了订阅,订阅将在当前周期结束时失效")
}
//
//
self.currentState = .subscriptionCancelled(productId, isFreeTrialCancelled: isFreeTrialCancelled)
//
if let expirationDate = expirationDate {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
if isFreeTrialCancelled {
print(" 免费试用将在 \(formatter.string(from: expirationDate)) 过期")
} else {
print(" 订阅将在 \(formatter.string(from: expirationDate)) 过期")
}
}
}
}
// ========== ==========
//
// -> ->
if let lastState = lastState, lastState != currentRenewalState {
//
switch currentRenewalState {
case .subscribed:
//
print("📱 订阅状态变化: \(productId) -> 已订阅")
// subscribed
case .expired:
// 使
print("⏰ 订阅状态变化: \(productId) -> 已过期")
//
case .inGracePeriod:
//
print("⚠️ 订阅状态变化: \(productId) -> 宽限期")
//
case .inBillingRetryPeriod:
//
print("🔄 订阅状态变化: \(productId) -> 计费重试期")
//
case .revoked:
// 退
print("❌ 订阅状态变化: \(productId) -> 已撤销")
self.currentState = .purchaseRevoked(productId)
default:
print("❓ 订阅状态变化: \(productId) -> 未知状态: \(currentRenewalState)")
}
}
// ========== ==========
// willAutoRenew
if let renewalInfo = renewalInfo {
self.lastRenewalInfo[productId] = renewalInfo
}
// RenewalState
self.lastSubscriptionStatus[productId] = currentRenewalState
}
}
}
/// 使
///
/// 使
/// -
/// -
/// -
/// - /
/// -
///
///
/// -
/// -
/// -
@MainActor
func checkSubscriptionStatusManually() async {
await checkSubscriptionStatus()
}
}
//MARK:
extension StoreKitService{
/// 使 URL
@MainActor
func openSubscriptionManagement() {
guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else { return }
#if os(iOS)
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
#elseif os(macOS)
NSWorkspace.shared.open(url)
#endif
}
/// iOS 15.0+ / macOS 12.0+
/// - Returns: false
@MainActor
func showManageSubscriptionsSheet() async -> Bool {
#if os(iOS)
if #available(iOS 15.0, *) {
do {
// windowScene
let windowScene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first
if let windowScene = windowScene {
try await AppStore.showManageSubscriptions(in: windowScene)
await loadPurchasedTransactions()
return true
} else {
// windowScene退 URL
openSubscriptionManagement()
return false
}
} catch {
print("显示订阅管理界面失败: \(error)")
// 退 URL
openSubscriptionManagement()
return false
}
} else {
// iOS 15.0 使 URL
openSubscriptionManagement()
return false
}
#elseif os(macOS)
if #available(macOS 12.0, *) {
do {
try await AppStore.showManageSubscriptions()
//
await loadPurchasedTransactions()
return true
} catch {
print("显示订阅管理界面失败: \(error)")
openSubscriptionManagement()
return false
}
} else {
openSubscriptionManagement()
return false
}
#else
openSubscriptionManagement()
return false
#endif
}
/// iOS 16.0+
/// - Throws: StoreKitError
/// - Note: Transaction.updates
@MainActor
@available(iOS 16.0, visionOS 1.0, *)
@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
func presentOfferCodeRedeemSheet() async throws {
#if os(iOS)
// windowScene
let windowScene = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first
guard let windowScene = windowScene else {
throw StoreKitError.unknownError
}
do {
try await AppStore.presentOfferCodeRedeemSheet(in: windowScene)
// Transaction.updates
//
await loadPurchasedTransactions()
} catch {
throw StoreKitError.purchaseFailed(error)
}
#else
throw StoreKitError.unknownError
#endif
}
///
/// - Note: iOS 15.0+ iOS 16.0+
/// - iOS 15.0: 使 SKStoreReviewController.requestReview() (StoreKit 1)
/// - iOS 16.0+: 使 AppStore.requestReview(in:) (StoreKit 2)
@MainActor
func requestReview() {
#if os(iOS)
if #available(iOS 16.0, *) {
// iOS 16.0+ 使 StoreKit 2 API
if let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first {
AppStore.requestReview(in: windowScene)
}
} else {
// iOS 15.0 ( iOS 10.3-15.x) 使 StoreKit 1 API
// iOS 15 StoreKit 2 AppStore.requestReview iOS 16+
// 退 StoreKit 1 SKStoreReviewController
SKStoreReviewController.requestReview()
}
#elseif os(macOS)
if #available(macOS 13.0, *) {
// macOS 13.0+ 使 StoreKit 2 API
if let windowScene = NSApplication.shared.windows.first?.windowScene {
AppStore.requestReview(in: windowScene)
}
} else if #available(macOS 10.14, *) {
// macOS 12.0+ ( macOS 10.14-12.x) 使 StoreKit 1 API
SKStoreReviewController.requestReview()
}
#endif
}
}
//MARK:
extension StoreKitService{
/// 线
@MainActor
private func notifyProductsLoaded(_ products: [Product]) {
delegate?.service(self, didLoadProducts: products)
}
/// 线
@MainActor
private func notifyPurchasedTransactionsUpdated(_ efficient: [Transaction], _ latests: [Transaction]) {
delegate?.service(self, didUpdatePurchasedTransactions: efficient, latests: latests)
}
/// 线
@MainActor
private func notifyStateChanged(_ state: StoreKitState) {
delegate?.service(self, didUpdateState: state)
}
}
//MARK:
extension StoreKitService{
private func printProductDetails(_ product:Product) async{
//
let beijingTimeZone = TimeZone(secondsFromGMT: 8 * 3600) ?? .current
let formatter = DateFormatter()
formatter.timeZone = beijingTimeZone
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print("════════════════════════════════════════")
print("✅ 购买成功 - 交易详细信息")
print("════════════════════════════════════════")
print("📦 产品信息:")
print(" - 产品ID: \(product.id)")
print(" - 产品类型: \(product.type)")
print(" - 产品名称: \(product.displayName)")
print(" - 产品描述: \(product.description)")
print(" - 产品价格: \(product.displayPrice)")
print(" - 价格数值: \(product.price)")
print(" - 家庭共享: \(product.isFamilyShareable)")
//print(" - JSON: \(String.init(data: product.jsonRepresentation, encoding: .utf8))")
//
if let subscription = product.subscription {
print("📱 订阅信息:")
print(" - 订阅组ID: \(subscription.subscriptionGroupID)")
//
let period = subscription.subscriptionPeriod
let periodName: String
switch period.unit {
case .day:
periodName = "\(period.value)"
case .week:
periodName = "\(period.value)"
case .month:
periodName = "\(period.value)"
case .year:
periodName = "\(period.value)"
@unknown default:
periodName = "未知"
}
print(" - 订阅周期: \(periodName)")
// 使
let isEligibleForIntroOffer = await subscription.isEligibleForIntroOffer
print(" - 是否有资格使用介绍性优惠: \(isEligibleForIntroOffer ? "" : "")")
//
if let introductoryOffer = subscription.introductoryOffer {
print(" - 介绍性优惠: 有")
printOfferDetails(introductoryOffer, indent: " ")
} else {
print(" - 介绍性优惠: 无")
}
//
if !subscription.promotionalOffers.isEmpty {
print(" - 促销优惠: 有 (\(subscription.promotionalOffers.count) 个)")
for (index, promotionalOffer) in subscription.promotionalOffers.enumerated() {
print(" [促销优惠 \(index + 1)]")
printOfferDetails(promotionalOffer, indent: " ")
}
} else {
print(" - 促销优惠: 无")
}
// iOS 18.0+
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
if !subscription.winBackOffers.isEmpty {
print(" - 赢回优惠: 有 (\(subscription.winBackOffers.count) 个)")
for (index, winBackOffer) in subscription.winBackOffers.enumerated() {
print(" [赢回优惠 \(index + 1)]")
printOfferDetails(winBackOffer, indent: " ")
}
} else {
print(" - 赢回优惠: 无")
}
}
}
let productJSON = ProductConverter.toDictionary(product)
print(" - JSON表示: \(productJSON)")
}
///
/// - Parameters:
/// - offer:
/// - indent:
private func printOfferDetails(_ offer: Product.SubscriptionOffer, indent: String) {
// ID nil nil
if let offerID = offer.id {
print("\(indent)* 优惠ID: \(offerID)")
} else {
print("\(indent)* 优惠ID: 无(介绍性优惠)")
}
//
let typeName: String
if offer.type == .introductory {
typeName = "介绍性优惠"
} else if offer.type == .promotional {
typeName = "促销优惠"
} else {
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
if offer.type == .winBack {
typeName = "赢回优惠"
} else {
typeName = "未知类型(\(offer.type.rawValue))"
}
} else {
typeName = "未知类型(\(offer.type.rawValue))"
}
}
print("\(indent)* 优惠类型: \(typeName)")
//
print("\(indent)* 显示价格: \(offer.displayPrice)")
print("\(indent)* 价格数值: \(offer.price)")
//
let paymentModeName: String
switch offer.paymentMode {
case .freeTrial:
paymentModeName = "免费试用"
case .payAsYouGo:
paymentModeName = "按需付费"
case .payUpFront:
paymentModeName = "预付"
default:
paymentModeName = "未知模式(\(offer.paymentMode.rawValue))"
}
print("\(indent)* 支付模式: \(paymentModeName)")
//
let offerPeriod = offer.period
let offerPeriodName: String
switch offerPeriod.unit {
case .day:
offerPeriodName = "\(offerPeriod.value)"
case .week:
offerPeriodName = "\(offerPeriod.value)"
case .month:
offerPeriodName = "\(offerPeriod.value)"
case .year:
offerPeriodName = "\(offerPeriod.value)"
@unknown default:
offerPeriodName = "未知"
}
print("\(indent)* 优惠周期: \(offerPeriodName)")
// 1 .payAsYouGo
print("\(indent)* 周期数量: \(offer.periodCount)")
}
///
private func printTransactionDetails(_ transaction: Transaction) async {
//
let beijingTimeZone = TimeZone(secondsFromGMT: 8 * 3600) ?? .current
let formatter = DateFormatter()
formatter.timeZone = beijingTimeZone
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print("")
print("💳 交易信息:")
print(" - 交易ID: \(transaction.id)") //
print(" - 产品ID: \(transaction.productID)") // ID
print(" - 产品类型: \(transaction.productType)") // ///
print(" - 购买日期: \(formatter.string(from: transaction.purchaseDate))") // UTC
print(" - 所有权类型: \(transaction.ownershipType)") // purchased/familyShared
print(" - 原始交易ID: \(transaction.originalID)") // ID
print(" - 原始购买日期: \(formatter.string(from: transaction.originalPurchaseDate))") //
//
if let expirationDate = transaction.expirationDate {
let dateStr = formatter.string(from: expirationDate)
print(" - 过期日期: \(dateStr)") //
} else {
print(" - 过期日期: 无")
}
// 退/
if let revocationDate = transaction.revocationDate {
let dateStr = formatter.string(from: revocationDate)
print(" - 撤销日期: \(dateStr)") // 退
} else {
print(" - 撤销日期: 无")
}
//
if let revocationReason = transaction.revocationReason {
print(" - 撤销原因: \(revocationReason)") // 退/
}
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *){
// purchased/upgraded/renewed
print(" - 购买理由: \(transaction.reason)")
}else{
print(" - 购买理由: 无")
}
print(" - 是否升级: \(transaction.isUpgraded)") //
//
print(" - 购买数量: \(transaction.purchasedQuantity)") //
//
if let price = transaction.price {
print(" - 交易价格: \(price)") //
}
//
if #available(iOS 16.0, *) {
if let currency = transaction.currency {
print(" - 货币代码: \(currency)") // CNYUSD
}
} else {
// Fallback on earlier versions
}
if #available(iOS 16.0, *) {
print(" - 环境: \(transaction.environment.rawValue)")
} else {
// Fallback on earlier versions
} // sandbox/production
print(" - 应用交易ID: \(transaction.appTransactionID)") // ID
print(" - 应用Bundle ID: \(transaction.appBundleID )") // Bundle
// Token
if let appAccountToken = transaction.appAccountToken {
print(" - 应用账户Token: \(appAccountToken)") // Token
}
// ID
if let subscriptionGroupID = transaction.subscriptionGroupID {
print(" - 订阅组ID: \(subscriptionGroupID)") // ID
}
//
//if let subscriptionStatus = await transaction.subscriptionStatus {
// print(" - : \(subscriptionStatus)") //
//}
print(" - 签名日期: \(formatter.string(from: transaction.signedDate))") //
if #available(iOS 17.0, *) {
print(" - 商店区域: \(transaction.storefront)")
} else {
// Fallback on earlier versions
} //
// WebID
if let webOrderLineItemID = transaction.webOrderLineItemID {
print(" - Web订单行项目ID: \(webOrderLineItemID)") // WebID
}
print(" - 设备验证: \(transaction.deviceVerification)") //
print(" - 设备验证Nonce: \(transaction.deviceVerificationNonce)") // Nonce
//
if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, *) {
// iOS 17.2+ 使 offer
if let offer = transaction.offer {
print(" - 优惠信息:")
print(" * 优惠类型: \(offer.type)")
if let offerID = offer.id {
print(" * 优惠ID: \(offerID)")
}
print(" * 支付模式: \(String(describing: offer.paymentMode?.rawValue))")
if #available(iOS 18.4, *) {
if let period = offer.period {
print(" * 优惠周期: \(period)")
}
} else {
// Fallback on earlier versions
}
}
} else if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
// iOS 15.0 - iOS 17.1 使
if let offerType = transaction.offerType {
print(" - 优惠信息:")
print(" * 优惠类型: \(offerType)")
if let offerID = transaction.offerID {
print(" * 优惠ID: \(offerID)")
}
if let paymentMode = transaction.offerPaymentModeStringRepresentation {
print(" * 支付模式: \(paymentMode)")
}
if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) {
// iOS 18.4+ offerPeriodStringRepresentation
// iOS 18.4+ 使 offer.period
} else {
// iOS 15.0 - iOS 18.3 使 offerPeriodStringRepresentation
if let period = transaction.offerPeriodStringRepresentation {
print(" * 优惠周期: \(period)")
}
}
}
} else {
// iOS 15.0
//
}
//
if #available(iOS 18.4, *) {
if let advancedCommerceInfo = transaction.advancedCommerceInfo {
print(" - 高级商务信息: \(advancedCommerceInfo)") //
}
} else {
// Fallback on earlier versions
}
// JSON
//if let jsonRepresentation = transaction.jsonRepresentation {
// print(" - JSON (200): \(String(jsonRepresentation.prefix(200)))...") // JSON
//}
// Debug
print(" - Debug描述: \(transaction.debugDescription)") //
print("")
print("════════════════════════════════════════")
print("")
let transactionJSON = TransactionConverter.toDictionary(transaction)
print(" - JSON表示: \(transactionJSON)")
}
}