// // DeepLinkHandler.swift // SportsTime // // Handles deep link URL routing for the app // import Foundation import SwiftUI // MARK: - Identifiable Share Code struct IdentifiableShareCode: Identifiable, Hashable { 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 } } }