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:
Trey t
2026-02-22 00:07:53 -06:00
parent 826eadbc0f
commit 91c5eac22d
32 changed files with 434 additions and 67 deletions

View File

@@ -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)
}
}