fix: codebase audit fixes — safety, accessibility, and production hygiene
Address 16 issues from external audit: - Move StoreKit transaction listener ownership to StoreManager singleton with proper deinit - Remove noisy VoiceOver announcements, add missing accessibility on StatPill and BootstrapLoadingView - Replace String @retroactive Identifiable with IdentifiableShareCode wrapper - Add crash guard in AchievementEngine getContributingVisitIds + cache stadium lookups - Pre-compute GamesHistoryViewModel filtered properties to avoid redundant SwiftUI recomputation - Remove force-unwraps in ProgressMapView with safe guard-let fallback - Add diff-based update gating in ItineraryTableViewWrapper to prevent unnecessary reloads - Replace deprecated UIScreen.main with UIWindowScene lookup - Add deinit task cancellation in ScheduleViewModel and SuggestedTripsGenerator - Wrap ~234 unguarded print() calls across 27 files in #if DEBUG Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,9 +15,6 @@ 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<Void, Never>?
|
||||
|
||||
init() {
|
||||
// UI Test Mode: disable animations and force classic style for deterministic tests
|
||||
if ProcessInfo.isUITesting || ProcessInfo.shouldDisableAnimations {
|
||||
@@ -37,7 +34,7 @@ struct SportsTimeApp: App {
|
||||
|
||||
// Start listening for transactions immediately
|
||||
if !ProcessInfo.isUITesting {
|
||||
transactionListener = StoreManager.shared.listenForTransactions()
|
||||
StoreManager.shared.startListeningForTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +124,7 @@ struct BootstrappedContentView: View {
|
||||
}
|
||||
.sheet(item: $deepLinkHandler.pendingPollShareCode) { code in
|
||||
NavigationStack {
|
||||
PollDetailView(shareCode: code)
|
||||
PollDetailView(shareCode: code.value)
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: .constant(deepLinkHandler.error != nil)) {
|
||||
@@ -176,7 +173,9 @@ struct BootstrappedContentView: View {
|
||||
|
||||
@MainActor
|
||||
private func performBootstrap() async {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Starting app bootstrap...")
|
||||
#endif
|
||||
isBootstrapping = true
|
||||
bootstrapError = nil
|
||||
|
||||
@@ -186,7 +185,9 @@ struct BootstrappedContentView: View {
|
||||
do {
|
||||
// 0. UI Test Mode: reset user data if requested
|
||||
if ProcessInfo.shouldResetState {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 0: Resetting user data for UI tests...")
|
||||
#endif
|
||||
try context.delete(model: SavedTrip.self)
|
||||
try context.delete(model: StadiumVisit.self)
|
||||
try context.delete(model: Achievement.self)
|
||||
@@ -197,59 +198,81 @@ struct BootstrappedContentView: View {
|
||||
}
|
||||
|
||||
// 1. Bootstrap from bundled JSON if first launch (no data exists)
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 1: Checking if bootstrap needed...")
|
||||
#endif
|
||||
try await bootstrapService.bootstrapIfNeeded(context: context)
|
||||
|
||||
// 2. Configure DataProvider with SwiftData context
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 2: Configuring DataProvider...")
|
||||
#endif
|
||||
AppDataProvider.shared.configure(with: context)
|
||||
|
||||
// 3. Configure BackgroundSyncManager with model container
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 3: Configuring BackgroundSyncManager...")
|
||||
#endif
|
||||
BackgroundSyncManager.shared.configure(with: modelContainer)
|
||||
|
||||
// 4. Load data from SwiftData into memory
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 4: Loading initial data from SwiftData...")
|
||||
#endif
|
||||
await AppDataProvider.shared.loadInitialData()
|
||||
if let loadError = AppDataProvider.shared.error {
|
||||
throw loadError
|
||||
}
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Loaded \(AppDataProvider.shared.teams.count) teams")
|
||||
print("🚀 [BOOT] Loaded \(AppDataProvider.shared.stadiums.count) stadiums")
|
||||
#endif
|
||||
|
||||
// 5. Load store products and entitlements
|
||||
if ProcessInfo.isUITesting {
|
||||
print("🚀 [BOOT] Step 5: UI Test Mode — forcing Pro, skipping StoreKit")
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 5: UI Test Mode — forcing Pro, skipping StoreKit")
|
||||
StoreManager.shared.debugProOverride = true
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 5: Loading store products...")
|
||||
#endif
|
||||
await StoreManager.shared.loadProducts()
|
||||
await StoreManager.shared.updateEntitlements()
|
||||
}
|
||||
|
||||
// 6. Start network monitoring and wire up sync callback
|
||||
if !ProcessInfo.isUITesting {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 6: Starting network monitoring...")
|
||||
#endif
|
||||
NetworkMonitor.shared.onSyncNeeded = {
|
||||
await BackgroundSyncManager.shared.triggerSyncFromNetworkRestoration()
|
||||
}
|
||||
NetworkMonitor.shared.startMonitoring()
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 6: UI Test Mode — skipping network monitoring")
|
||||
#endif
|
||||
}
|
||||
|
||||
// 7. Configure analytics
|
||||
if !ProcessInfo.isUITesting {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 7: Configuring analytics...")
|
||||
#endif
|
||||
AnalyticsManager.shared.configure()
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 7: UI Test Mode — skipping analytics")
|
||||
#endif
|
||||
}
|
||||
|
||||
// 8. App is now usable
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 8: Bootstrap complete - app ready")
|
||||
#endif
|
||||
isBootstrapping = false
|
||||
UIAccessibility.post(notification: .screenChanged, argument: nil)
|
||||
|
||||
@@ -264,7 +287,9 @@ struct BootstrappedContentView: View {
|
||||
}
|
||||
|
||||
// 10. Background: Try to refresh from CloudKit (non-blocking)
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Step 10: Starting background CloudKit sync...")
|
||||
#endif
|
||||
Task(priority: .background) {
|
||||
await self.performBackgroundSync(context: self.modelContainer.mainContext)
|
||||
await MainActor.run {
|
||||
@@ -272,11 +297,15 @@ struct BootstrappedContentView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("🚀 [BOOT] Steps 9-10: UI Test Mode — skipping CloudKit sync")
|
||||
#endif
|
||||
hasCompletedInitialSync = true
|
||||
}
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("❌ [BOOT] Bootstrap failed: \(error.localizedDescription)")
|
||||
#endif
|
||||
bootstrapError = error
|
||||
isBootstrapping = false
|
||||
}
|
||||
@@ -286,7 +315,6 @@ struct BootstrappedContentView: View {
|
||||
private func performBackgroundSync(context: ModelContext) async {
|
||||
let log = SyncLogger.shared
|
||||
log.log("🔄 [SYNC] Starting background sync...")
|
||||
AccessibilityAnnouncer.announce("Sync started.")
|
||||
|
||||
// Log diagnostic info for debugging CloudKit container issues
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
|
||||
@@ -341,7 +369,9 @@ struct BootstrappedContentView: View {
|
||||
} else {
|
||||
log.log("🔄 [SYNC] No updates - skipping DataProvider reload")
|
||||
}
|
||||
AccessibilityAnnouncer.announce("Sync complete. Updated \(result.totalUpdated) records.")
|
||||
if result.totalUpdated > 0 {
|
||||
AccessibilityAnnouncer.announce("Sync complete. Updated \(result.totalUpdated) records.")
|
||||
}
|
||||
} catch CanonicalSyncService.SyncError.cloudKitUnavailable {
|
||||
log.log("❌ [SYNC] CloudKit unavailable - using local data only")
|
||||
AccessibilityAnnouncer.announce("Cloud sync unavailable. Using local data.")
|
||||
@@ -353,12 +383,6 @@ struct BootstrappedContentView: View {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - String Identifiable for Sheet
|
||||
|
||||
extension String: @retroactive Identifiable {
|
||||
public var id: String { self }
|
||||
}
|
||||
|
||||
// MARK: - Bootstrap Loading View
|
||||
|
||||
struct BootstrapLoadingView: View {
|
||||
@@ -370,6 +394,7 @@ struct BootstrapLoadingView: View {
|
||||
.font(.headline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user