Widget distance menu: the Distance picker only makes sense for
Cheapest ordering. It was hidden for Favourites but Closest still
showed it (regressed when the per-widget Distance picker was added).
parameterSummary now nests When clauses: Distance visible for
Cheapest only; Closest and Favourites hide it.
Notifications: three compounding defects kept real geofence alerts
from ever firing while the app was suspended:
1. Always permission was never properly requested. requestPermissions
fired WhenInUse and Always back-to-back; iOS ignores the second
call while the first prompt is pending, leaving the app stuck on
WhenInUse — and region entries are never delivered in the
background. Added locationManagerDidChangeAuthorization to
ProximityMonitor: escalate WhenInUse -> Always, and re-register
geofences on grant (regions registered under WhenInUse-only won't
deliver in the background).
2. The 18-region window was frozen in the background. Re-registration
lived in SwiftUI .onChange(of: locationManager.current), which
never runs while suspended. Added a delegate hook
(LocationManager.onLocationUpdate) fired from didUpdateLocations on
every fix including background significant-change wake-ups; the app
wires it to re-register geofences around the new position.
3. Region-registration failures were invisible. monitoringDidFailFor
was never implemented, so a failed startMonitoring (region budget,
auth, radius) silently stopped alerts. Now surfaced as
monitor.lastRegionError and shown in Settings -> Debug; cleared on
the next successful registration.
1. CarPlay widget taps could never launch Maps — FuelBoard is not a
CarPlay app, and Apple only lets a widget launch its own app in
CarPlay when that app is CarPlay-enabled. Marked the widget as a
disfavored CarPlay location (iOS 26+; no-op below): read-only in
the car, no dead tap. Header comment updated to match reality.
2. Widgets now track the user's location: timeline refresh 15m -> 5m,
and the app's location manager reloads all widget timelines after
every significant move (250m filter, throttled to 60s), so the
widget re-orders around the new position while driving instead of
waiting for the next tick.
3. Debug 'Cheapest in range' no longer shows the ALERT prediction
(alertsFuel + alert radius — a different pool by design). It now
mirrors the app list exactly: the TOP-badge station for the
selected fuel within the stationLimit radius (new DebugAppCheapest
computed in ContentView, passed to Settings). Alert-prediction
cheapest kept as fallback when the app view hasn't resolved.
The 3-row cap was hardcoded for the medium widget height; large widgets
have ~2x the height and fit 5 compact rows. Timeline now carries up to
8 stations (was 4) so the large list never starves. Distance picker
still controls which stations are in range; the visible row count now
depends on widget size, not the distance setting.
- New Sort by value 'Favourites' in Edit Widget (per-widget config).
- ParameterSummary hides the Distance picker when Favourites is selected
(favourites are not radius-bound).
- Favourites mode reads keychain favourites (shared access group works on
SideStore free even without App Groups), filters to the widget's fuel,
refreshes prices from a focused relay fetch when in range, and ranks
cheapest-first with distance tiebreak. Not radius-filtered.
- App now re-saves refreshed favourites to keychain after every fetch so
the widget shows current prices instead of star-time snapshots.
- Empty state + headings adapt to favourites mode. 35 tests pass.
- The widget now uses the native maps://?daddr= scheme for taps in both
sizes. In CarPlay there is no containing app to relay through, but since
Apple Maps IS a CarPlay app, the system may route the tap straight to
Maps on the car display (WWDC25: widgets can launch apps in CarPlay).
- iOS keeps working via onOpenURL: the app now forwards maps:// URLs (if
the Home Screen delivers them to the app), plus legacy fuelboard:// and
http://maps.apple.com links from older cached timelines.
- No public API exists for a widget to detect CarPlay context, so the
single maps:// link serves both; the app-side forward is the iOS safety
net. 35 tests pass.
- WidgetKit widgets can only open their containing app — the Link URL is
delivered to FuelBoard via onOpenURL, never opened directly in Maps. The
widget now links to a registered fuelboard://directions?lat=&lng= URL,
and FuelBoardApp forwards it to native maps://?daddr= directions.
(http://maps.apple.com and raw maps:// both silently dropped in-app.)
- Widget rows (small + medium) now show the full station name (station.name)
instead of just the brand.
- Onboarding network page: removed the 'Check again' secondary screen after
the Local Network prompt — Open Settings remains the only retry path.
35 tests pass.
- Widget tap-to-directions now uses the native maps:// scheme instead of
http://maps.apple.com. The universal-link http URL failed to open from a
widget and fell back to launching the containing app; maps:// opens
Apple Maps directly for both small (whole widget) and medium (per row).
- Onboarding: removed the Skip button (mandatory flow), and system prompts
(location, notifications, local network) now fire only when the user taps
Continue/Allow on each page — not when the page appears. Pages
auto-advance once a permission is granted.
- After 'Start Using FuelBoard' the first data fetch is forced, so a
reinstall that leaves a recent lastRefresh in app-group defaults can no
longer skip the fetch and leave the station list empty. 35 tests pass.
- New Distance picker in Edit Widget (5/10/15 miles), independent of the
app's distance filter; used for both the relay fetch radius and the
strict in-widget filter.
- Brand text is now title-cased via sanitizedStationTitle (relay sends
all-caps brands like SHELL/TESCO/MORRISONS).
- Prices now show as £1.399 (monospaced) matching the app, instead of
139.9p.
- Directions deep-link (mapsDirectionsURL) already per-row on medium and
whole-widget on small; unchanged. 35 tests pass.
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.
FuelPriceWidget now uses AppIntentConfiguration with a per-widget
FuelBoardWidgetConfigurationIntent (fuel + sort). Each widget placed on
the home screen carries its OWN fuel type (Unleaded/Premium/Diesel) and
ordering (Cheapest/Closest), edited by long-press -> Edit Widget —
independent of the app's selection, so multiple widgets can show
different data side by side. Widget no longer reads the app's selected
fuel. 35 tests pass.
- FuelType labels: 'Unleaded (E10)'/'Premium (E5)' -> 'Unleaded'/'Premium'
- New DistanceUnit (miles/km) preference; all app + widget distances,
radii and alert text convert/display in the chosen unit
- Settings tab (4th tab): segmented miles/km picker, 'Show introduction'
replay button (onboarding moved out of Alerts tab), StoreKit tip £4.99
- ProximityMonitor + widget notifications use the chosen unit
- Tests: label expectations updated, DistanceUnit conversion + format tests