Harden App Store build for review

This commit is contained in:
FuelBoard Contributor
2026-10-01 12:19:05 +01:00
parent c7731d2b36
commit 50c543c776
14 changed files with 177 additions and 412 deletions
+19 -85
View File
@@ -82,8 +82,8 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
/// Diagnostics beacon tag: empty → derive from the intent type name. A
/// widget kind that shares an intent (the A/B test small widget reuses the
/// medium intent) passes an explicit tag so its beacons stay
/// distinguishable in the relay log and keychain slots.
/// medium intent) passes an explicit tag so its keychain diagnostic slots
/// stay distinguishable.
var beaconTag: String = ""
func placeholder(in context: Context) -> FuelPriceEntry {
@@ -122,10 +122,9 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
}
/// Fire-and-forget diagnostics beacon: writes the entry state to keychain
/// (app-readable, one slot per intent type) and GETs the relay so its
/// access log records that a widget timeline actually ran in the
/// extension and what it produced. Deliberately outside the timeline
/// result — can never affect rendering.
/// (app-readable, one slot per intent type) and records what a widget
/// timeline produced for Settings → Widget Diagnostics. Deliberately
/// outside the timeline result — can never affect rendering.
private func writeDiagBeacon(entry: FuelPriceEntry) {
let d = entry.data
let intentType = beaconTag.isEmpty ? String(describing: Configuration.self) : beaconTag
@@ -137,29 +136,8 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
"station":"\(first?.name ?? "")","price":\(first?.prices[d.fuel] ?? -1)}
"""
FuelStore.saveWidgetDiag(json, intentType: intentType)
// The relay GET is dev-only (same flag as the app beacon): consumers
// must never make a local-network attempt. The local app-group diag
// write above stays — Settings → Widget Diagnostics reads that.
guard FuelStore.loadRelayFallbackEnabled() else { return }
guard var components = URLComponents(
url: RelayFuelProvider().baseURL.appendingPathComponent("api/v1/widget-diag"),
resolvingAgainstBaseURL: false
) else { return }
components.queryItems = [
URLQueryItem(name: "intent", value: intentType),
URLQueryItem(name: "source", value: d.locationSource),
URLQueryItem(name: "n", value: String(d.stations.count)),
URLQueryItem(name: "fuel", value: d.fuel.rawValue),
URLQueryItem(name: "sort", value: d.sort.rawValue),
URLQueryItem(name: "fav", value: d.isFavourites ? "1" : "0"),
URLQueryItem(name: "station", value: first?.name ?? ""),
URLQueryItem(name: "price", value: String(first?.prices[d.fuel] ?? -1)),
]
guard let url = components.url else { return }
let request = RelayFuelProvider.relayRequest(url, client: "widget", timeout: 2)
Task {
_ = try? await URLSession.shared.data(for: request)
}
// Local app-group diagnostics only. No network beacon is sent: the
// App Store build must not contain a private LAN relay fallback.
}
private func makeEntryCore(configuration: Configuration) async -> FuelPriceEntry {
@@ -201,7 +179,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
// Favourites tab drag-to-reorder). Not radius-bound — a favourite
// in Edinburgh shows on a widget in London. Prices come from the
// keychain snapshot (the app refreshes favourite prices into
// keychain after every fetch), or fresher from a focused relay
// keychain after every fetch), or fresher from a focused mirror
// fetch when a favourite happens to be in range. The small face
// renders the FIRST of this list — the user's top favourite for
// the chosen fuel. (The old per-widget pinned-favourite picker
@@ -223,7 +201,7 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
ordered = refreshed
} else {
// 3a) 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
// public mirror 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.
@@ -289,65 +267,21 @@ struct FuelPriceTimelineProvider<Configuration: WidgetConfigurationIntent & Widg
)
}
/// One focused fetch around the widget's location. GitHub mirror FIRST —
/// HTTPS, works anywhere, and the app-group day-cache usually makes it a
/// local decode rather than a 2.8 MB download. The LAN relay only joins
/// behind the dev flag (fuelboard.relayFallback); consumers must never
/// attempt local-network access. Returns nil on any failure so callers
/// fall back to cache/placeholder.
/// One focused fetch around the widget's location. Public HTTPS mirror only;
/// no private LAN fallback is attempted. Returns nil on any failure so
/// callers fall back to cache/placeholder.
private static func fetchFocused(near location: Coordinate, fuel: FuelType, radiusKM: Double) async -> [FuelStation]? {
if let stations = try? await MirrorFuelProvider().fetchStations(
near: location.lat, lng: location.lng, fuel: fuel, radiusKM: radiusKM) {
return stations
}
guard FuelStore.loadRelayFallbackEnabled() else { return nil }
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"),
]
let request = RelayFuelProvider.relayRequest(components.url!, client: "widget", timeout: 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
}
try? await MirrorFuelProvider().fetchStations(
near: location.lat, lng: location.lng, fuel: fuel, radiusKM: radiusKM)
}
/// Fuel-only UK-wide fetch (no lat/lng/radius) — used when the widget has
/// no location fix so the face shows real stations instead of sample
/// data. GitHub mirror first (full dump + local fuel filter), relay only
/// behind the dev flag. Same nil-on-failure semantics as fetchFocused.
/// no location fix so the face shows real stations instead of sample data.
/// Public HTTPS mirror only; same nil-on-failure semantics as fetchFocused.
private static func fetchFuelOnly(fuel: FuelType, limit: Int) async -> [FuelStation]? {
if let stations = try? await MirrorFuelProvider().fetchStations(
near: nil, lng: nil, fuel: fuel, radiusKM: nil) {
return Array(stations.filter { $0.prices[fuel] != nil }.prefix(limit))
}
guard FuelStore.loadRelayFallbackEnabled() else { return nil }
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)),
]
let request = RelayFuelProvider.relayRequest(components.url!, client: "widget", timeout: 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
}
guard let stations = try? await MirrorFuelProvider().fetchStations(
near: nil, lng: nil, fuel: fuel, radiusKM: nil) else { return nil }
return Array(stations.filter { $0.prices[fuel] != nil }.prefix(limit))
}
}