// ContentView.swift import SwiftUI import UIKit struct ContentView: View { @EnvironmentObject private var auth: AuthManager @EnvironmentObject private var sync: SyncManager @StateObject private var updateChecker = UpdateChecker.shared @Environment(\.scenePhase) private var scenePhase var body: some View { Group { if auth.isLoading && !auth.isAuthenticated { VStack(spacing: 16) { Image(systemName: "checkmark.seal.fill") .font(.system(size: 64)) .foregroundStyle(.blue) ProgressView("Loading…") } } else if auth.isAuthenticated { DashboardView() } else { LoginView() } } .animation(.easeInOut(duration: 0.3), value: auth.isAuthenticated) .task { // 1. Restore session first — sets isAuthenticated and loads tokens. await AuthManager.shared.restoreSession() // 2. Only start network monitoring AFTER auth is resolved. // startMonitoring() fires triggerSync() immediately on connectivity, // so it must not run before tokens are in place. SyncManager.shared.startMonitoring() // 3. If already online and authenticated, do an initial sync. if SyncManager.shared.isOnline && AuthManager.shared.isAuthenticated { await SyncManager.shared.triggerSync() } // 4. Check the App Store for a newer version. Independent of auth — // runs even on the login screen so unauthenticated devices still // get prompted. Throttled internally to once per 24h; silent on // any failure (offline, parse error, etc.) so it never disrupts // normal app use. await updateChecker.checkForUpdate() // 5. Register device now that auth is fully resolved. // The .onChange(scenePhase == .active) fires BEFORE restoreSession() // completes on first launch, so auth.isAuthenticated is false there // and registration is skipped. This call covers that gap. if AuthManager.shared.isAuthenticated { await APIClient.shared.registerDevice() } } // Stop the 60s notification poll when the app goes to background and // restart it when it returns to the foreground. iOS suspends Tasks // automatically in the background anyway, but explicitly managing the // poll task here: // • Prevents the Task object from accumulating sleep-resume cycles // that never fired while suspended. // • Triggers an immediate sync+poll when the inspector returns to the // app after being away, rather than waiting up to 60s for the next // scheduled poll tick. // • Makes the lifecycle intent explicit and avoids relying on implicit // iOS suspension behaviour. .onChange(of: scenePhase) { _, newPhase in switch newPhase { case .background: SyncManager.shared.suspendPolling() case .active: SyncManager.shared.resumePolling() // Register / update device record on every foreground. // Fire-and-forget — auth guard is inside registerDevice(). if auth.isAuthenticated { Task { await APIClient.shared.registerDevice() } } default: break } } .alert("Update Available", isPresented: $updateChecker.updateAvailable) { Button("Update") { if let url = updateChecker.appStoreURL { UIApplication.shared.open(url) } } Button("Later", role: .cancel) { updateChecker.dismissForNow() } } message: { if let version = updateChecker.latestVersion { Text("Version \(version) is available on the App Store. Update now for the latest fixes and features.") } else { Text("A new version is available on the App Store.") } } } }