Widget fetches its own data from the relay

The widget previously read stations only from the app-group cache, which
SideStore free accounts don't provision — the widget process saw empty
data and silently fell back to sample stations (stuck on Morrisons).
Now the widget makes its own focused relay fetch around its location
(radius + limit 500, 5s timeout) whenever it has a fix, falling back to
the shared cache then samples. Widget data now reflects the relay
independently of the app. 35 tests pass.
This commit is contained in:
FuelBoard Contributor
2026-08-12 12:08:37 +01:00
parent 20589f41fe
commit 5ae0f945b3
+45 -5
View File
@@ -101,13 +101,22 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
FuelStore.saveLocation(lat: location.lat, lng: location.lng)
}
// 3) Load stations, then sort per this widget's config, scoped to
// the chosen search radius so the widget's "cheapest" matches
// the app's list.
var stations = FuelStore.loadStations()
// 3) Load stations. The widget prefers its OWN focused fetch from the
// relay so it shows real prices even when the app-group cache isn't
// shared (SideStore free accounts don't provision the group, which
// previously left the widget stuck on sample data). Falls back to the
// shared cache, then to samples.
var stations: [FuelStation] = []
if let location {
let radiusKM = unitToKM()
if let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) {
stations = fetched
}
}
if stations.isEmpty { stations = FuelStore.loadStations() }
if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
let unit = FuelStore.loadDistanceUnit()
let radiusKM = unit.toKM(Double(FuelStore.loadStationLimit())) // chosen units km
let radiusKM = unitToKM()
if let location {
// STRICT: cached data fetched around another location must never
// leak out-of-radius stations into the widget.
@@ -149,6 +158,37 @@ struct FuelPriceTimelineProvider: AppIntentTimelineProvider {
unit: FuelStore.loadDistanceUnit()
)
}
/// The search radius in km for the widget's own focused relay fetch.
private func unitToKM() -> Double {
FuelStore.loadDistanceUnit().toKM(Double(FuelStore.loadStationLimit()))
}
/// One focused fetch from the relay around the widget's location small
/// response (radius + limit 500), bounded so a dead relay can't stall the
/// timeline. Returns nil on any failure so callers fall back to cache.
private static func fetchFocused(near location: Coordinate, fuel: FuelType, radiusKM: Double) async -> [FuelStation]? {
var components = URLComponents(
url: RelayFuelProvider().baseURL.appendingPathComponent("api/v1/stations"),
resolvingAgainstBaseURL: false
)!
components.queryItems = [
URLQueryItem(name: "fuel", value: fuel.rawValue),
URLQueryItem(name: "lat", value: String(location.lat)),
URLQueryItem(name: "lng", value: String(location.lng)),
URLQueryItem(name: "radius", value: String(radiusKM)),
URLQueryItem(name: "limit", value: "500"),
]
var request = URLRequest(url: components.url!)
request.timeoutInterval = 5
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? FuelPriceProvider.decodeStations(from: data)
} catch {
return nil
}
}
}
struct FuelPriceWidgetView: View {