import SwiftUI import SwiftData import StoreKit struct ContentView: View { @Environment(\.modelContext) private var context @Environment(\.scenePhase) private var scenePhase @Query private var allSettings: [AppSettings] @State private var isReady = false @State private var hasCompletedColdLaunch = false private let premiumSyncService = PremiumSyncService() private var settings: AppSettings? { allSettings.first } var body: some View { Group { if !isReady { ZStack { Color.mealMoodBackground.ignoresSafeArea() AppIconPlaceholder(size: 120) } } else if let settings = settings, settings.onboardingCompleted { HomeView() } else { OnboardingView { // Onboarding completed } } } .environment(\.locale, Locale(identifier: settings?.languageEnum.localeIdentifier ?? Locale.current.identifier)) // MARK: Transaction.updates listener (StoreKit 2 recommended pattern) // Fires for new purchases, renewals and revocations while the app is running. // This is the most reliable way to catch a purchase even if the app was // interrupted during the payment flow. .task { for await result in Transaction.updates { guard case .verified(let transaction) = result else { continue } guard let s = settings else { await transaction.finish() continue } let isActive = transaction.revocationDate == nil && (transaction.expirationDate.map { $0 > Date() } ?? true) if isActive && !s.isPremium { s.isPremium = true try? context.save() } else if transaction.revocationDate != nil && s.isPremium { // Explicitly revoked subscription → downgrade s.isPremium = false try? context.save() } await transaction.finish() } } .onAppear { initializeAppIfNeeded() Task { if settings?.iCloudSyncEnabledResolved ?? true { await ICloudSyncService.shared.pullRemoteIfNeeded(context: context) } await syncPremiumStatus(isColdLaunch: true) hasCompletedColdLaunch = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation(.easeInOut) { isReady = true } } } .onChange(of: scenePhase) { _, phase in Task { switch phase { case .active: // Only run foreground sync after cold launch is complete. // The first activation is covered by .onAppear above. if hasCompletedColdLaunch { await syncPremiumStatus(isColdLaunch: false) } case .inactive, .background: if settings?.iCloudSyncEnabledResolved ?? true { await ICloudSyncService.shared.pushLocalSnapshot(context: context) } @unknown default: break } } } } // MARK: - Private private func initializeAppIfNeeded() { // CloudKit mirroring can duplicate singletons/default data created on // other devices — collapse them before anything reads the store. if CloudSyncRuntime.isCloudKitActive { DeduplicationService.run(context: context) } if allSettings.isEmpty { DefaultDataService.createDefaultSettings(context: context) } let tagDescriptor = FetchDescriptor() if (try? context.fetch(tagDescriptor))?.isEmpty ?? true { DefaultDataService.createDefaultTags(context: context) } } private func syncPremiumStatus(isColdLaunch: Bool) async { guard let settings else { return } let newPremium = await premiumSyncService.sync( currentIsPremium: settings.isPremium, isColdLaunch: isColdLaunch ) if settings.isPremium != newPremium { settings.isPremium = newPremium try? context.save() } CrashlyticsService.setUserProperties(isPremium: settings.isPremium) } }