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 { guard !isPurchasing else { return } guard let product = pioneerProduct else { // Surface an actionable error so the UI can inform the user self.errorMessage = "Subscription product unavailable. Please try again later." return } isPurchasing = true defer { isPurchasing = false } do { let result = try await product.purchase() switch result { case .success(let verification): switch verification { case .unverified(_, let error): self.errorMessage = "Purchase unverified: \(error.localizedDescription)" case .verified(let transaction): // Update entitlement for the purchased product updateEntitlement(from: transaction) await transaction.finish() } case .userCancelled: break case .pending: break @unknown default: break } } catch { self.errorMessage = "Purchase failed: \(error.localizedDescription)" } } // 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 } } }