import Foundation import StoreKit @MainActor final class IAPManager: ObservableObject { @Published var isPurchasing: Bool = false @Published var pioneerProduct: Product? @Published var errorMessage: String? @Published var isSubscribed: Bool = false @Published var subscriptionExpiry: Date? = nil // Product IDs from App Store Connect private let productIDs: [String] = [ "MEMBERSHIP_PIONEER_MONTHLY" ] init() { Task { await observeTransactions() } } // Load products defined in App Store Connect func loadProducts() async { do { let products = try await Product.products(for: productIDs) // You can refine selection logic if you have multiple tiers self.pioneerProduct = products.first if products.isEmpty { // No products found is a common setup issue (App Store Connect, StoreKit config, or bundle ID) self.errorMessage = "No subscription products found. Please try again later." } } catch { self.errorMessage = "Failed to load products: \(error.localizedDescription)" } } // Trigger App Store purchase sheet func purchasePioneer() async throws -> String { guard !isPurchasing else { throw NSError(domain: "IAPError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Purchase already in progress"]) } guard let product = pioneerProduct else { throw NSError(domain: "IAPError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Subscription product unavailable"]) } isPurchasing = true defer { isPurchasing = false } do { let result = try await product.purchase() switch result { case .success(let verification): switch verification { case .unverified(_, let error): throw error case .verified(let transaction): print("🎉 订阅成功!", transaction) print("🔄 交易验证通过 - ID: \(transaction.id), 原始ID: \(transaction.originalID), 产品ID: \(transaction.productID)") updateEntitlement(from: transaction) let transactionID = String(transaction.id) print("📝 使用交易ID: \(transactionID)") await transaction.finish() return transactionID } case .userCancelled: throw NSError(domain: "IAPError", code: -3, userInfo: [NSLocalizedDescriptionKey: "Purchase was cancelled"]) case .pending: throw NSError(domain: "IAPError", code: -4, userInfo: [NSLocalizedDescriptionKey: "Purchase is pending approval"]) @unknown default: throw NSError(domain: "IAPError", code: -5, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"]) } } catch { self.errorMessage = "Purchase failed: \(error.localizedDescription)" throw error } } // Restore purchases (sync entitlements) func restorePurchases() async { do { try await AppStore.sync() } catch { self.errorMessage = "Restore failed: \(error.localizedDescription)" } } // Observe transaction updates for entitlement changes private func observeTransactions() async { for await result in Transaction.updates { do { let transaction: Transaction = try checkVerified(result) // Update entitlement state for transaction.productID updateEntitlement(from: transaction) await transaction.finish() } catch { // Ignore unverified transactions } } } // Check current entitlements (useful on launch) func refreshEntitlements() async { for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result, productIDs.contains(transaction.productID) { updateEntitlement(from: transaction) } } } // Helper: verify private func checkVerified(_ result: VerificationResult) throws -> T { switch result { case .unverified(_, let error): throw error case .verified(let safe): return safe } } private func updateEntitlement(from transaction: Transaction) { guard productIDs.contains(transaction.productID) else { return } // For auto-renewable subs, use expirationDate and revocationDate if transaction.revocationDate == nil { self.isSubscribed = true self.subscriptionExpiry = transaction.expirationDate } else { self.isSubscribed = false self.subscriptionExpiry = nil } } }