// WidgetLocationFetcher.swift — one-shot location for the WIDGET EXTENSION. // // WidgetKit providers run in a short budget, so this fetcher is deliberately // timeout-bounded: it requests a single location and gives up after a few // seconds, falling back to whatever the caller already has cached. // // Location permission is shared with the containing app (same TCC identity), // so if the user granted "While Using" in FuelBoard, the widget can use // location directly — no extra prompt in the car. import Foundation import CoreLocation @MainActor final class WidgetLocationFetcher: NSObject, CLLocationManagerDelegate { static let shared = WidgetLocationFetcher() private let manager = CLLocationManager() private var continuation: CheckedContinuation? private var timeoutTask: Task? private override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyHundredMeters } /// Requests the current location. Returns nil on denial, timeout, or /// no-permission — callers should fall back to cached data. func currentLocation(timeout: TimeInterval = 4) async -> CLLocation? { guard CLLocationManager.locationServicesEnabled() else { return nil } switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: break case .notDetermined: // Widget extensions share the app's TCC identity; if the app was // never opened, the system can't prompt from here — fall back. return nil case .denied, .restricted: return nil @unknown default: return nil } return await withCheckedContinuation { cont in continuation = cont manager.requestLocation() timeoutTask = Task { [weak self] in try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) self?.continuation?.resume(returning: nil) self?.continuation = nil } } } nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { MainActor.assumeIsolated { timeoutTask?.cancel() continuation?.resume(returning: locations.last) continuation = nil } } nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { MainActor.assumeIsolated { timeoutTask?.cancel() continuation?.resume(returning: nil) continuation = nil } } }