import Foundation import StoreKit import Combine @MainActor class IAPService: ObservableObject { // MARK: - Published Properties @Published private(set) var isPremium = false @Published private(set) var products: [Product] = [] @Published private(set) var purchaseState: PurchaseState = .idle @Published private(set) var isFamilyShared = false // MARK: - Constants static let premiumProductID = "com.investmenttracker.premium" static let premiumPrice = "€4.69" // MARK: - Private Properties private var updateListenerTask: Task? private var cancellables = Set() // MARK: - Purchase State enum PurchaseState: Equatable { case idle case purchasing case purchased case failed(String) case restored } // MARK: - Initialization init() { updateListenerTask = listenForTransactions() Task { await loadProducts() await updatePremiumStatus() } } deinit { updateListenerTask?.cancel() } // MARK: - Load Products func loadProducts() async { do { products = try await Product.products(for: [Self.premiumProductID]) print("Loaded \(products.count) products") } catch { print("Failed to load products: \(error)") } } // MARK: - Purchase func purchase() async throws { guard let product = products.first else { throw IAPError.productNotFound } purchaseState = .purchasing do { let result = try await product.purchase() switch result { case .success(let verification): let transaction = try checkVerified(verification) // Check if family shared isFamilyShared = transaction.ownershipType == .familyShared await transaction.finish() await updatePremiumStatus() purchaseState = .purchased // Track analytics FirebaseService.shared.logPurchaseSuccess( productId: product.id, price: product.price, isFamilyShared: isFamilyShared ) case .userCancelled: purchaseState = .idle case .pending: purchaseState = .idle @unknown default: purchaseState = .idle } } catch { purchaseState = .failed(error.localizedDescription) FirebaseService.shared.logPurchaseFailure( productId: product.id, error: error.localizedDescription ) throw error } } // MARK: - Restore Purchases func restorePurchases() async { purchaseState = .purchasing do { try await AppStore.sync() await updatePremiumStatus() if isPremium { purchaseState = .restored } else { purchaseState = .failed("No purchases to restore") } } catch { purchaseState = .failed(error.localizedDescription) } } // MARK: - Update Premium Status func updatePremiumStatus() async { var isEntitled = false var familyShared = false for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { if transaction.productID == Self.premiumProductID { isEntitled = true familyShared = transaction.ownershipType == .familyShared break } } } isPremium = isEntitled isFamilyShared = familyShared // Update Core Data let context = CoreDataStack.shared.viewContext PremiumStatus.updateStatus( isPremium: isEntitled, productIdentifier: Self.premiumProductID, transactionId: nil, isFamilyShared: familyShared, in: context ) } // MARK: - Transaction Listener private func listenForTransactions() -> Task { return Task.detached { [weak self] in for await result in Transaction.updates { if case .verified(let transaction) = result { await transaction.finish() await self?.updatePremiumStatus() } } } } // MARK: - Verification private func checkVerified(_ result: VerificationResult) throws -> T { switch result { case .unverified(_, let error): throw IAPError.verificationFailed(error.localizedDescription) case .verified(let safe): return safe } } // MARK: - Product Info var premiumProduct: Product? { products.first { $0.id == Self.premiumProductID } } var formattedPrice: String { premiumProduct?.displayPrice ?? Self.premiumPrice } } // MARK: - IAP Error enum IAPError: LocalizedError { case productNotFound case verificationFailed(String) case purchaseFailed(String) var errorDescription: String? { switch self { case .productNotFound: return "Product not found. Please try again later." case .verificationFailed(let message): return "Verification failed: \(message)" case .purchaseFailed(let message): return "Purchase failed: \(message)" } } } // MARK: - Premium Features extension IAPService { static let premiumFeatures: [(icon: String, title: String, description: String)] = [ ("infinity", "Unlimited Sources", "Track as many investments as you want"), ("clock.arrow.circlepath", "Full History", "Access your complete investment history"), ("chart.bar.xaxis", "Advanced Charts", "5 types of detailed analytics charts"), ("wand.and.stars", "Predictions", "AI-powered 12-month forecasts"), ("square.and.arrow.up", "Export Data", "Export to CSV and JSON formats"), ("xmark.circle", "No Ads", "Ad-free experience forever"), ("person.2", "Family Sharing", "Share with up to 5 family members") ] }