Small widget: fuel-only relay fetch when no location, so default/Cheapest/Closest faces populate with real stations instead of sample data. Root cause of the persistent skeleton: favourites mode reads keychain (works on free accounts), but the non-favourites path needs stations — the app-group cache can be absent on free accounts and keychain can't hold the station dump, so with no location fix the widget fell through to SampleFuelProvider and never populated. Now when location is nil the widget fetches /api/v1/stations?fuel=X&limit=500 (fuel-only, cheapest-first, verified against the relay).

This commit is contained in:
FuelBoard Contributor
2026-08-13 18:51:41 +01:00
parent 38b47eea38
commit 8646521176
+34
View File
@@ -226,6 +226,16 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
if let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) { if let fetched = await Self.fetchFocused(near: location, fuel: fuel, radiusKM: radiusKM) {
stations = fetched stations = fetched
} }
} else {
// No location (permission not granted / fix timed out): fetch
// fuel-only UK-wide so a fresh/default widget STILL populates
// with real stations. The app-group cache can be unavailable
// on free accounts and keychain can't hold the station dump,
// so the old fallback ended on sample data the "skeleton"
// face that never populated.
if let fetched = await Self.fetchFuelOnly(fuel: fuel, limit: 500) {
stations = fetched
}
} }
if stations.isEmpty { stations = FuelStore.loadStations() } if stations.isEmpty { stations = FuelStore.loadStations() }
if stations.isEmpty { stations = SampleFuelProvider.sampleStations } if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
@@ -296,6 +306,30 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
return nil return nil
} }
} }
/// Fuel-only UK-wide fetch (no lat/lng/radius) the relay returns the
/// cheapest-first dataset for the fuel, bounded by limit. Used when the
/// widget has no location fix so the face shows real stations instead of
/// sample data. Same timeout/bounded semantics as fetchFocused.
private static func fetchFuelOnly(fuel: FuelType, limit: Int) async -> [FuelStation]? {
var components = URLComponents(
url: RelayFuelProvider().baseURL.appendingPathComponent("api/v1/stations"),
resolvingAgainstBaseURL: false
)!
components.queryItems = [
URLQueryItem(name: "fuel", value: fuel.rawValue),
URLQueryItem(name: "limit", value: String(limit)),
]
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 { struct FuelPriceWidgetView: View {