Files
Sportstime/SportsTime/Core/Services/DeepLinkHandler.swift
Trey t 91c5eac22d 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>
2026-02-22 00:07:53 -06:00

154 lines
4.0 KiB
Swift

//
// DeepLinkHandler.swift
// SportsTime
//
// Handles deep link URL routing for the app
//
import Foundation
import SwiftUI
// MARK: - Identifiable Share Code
struct IdentifiableShareCode: Identifiable {
let id: String
var value: String { id }
}
// MARK: - Deep Link Handler
@MainActor
@Observable
final class DeepLinkHandler {
static let shared = DeepLinkHandler()
// MARK: - State
/// The pending poll share code from a deep link
/// Setting to nil clears the pending state
var pendingPollShareCode: IdentifiableShareCode? {
didSet {
if pendingPollShareCode == nil {
pendingPoll = nil
}
}
}
/// The loaded poll from a deep link (if successfully fetched)
private(set) var pendingPoll: TripPoll?
/// Whether the handler is currently loading a poll
private(set) var isLoading = false
/// Any error that occurred while handling a deep link
var error: DeepLinkError?
private init() {}
// MARK: - URL Handling
/// Handles an incoming URL and routes to the appropriate destination
/// - Parameter url: The URL to handle
/// - Returns: The parsed deep link destination, if valid
@discardableResult
func handleURL(_ url: URL) -> DeepLinkDestination? {
guard let destination = parseURL(url) else { return nil }
switch destination {
case .poll(let shareCode):
pendingPollShareCode = IdentifiableShareCode(id: shareCode)
}
return destination
}
/// Parses a URL into a deep link destination
/// - Parameter url: The URL to parse
/// - Returns: The parsed destination, or nil if invalid
func parseURL(_ url: URL) -> DeepLinkDestination? {
// Validate scheme
guard url.scheme == "sportstime" else { return nil }
switch url.host {
case "poll":
// sportstime://poll/{code}
guard let code = url.pathComponents.dropFirst().first,
code.count == 6 else { return nil }
return .poll(shareCode: code.uppercased())
default:
return nil
}
}
/// Loads a poll by share code from CloudKit
/// - Parameter shareCode: The 6-character share code
func loadPoll(shareCode: String) async {
isLoading = true
error = nil
do {
let poll = try await PollService.shared.fetchPoll(byShareCode: shareCode)
pendingPoll = poll
} catch let pollError as PollError {
switch pollError {
case .pollNotFound:
error = .pollNotFound
case .networkUnavailable:
error = .networkUnavailable
default:
error = .loadFailed(pollError)
}
} catch {
self.error = .loadFailed(error)
}
isLoading = false
}
/// Clears any pending deep link state
func clearPending() {
pendingPollShareCode = nil
pendingPoll = nil
error = nil
}
}
// MARK: - Deep Link Destination
enum DeepLinkDestination: Equatable {
case poll(shareCode: String)
}
// MARK: - Deep Link Error
enum DeepLinkError: LocalizedError, Equatable {
case pollNotFound
case networkUnavailable
case loadFailed(Error)
var errorDescription: String? {
switch self {
case .pollNotFound:
return "This poll no longer exists or the code is invalid."
case .networkUnavailable:
return "Unable to connect. Please check your internet connection."
case .loadFailed(let error):
return "Failed to load: \(error.localizedDescription)"
}
}
static func == (lhs: DeepLinkError, rhs: DeepLinkError) -> Bool {
switch (lhs, rhs) {
case (.pollNotFound, .pollNotFound):
return true
case (.networkUnavailable, .networkUnavailable):
return true
case (.loadFailed, .loadFailed):
return true
default:
return false
}
}
}