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:
@@ -631,11 +631,22 @@ enum AchievementRegistry {
|
||||
)
|
||||
]
|
||||
|
||||
// MARK: - Lookup Dictionary
|
||||
|
||||
private static let definitionsById: [String: AchievementDefinition] = {
|
||||
Dictionary(uniqueKeysWithValues: all.map { ($0.id, $0) })
|
||||
}()
|
||||
|
||||
/// O(1) lookup by typeId
|
||||
static func definition(for typeId: String) -> AchievementDefinition? {
|
||||
definitionsById[typeId]
|
||||
}
|
||||
|
||||
// MARK: - Lookup Methods
|
||||
|
||||
/// Get achievement by ID
|
||||
static func achievement(byId id: String) -> AchievementDefinition? {
|
||||
all.first { $0.id == id }
|
||||
definitionsById[id]
|
||||
}
|
||||
|
||||
/// Get achievements by category
|
||||
|
||||
@@ -81,7 +81,7 @@ struct Game: Identifiable, Codable, Hashable {
|
||||
}
|
||||
|
||||
var gameDate: Date {
|
||||
Calendar.current.startOfDay(for: dateTime)
|
||||
dateTime.startOfDay(in: TimeZone(identifier: "UTC"))
|
||||
}
|
||||
|
||||
/// Alias for TripPlanningEngine compatibility
|
||||
|
||||
@@ -137,6 +137,18 @@ struct VisitSummary: Identifiable {
|
||||
let photoCount: Int
|
||||
let notes: String?
|
||||
|
||||
private static let mediumDateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .medium
|
||||
return f
|
||||
}()
|
||||
|
||||
private static let shortDateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "MMM d, yyyy"
|
||||
return f
|
||||
}()
|
||||
|
||||
/// Combined matchup for backwards compatibility
|
||||
var matchup: String? {
|
||||
guard let home = homeTeamName, let away = awayTeamName else { return nil }
|
||||
@@ -144,15 +156,11 @@ struct VisitSummary: Identifiable {
|
||||
}
|
||||
|
||||
var dateDescription: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter.string(from: visitDate)
|
||||
Self.mediumDateFormatter.string(from: visitDate)
|
||||
}
|
||||
|
||||
var shortDateDescription: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMM d, yyyy"
|
||||
return formatter.string(from: visitDate)
|
||||
Self.shortDateFormatter.string(from: visitDate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,4 +95,8 @@ extension Stadium: Equatable {
|
||||
static func == (lhs: Stadium, rhs: Stadium) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,8 @@ extension Team: Equatable {
|
||||
static func == (lhs: Team, rhs: Team) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +96,14 @@ struct Trip: Identifiable, Codable, Hashable {
|
||||
var startDate: Date { stops.first?.arrivalDate ?? preferences.startDate }
|
||||
var endDate: Date { stops.last?.departureDate ?? preferences.endDate }
|
||||
|
||||
private static let dateRangeFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "MMM d, yyyy"
|
||||
return f
|
||||
}()
|
||||
|
||||
var formattedDateRange: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMM d, yyyy"
|
||||
return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))"
|
||||
"\(Self.dateRangeFormatter.string(from: startDate)) - \(Self.dateRangeFormatter.string(from: endDate))"
|
||||
}
|
||||
|
||||
var formattedTotalDistance: String { String(format: "%.0f miles", totalDistanceMiles) }
|
||||
@@ -191,10 +195,14 @@ struct ItineraryDay: Identifiable, Hashable {
|
||||
let stops: [TripStop]
|
||||
let travelSegments: [TravelSegment]
|
||||
|
||||
private static let dayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEEE, MMM d"
|
||||
return f
|
||||
}()
|
||||
|
||||
var formattedDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, MMM d"
|
||||
return formatter.string(from: date)
|
||||
Self.dayFormatter.string(from: date)
|
||||
}
|
||||
|
||||
var isRestDay: Bool { stops.first?.isRestDay ?? false }
|
||||
|
||||
@@ -63,12 +63,17 @@ struct TripPoll: Identifiable, Codable, Hashable {
|
||||
// MARK: - Trip Hash
|
||||
|
||||
static func computeTripHash(_ trip: Trip) -> String {
|
||||
var hasher = Hasher()
|
||||
hasher.combine(trip.stops.map { $0.city })
|
||||
hasher.combine(trip.stops.flatMap { $0.games })
|
||||
hasher.combine(trip.preferences.startDate)
|
||||
hasher.combine(trip.preferences.endDate)
|
||||
return String(hasher.finalize())
|
||||
let cities = trip.stops.map { $0.city }.joined(separator: "|")
|
||||
let games = trip.stops.flatMap { $0.games }.joined(separator: "|")
|
||||
let start = String(trip.preferences.startDate.timeIntervalSince1970)
|
||||
let end = String(trip.preferences.endDate.timeIntervalSince1970)
|
||||
let input = "\(cities);\(games);\(start);\(end)"
|
||||
// Simple deterministic hash: DJB2
|
||||
var hash: UInt64 = 5381
|
||||
for byte in input.utf8 {
|
||||
hash = hash &* 33 &+ UInt64(byte)
|
||||
}
|
||||
return String(hash, radix: 16)
|
||||
}
|
||||
|
||||
// MARK: - Deep Link URL
|
||||
@@ -87,22 +92,53 @@ struct TripPoll: Identifiable, Codable, Hashable {
|
||||
struct PollVote: Identifiable, Codable, Hashable {
|
||||
let id: UUID
|
||||
let pollId: UUID
|
||||
let odg: String // voter's userRecordID
|
||||
let voterId: String // voter's userRecordID
|
||||
var rankings: [Int] // trip indices in preference order
|
||||
let votedAt: Date
|
||||
var modifiedAt: Date
|
||||
|
||||
/// Backward-compatible decoding: accepts both "voterId" and legacy "odg" keys
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, pollId, voterId, rankings, votedAt, modifiedAt
|
||||
case odg // legacy key
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(UUID.self, forKey: .id)
|
||||
pollId = try container.decode(UUID.self, forKey: .pollId)
|
||||
// Decode from "voterId" first, falling back to legacy "odg"
|
||||
if let vid = try container.decodeIfPresent(String.self, forKey: .voterId) {
|
||||
voterId = vid
|
||||
} else {
|
||||
voterId = try container.decode(String.self, forKey: .odg)
|
||||
}
|
||||
rankings = try container.decode([Int].self, forKey: .rankings)
|
||||
votedAt = try container.decode(Date.self, forKey: .votedAt)
|
||||
modifiedAt = try container.decode(Date.self, forKey: .modifiedAt)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(pollId, forKey: .pollId)
|
||||
try container.encode(voterId, forKey: .voterId)
|
||||
try container.encode(rankings, forKey: .rankings)
|
||||
try container.encode(votedAt, forKey: .votedAt)
|
||||
try container.encode(modifiedAt, forKey: .modifiedAt)
|
||||
}
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
pollId: UUID,
|
||||
odg: String,
|
||||
voterId: String,
|
||||
rankings: [Int],
|
||||
votedAt: Date = Date(),
|
||||
modifiedAt: Date = Date()
|
||||
) {
|
||||
self.id = id
|
||||
self.pollId = pollId
|
||||
self.odg = odg
|
||||
self.voterId = voterId
|
||||
self.rankings = rankings
|
||||
self.votedAt = votedAt
|
||||
self.modifiedAt = modifiedAt
|
||||
|
||||
Reference in New Issue
Block a user