// // ReflectApp.swift // Shared // // Created by Trey Tartt on 1/5/22. // import SwiftUI import SwiftData import BackgroundTasks import WidgetKit @main struct ReflectApp: App { @Environment(\.scenePhase) private var scenePhase @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate let dataController = DataController.shared @StateObject var iapManager = IAPManager.shared @StateObject var authManager = BiometricAuthManager() @StateObject var healthKitManager = HealthKitManager.shared @AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date() @State private var showSubscriptionFromWidget = false @State private var showStorageFallbackAlert = SharedModelContainer.isUsingInMemoryFallback init() { // Configure UI test mode before anything else if UITestMode.isUITesting { UITestMode.configureIfNeeded() } AnalyticsManager.shared.configure() BGTaskScheduler.shared.cancelAllTaskRequests() BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in guard let processingTask = task as? BGProcessingTask else { return } BGTask.runFillInMissingDatesTask(task: processingTask) } BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.weatherRetryID, using: nil) { task in guard let processingTask = task as? BGProcessingTask else { return } BGTask.runWeatherRetryTask(task: processingTask) } BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.weeklyDigestID, using: nil) { task in guard let processingTask = task as? BGProcessingTask else { return } BGTask.runWeeklyDigestTask(task: processingTask) } UNUserNotificationCenter.current().setBadgeCount(0) // Reset tips session on app launch ReflectTipsManager.shared.resetSession() // Initialize Live Activity scheduler LiveActivityScheduler.shared.scheduleBasedOnCurrentTime() // Initialize Watch Connectivity for cross-device widget updates _ = WatchConnectivityManager.shared } var body: some Scene { WindowGroup { ZStack { MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)), monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)), yearView: YearView(viewModel: YearViewModel()), insightsView: InsightsView()) .modelContainer(dataController.container) .environmentObject(iapManager) .environmentObject(authManager) .environmentObject(healthKitManager) .sheet(isPresented: $showSubscriptionFromWidget) { ReflectSubscriptionStoreView(source: "widget_deeplink") .environmentObject(iapManager) } .onOpenURL { url in handleDeepLink(url) } .alert("Data Storage Unavailable", isPresented: $showStorageFallbackAlert) { Button("OK", role: .cancel) { } .accessibilityIdentifier(AccessibilityID.AppAlert.storageUnavailableOK) } message: { Text("Your mood data cannot be saved permanently. Please restart the app. If the problem persists, reinstall the app.") } .onAppear { if SharedModelContainer.isUsingInMemoryFallback { AnalyticsManager.shared.track(.storageFallbackActivated) } if let url = AppDelegate.pendingDeepLinkURL { AppDelegate.pendingDeepLinkURL = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { handleDeepLink(url) } } } // Lock screen overlay if authManager.isLockEnabled && !authManager.isUnlocked { LockScreenView(authManager: authManager) .transition(.opacity) } } }.onChange(of: scenePhase) { _, newPhase in if newPhase == .background { BGTask.scheduleBackgroundProcessing() BGTask.scheduleWeeklyDigest() WidgetCenter.shared.reloadAllTimelines() // Flush pending analytics events AnalyticsManager.shared.flush() // Lock the app when going to background authManager.lock() } if newPhase == .active { UNUserNotificationCenter.current().setBadgeCount(0) // Authenticate if locked - this must happen immediately on main thread if authManager.isLockEnabled && !authManager.isUnlocked { Task { await authManager.authenticate() } } // Defer all non-critical foreground work to avoid blocking UI Task(priority: .utility) { // Refresh from disk to pick up widget/watch changes DataController.shared.refreshFromDisk() // Fill in any missing dates (moved from AppDelegate) DataController.shared.fillInMissingDates() // Clean up any duplicate entries (after backfill so backfill dupes are caught) DataController.shared.removeDuplicates() // Reschedule notifications for new title LocalNotification.rescheduleNotifiations() // Update super properties on foreground AnalyticsManager.shared.updateSuperProperties() } // Defer Live Activity scheduling (heavy DB operations) Task(priority: .utility) { await LiveActivityScheduler.shared.scheduleBasedOnCurrentTime() } // Catch up on side effects from widget/watch votes Task(priority: .utility) { await MoodLogger.shared.processPendingSideEffects() } // Check subscription status (network call) - throttled Task(priority: .background) { await iapManager.checkSubscriptionStatus() await iapManager.trackSubscriptionAnalytics(source: "app_foreground") } } } } private func handleDeepLink(_ url: URL) { if url.scheme == "reflect" && url.host == "subscribe" { showSubscriptionFromWidget = true } } }