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 #if DEBUG @Published var debugOverrideEnabled = false #endif // MARK: - Constants static let premiumProductID = "com.portfoliojournal.premium" static let premiumPrice = "€4.69" // MARK: - Private Properties private var updateListenerTask: Task? private var cancellables = Set() private let sharedDefaults = UserDefaults(suiteName: AppConstants.appGroupIdentifier) // MARK: - Purchase State enum PurchaseState: Equatable { case idle case purchasing case purchased case failed(String) case restored } // MARK: - Initialization init() { #if DEBUG debugOverrideEnabled = UserDefaults.standard.bool(forKey: "debugPremiumOverride") #endif 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 #if DEBUG if debugOverrideEnabled { isPremium = true isFamilyShared = false return } #endif for await result in StoreKit.Transaction.currentEntitlements { if case .verified(let transaction) = result { if transaction.productID == Self.premiumProductID { isEntitled = true familyShared = transaction.ownershipType == .familyShared break } } } isPremium = isEntitled isFamilyShared = familyShared sharedDefaults?.set(isEntitled, forKey: "premiumUnlocked") // Update Core Data let context = CoreDataStack.shared.viewContext PremiumStatus.updateStatus( isPremium: isEntitled, productIdentifier: Self.premiumProductID, transactionId: nil, isFamilyShared: familyShared, in: context ) } #if DEBUG func setDebugPremiumOverride(_ enabled: Bool) { debugOverrideEnabled = enabled UserDefaults.standard.set(enabled, forKey: "debugPremiumOverride") if enabled { isPremium = true isFamilyShared = false sharedDefaults?.set(true, forKey: "premiumUnlocked") } else { Task { await updatePremiumStatus() } } } #endif // MARK: - Transaction Listener private func listenForTransactions() -> Task { return Task.detached { [weak self] in for await result in StoreKit.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)] = [ ("person.2", "Multiple Accounts", "Separate portfolios for business or family"), ("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") ] }