// // SportsTimeApp.swift // SportsTime // // Created by Trey Tartt on 1/6/26. // import SwiftUI import SwiftData import BackgroundTasks @main struct SportsTimeApp: App { /// App delegate for handling push notifications @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate /// Task that listens for StoreKit transaction updates private var transactionListener: Task? init() { // Register background tasks BEFORE app finishes launching // This must happen synchronously in init or applicationDidFinishLaunching BackgroundSyncManager.shared.registerTasks() // Start listening for transactions immediately transactionListener = StoreManager.shared.listenForTransactions() } var sharedModelContainer: ModelContainer = { let schema = Schema([ // User data models SavedTrip.self, TripVote.self, UserPreferences.self, CachedSchedule.self, // Stadium progress models StadiumVisit.self, VisitPhotoMetadata.self, Achievement.self, CachedGameScore.self, // Poll models LocalTripPoll.self, LocalPollVote.self, // Canonical data models SyncState.self, CanonicalStadium.self, StadiumAlias.self, CanonicalTeam.self, TeamAlias.self, LeagueStructureModel.self, CanonicalGame.self, CanonicalSport.self, ]) let modelConfiguration = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, cloudKitDatabase: .none // Local only; CloudKit used separately for schedules ) do { return try ModelContainer(for: schema, configurations: [modelConfiguration]) } catch { fatalError("Could not create ModelContainer: \(error)") } }() var body: some Scene { WindowGroup { BootstrappedContentView(modelContainer: sharedModelContainer) .environment(\.isDemoMode, ProcessInfo.isDemoMode) } .modelContainer(sharedModelContainer) } } // MARK: - Bootstrapped Content View /// Wraps the main content with bootstrap logic. /// Shows a loading indicator until bootstrap completes, then shows HomeView. struct BootstrappedContentView: View { let modelContainer: ModelContainer @Environment(\.scenePhase) private var scenePhase @State private var isBootstrapping = true @State private var bootstrapError: Error? @State private var hasCompletedInitialSync = false @State private var showOnboardingPaywall = false @State private var deepLinkHandler = DeepLinkHandler.shared @State private var appearanceManager = AppearanceManager.shared private var shouldShowOnboardingPaywall: Bool { !UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro } var body: some View { Group { if isBootstrapping { BootstrapLoadingView() } else if let error = bootstrapError { BootstrapErrorView(error: error) { Task { await performBootstrap() } } } else { HomeView() .sheet(isPresented: $showOnboardingPaywall) { OnboardingPaywallView(isPresented: $showOnboardingPaywall) .interactiveDismissDisabled() } .sheet(item: $deepLinkHandler.pendingPollShareCode) { code in NavigationStack { PollDetailView(shareCode: code) } } .alert("Error", isPresented: .constant(deepLinkHandler.error != nil)) { Button("OK") { deepLinkHandler.clearPending() } } message: { Text(deepLinkHandler.error?.localizedDescription ?? "") } .onAppear { if shouldShowOnboardingPaywall { showOnboardingPaywall = true } } } } .task { await performBootstrap() } .onOpenURL { url in deepLinkHandler.handleURL(url) } .onChange(of: scenePhase) { _, newPhase in switch newPhase { case .active: // Sync when app comes to foreground (but not on initial launch) if hasCompletedInitialSync { Task { await performBackgroundSync(context: modelContainer.mainContext) } } case .background: // Schedule background tasks when app goes to background BackgroundSyncManager.shared.scheduleAllTasks() default: break } } .preferredColorScheme(appearanceManager.currentMode.colorScheme) } @MainActor private func performBootstrap() async { print("🚀 [BOOT] Starting app bootstrap...") isBootstrapping = true bootstrapError = nil let context = modelContainer.mainContext let bootstrapService = BootstrapService() do { // 1. Bootstrap from bundled JSON if first launch (no data exists) print("🚀 [BOOT] Step 1: Checking if bootstrap needed...") try await bootstrapService.bootstrapIfNeeded(context: context) // 2. Configure DataProvider with SwiftData context print("🚀 [BOOT] Step 2: Configuring DataProvider...") AppDataProvider.shared.configure(with: context) // 3. Configure BackgroundSyncManager with model container print("🚀 [BOOT] Step 3: Configuring BackgroundSyncManager...") BackgroundSyncManager.shared.configure(with: modelContainer) // 4. Load data from SwiftData into memory print("🚀 [BOOT] Step 4: Loading initial data from SwiftData...") await AppDataProvider.shared.loadInitialData() print("🚀 [BOOT] Loaded \(AppDataProvider.shared.teams.count) teams") print("🚀 [BOOT] Loaded \(AppDataProvider.shared.stadiums.count) stadiums") // 5. Load store products and entitlements print("🚀 [BOOT] Step 5: Loading store products...") await StoreManager.shared.loadProducts() await StoreManager.shared.updateEntitlements() // 6. Start network monitoring and wire up sync callback print("🚀 [BOOT] Step 6: Starting network monitoring...") NetworkMonitor.shared.onSyncNeeded = { await BackgroundSyncManager.shared.triggerSyncFromNetworkRestoration() } NetworkMonitor.shared.startMonitoring() // 7. App is now usable print("🚀 [BOOT] Step 7: Bootstrap complete - app ready") isBootstrapping = false // 8. Schedule background tasks for future syncs BackgroundSyncManager.shared.scheduleAllTasks() // 9. Background: Try to refresh from CloudKit (non-blocking) print("🚀 [BOOT] Step 9: Starting background CloudKit sync...") Task.detached(priority: .background) { await self.performBackgroundSync(context: context) await MainActor.run { self.hasCompletedInitialSync = true } } } catch { print("❌ [BOOT] Bootstrap failed: \(error.localizedDescription)") bootstrapError = error isBootstrapping = false } } @MainActor private func performBackgroundSync(context: ModelContext) async { let log = SyncLogger.shared log.log("🔄 [SYNC] Starting background sync...") // Reset stale syncInProgress flag (in case app was killed mid-sync) let syncState = SyncState.current(in: context) if syncState.syncInProgress { log.log("⚠️ [SYNC] Resetting stale syncInProgress flag") syncState.syncInProgress = false try? context.save() } let syncService = CanonicalSyncService() do { let result = try await syncService.syncAll(context: context) log.log("🔄 [SYNC] Sync completed in \(String(format: "%.2f", result.duration))s") log.log("🔄 [SYNC] Stadiums: \(result.stadiumsUpdated)") log.log("🔄 [SYNC] Teams: \(result.teamsUpdated)") log.log("🔄 [SYNC] Games: \(result.gamesUpdated)") log.log("🔄 [SYNC] League Structures: \(result.leagueStructuresUpdated)") log.log("🔄 [SYNC] Team Aliases: \(result.teamAliasesUpdated)") log.log("🔄 [SYNC] Stadium Aliases: \(result.stadiumAliasesUpdated)") log.log("🔄 [SYNC] Sports: \(result.sportsUpdated)") log.log("🔄 [SYNC] Skipped (incompatible): \(result.skippedIncompatible)") log.log("🔄 [SYNC] Skipped (older): \(result.skippedOlder)") log.log("🔄 [SYNC] Total updated: \(result.totalUpdated)") // If any data was updated, reload the DataProvider if !result.isEmpty { log.log("🔄 [SYNC] Reloading DataProvider...") await AppDataProvider.shared.loadInitialData() log.log("🔄 [SYNC] DataProvider reloaded. Teams: \(AppDataProvider.shared.teams.count), Stadiums: \(AppDataProvider.shared.stadiums.count)") } else { log.log("🔄 [SYNC] No updates - skipping DataProvider reload") } } catch CanonicalSyncService.SyncError.cloudKitUnavailable { log.log("❌ [SYNC] CloudKit unavailable - using local data only") } catch { log.log("❌ [SYNC] Error: \(error.localizedDescription)") } } } // MARK: - String Identifiable for Sheet extension String: @retroactive Identifiable { public var id: String { self } } // MARK: - Bootstrap Loading View struct BootstrapLoadingView: View { var body: some View { VStack(spacing: 20) { LoadingSpinner(size: .large) Text("Setting up SportsTime...") .font(.headline) .foregroundStyle(.secondary) } } } // MARK: - Bootstrap Error View struct BootstrapErrorView: View { let error: Error let onRetry: () -> Void var body: some View { VStack(spacing: 20) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 50)) .foregroundStyle(.orange) Text("Setup Failed") .font(.title2) .fontWeight(.semibold) Text(error.localizedDescription) .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) Button("Try Again") { onRetry() } .buttonStyle(.borderedProminent) } .padding() } }