Files
JQC_iOS_App/JanitorialQC/ContentView.swift
T

97 lines
4.3 KiB
Swift

// 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.")
}
}
}
}