// JQCApp.swift // ------------ // App entry point. import SwiftUI import SwiftData import BackgroundTasks import UserNotifications @main struct JanitorialQCApp: App { @StateObject private var auth = AuthManager.shared @StateObject private var sync = SyncManager.shared init() { registerBackgroundTasks() requestNotificationPermission() // Set delegate so notifications display as banners when the app is in // the foreground. Without this iOS silently drops them. UNUserNotificationCenter.current().delegate = NotificationDelegate.shared } var body: some Scene { WindowGroup { ContentView() .environmentObject(auth) .environmentObject(sync) } .modelContainer(for: [ LocalFacility.self, LocalArea.self, LocalTemplate.self, LocalInspection.self, LocalIssue.self, PendingPhoto.self, SyncQueueEntry.self, ], isUndoEnabled: false) { result in switch result { case .success(let container): // Only set the model context here — do NOT await anything. // Session restore and sync are triggered by ContentView.task{} // which runs on the MainActor inside the SwiftUI lifecycle, // guaranteeing isLoading changes are seen by the view immediately. SyncManager.shared.modelContext = container.mainContext case .failure(let error): fatalError("SwiftData container failed: \(error)") } } } // ── Local notification permission ───────────────────────────────────── private func requestNotificationPermission() { UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .sound, .badge] ) { granted, error in if let error { print("[JQC] Notification permission error: \(error)") } } } private func registerBackgroundTasks() { BGTaskScheduler.shared.register( forTaskWithIdentifier: "com.jqc.sync", using: nil ) { task in guard let processingTask = task as? BGProcessingTask else { task.setTaskCompleted(success: false) return } handleBackgroundSync(task: processingTask) } } private func handleBackgroundSync(task: BGProcessingTask) { scheduleBackgroundSync() let syncTask = Task { await SyncManager.shared.triggerSync() } task.expirationHandler = { syncTask.cancel() } Task { await syncTask.value task.setTaskCompleted(success: !syncTask.isCancelled) } } } // ── Notification delegate ───────────────────────────────────────────────────── // Allows local notifications to appear as banners while the app is in the // foreground. Without this delegate iOS discards them silently. final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { static let shared = NotificationDelegate() private override init() {} func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { // Show banner + play sound even when the app is active in foreground. completionHandler([.banner, .sound]) } } func scheduleBackgroundSync() { let request = BGProcessingTaskRequest(identifier: "com.jqc.sync") request.requiresNetworkConnectivity = true request.requiresExternalPower = false try? BGTaskScheduler.shared.submit(request) }