Files
keyboard/keyBoard/Class/Pay/StoreKit2Manager/Internal/StoreKitService.swift
2025-12-16 15:47:12 +08:00

1308 lines
58 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 payload = verification.jwsRepresentation
let transaction = try verifyPurchase(verification)
await printProductDetails(product)
//
await printTransactionDetails(transaction)
await MainActor.run {
self.delegate?.service(self, didCompletePurchaseFor: transaction.productID, payload: payload)
}
//
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 {
//
if self.config.showLog {
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)")
}
}