diff --git a/FuelBoard/OnboardingView.swift b/FuelBoard/OnboardingView.swift index 683cd59..6721834 100644 --- a/FuelBoard/OnboardingView.swift +++ b/FuelBoard/OnboardingView.swift @@ -69,6 +69,18 @@ struct OnboardingView: View { } } } + // Replay path (test button / returning user): the Local Network + // permission is already decided, so probe silently up front. The + // probe fires no prompt once the permission is granted or denied — + // only the FIRST attempt (undetermined) shows the system alert, and + // that only happens on first run. By the time the data page appears + // its status is known, so it shows the green tick (or denied text) + // immediately, matching the Location and Notifications pages. + .onAppear { + if FuelStore.loadHasCompletedOnboarding() { + prompter.preflightDataAccess() + } + } .background( LinearGradient( colors: [Color(.systemBackground), Color.accentColor.opacity(0.06)], @@ -482,6 +494,66 @@ final class OnboardingPermissionPrompter: NSObject, ObservableObject, @preconcur } } + /// Replay pre-flight: determines the Local Network status WITHOUT any + /// user-visible check. Once the permission is decided (granted or denied) + /// a probe resolves silently — granted connects in milliseconds, denied + /// reports `.waiting` with `unsatisfiedReason == .localNetworkDenied`. + /// Only the FIRST-ever attempt (undetermined) shows the system alert; in + /// that case we cancel and leave the state unknown so the data page's + /// Continue triggers the prompt in context. First run never calls this. + func preflightDataAccess() { + guard !dataLoading, !dataGranted, !dataDenied else { return } + dataLoading = true + let baseURL = RelayFuelProvider().baseURL + guard let host = baseURL.host, + let port = baseURL.port else { + dataLoading = false + return + } + let connection = NWConnection( + host: NWEndpoint.Host(host), + port: NWEndpoint.Port(rawValue: UInt16(port))!, + using: .tcp + ) + dataConnection = connection + connection.stateUpdateHandler = { [weak self] state in + Task { @MainActor in + guard let self else { return } + switch state { + case .ready: + self.dataConnection = nil + self.dataLoading = false + self.dataGranted = true + self.dataDenied = false + case .waiting: + if connection.currentPath?.unsatisfiedReason == .localNetworkDenied { + // Denied in Settings — resolved silently, no prompt. + self.dataConnection = nil + self.dataLoading = false + self.dataGranted = false + self.dataDenied = true + } else { + // Prompt pending (permission undetermined — only + // possible after a reinstall, where the onboarding + // flag persisted but the permission reset). Cancel so + // page 4's Continue fires the prompt in context. + self.dataConnection = nil + self.dataLoading = false + connection.cancel() + } + case .failed, .cancelled: + self.dataConnection = nil + self.dataLoading = false + self.dataGranted = false + self.dataDenied = true + default: + break // .preparing — probe starting, hold on + } + } + } + connection.start(queue: .main) + } + nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { Task { @MainActor in self.refreshLocationStatus()