fix: comprehensive codebase hardening — crashes, silent failures, performance, and security

Fixes ~95 issues from deep audit across 12 categories in 82 files:

- Crash prevention: double-resume in PhotoMetadataExtractor, force unwraps in
  DateRangePicker, array bounds checks in polls/achievements, ProGate hit-test
  bypass, Dictionary(uniqueKeysWithValues:) → uniquingKeysWith in 4 files
- Silent failure elimination: all 34 try? sites replaced with do/try/catch +
  logging (SavedTrip, TripDetailView, CanonicalSyncService, BootstrapService,
  CanonicalModels, CKModels, SportsTimeApp, and more)
- Performance: cached DateFormatters (7 files), O(1) team lookups via
  AppDataProvider, achievement definition dictionary, AnimatedBackground
  consolidated from 19 Tasks to 1, task cancellation in SharePreviewView
- Concurrency: UIKit drawing → MainActor.run, background fetch timeout guard,
  @MainActor on ThemeManager/AppearanceManager, SyncLogger read/write race fix
- Planning engine: game end time in travel feasibility, state-aware city
  normalization, exact city matching, DrivingConstraints parameter propagation
- IAP: unknown subscription states → expired, unverified transaction logging,
  entitlements updated before paywall dismiss, restore visible to all users
- Security: API key to Info.plist lookup, filename sanitization in PDF export,
  honest User-Agent, removed stale "Feels" analytics super properties
- Navigation: consolidated competing navigationDestination, boolean → value-based
- Testing: 8 sleep() → waitForExistence, duplicates extracted, Swift 6 compat
- Service bugs: infinite retry cap, duplicate achievement prevention, TOCTOU vote
  fix, PollVote.odg → voterId rename, deterministic placeholder IDs, parallel
  MKDirections, Sendable-safe POI struct

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-27 17:03:09 -06:00
parent e046cb6b34
commit c94e373e33
82 changed files with 1163 additions and 599 deletions

View File

@@ -14,6 +14,8 @@ import SwiftUI
struct AnimatedSportsBackground: View {
@Environment(\.colorScheme) private var colorScheme
@State private var animate = false
@State private var glowOpacities: [Double] = Array(repeating: 0, count: AnimatedSportsIcon.configs.count)
@State private var glowDriverTask: Task<Void, Never>?
var body: some View {
ZStack {
@@ -25,7 +27,7 @@ struct AnimatedSportsBackground: View {
// Floating sports icons with gentle glow
ForEach(0..<AnimatedSportsIcon.configs.count, id: \.self) { index in
AnimatedSportsIcon(index: index, animate: animate)
AnimatedSportsIcon(index: index, animate: animate, glowOpacity: glowOpacities[index])
}
}
.accessibilityHidden(true)
@@ -34,6 +36,60 @@ struct AnimatedSportsBackground: View {
withAnimation(.easeInOut(duration: 5.0).repeatForever(autoreverses: true)) {
animate = true
}
startGlowDriver()
}
.onDisappear {
animate = false
glowDriverTask?.cancel()
glowDriverTask = nil
}
}
/// Single task that drives glow animations for all icons
private func startGlowDriver() {
glowDriverTask = Task { @MainActor in
let iconCount = AnimatedSportsIcon.configs.count
// Track next glow time for each icon
var nextGlowTime: [TimeInterval] = (0..<iconCount).map { _ in
Double.random(in: 2.0...8.0)
}
// Track glow state: 0 = waiting, 1 = glowing, 2 = fading out
var glowState: [Int] = Array(repeating: 0, count: iconCount)
var stateTimer: [TimeInterval] = Array(repeating: 0, count: iconCount)
let tickInterval: TimeInterval = 0.5
let startTime = Date.timeIntervalSinceReferenceDate
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(tickInterval))
guard !Task.isCancelled else { break }
let elapsed = Date.timeIntervalSinceReferenceDate - startTime
for i in 0..<iconCount {
switch glowState[i] {
case 0: // Waiting
if elapsed >= nextGlowTime[i] {
withAnimation(.easeIn(duration: 0.8)) { glowOpacities[i] = 1 }
glowState[i] = 1
stateTimer[i] = elapsed
}
case 1: // Glowing - hold for 1.2s
if elapsed - stateTimer[i] >= 1.2 {
withAnimation(.easeOut(duration: 1.0)) { glowOpacities[i] = 0 }
glowState[i] = 2
stateTimer[i] = elapsed
}
case 2: // Fading out - wait 1.0s then schedule next
if elapsed - stateTimer[i] >= 1.0 {
glowState[i] = 0
nextGlowTime[i] = elapsed + Double.random(in: 6.0...12.0)
}
default:
break
}
}
}
}
}
}
@@ -108,8 +164,7 @@ struct RouteMapLayer: View {
struct AnimatedSportsIcon: View {
let index: Int
let animate: Bool
@State private var glowOpacity: Double = 0
@State private var glowTask: Task<Void, Never>?
var glowOpacity: Double = 0
static let configs: [(x: CGFloat, y: CGFloat, icon: String, rotation: Double, scale: CGFloat)] = [
// Edge icons
@@ -162,29 +217,5 @@ struct AnimatedSportsIcon: View {
value: animate
)
}
.onAppear {
glowTask = Task { @MainActor in
// Random initial delay
try? await Task.sleep(for: .seconds(Double.random(in: 2.0...8.0)))
while !Task.isCancelled {
guard !Theme.Animation.prefersReducedMotion else {
try? await Task.sleep(for: .seconds(6.0))
continue
}
// Slow fade in
withAnimation(.easeIn(duration: 0.8)) { glowOpacity = 1 }
// Hold briefly then slow fade out
try? await Task.sleep(for: .seconds(1.2))
guard !Task.isCancelled else { break }
withAnimation(.easeOut(duration: 1.0)) { glowOpacity = 0 }
// Wait before next glow
try? await Task.sleep(for: .seconds(Double.random(in: 6.0...12.0)))
}
}
}
.onDisappear {
glowTask?.cancel()
glowTask = nil
}
}
}

View File

@@ -62,8 +62,9 @@ enum AppTheme: String, CaseIterable, Identifiable {
// MARK: - Theme Manager
@MainActor
@Observable
final class ThemeManager: @unchecked Sendable {
final class ThemeManager {
static let shared = ThemeManager()
var currentTheme: AppTheme {
@@ -130,8 +131,9 @@ enum AppearanceMode: String, CaseIterable, Identifiable {
// MARK: - Appearance Manager
@MainActor
@Observable
final class AppearanceManager: @unchecked Sendable {
final class AppearanceManager {
static let shared = AppearanceManager()
var currentMode: AppearanceMode {
@@ -154,7 +156,7 @@ final class AppearanceManager: @unchecked Sendable {
enum Theme {
private static var current: AppTheme { ThemeManager.shared.currentTheme }
private static var current: AppTheme { MainActor.assumeIsolated { ThemeManager.shared.currentTheme } }
// MARK: - Primary Accent Color