Author SHA1 Message Date
FuelBoard Contributor d73a64023f road distance: trigger at launch + foreground, fall back to saved location
Road-distance routing previously only fired once a fresh GPS fix arrived via
the location hook/onChange. A returning user on a cold launch waited for that
fix, and if onboarding/flag issues kept tracking from starting, distances
stayed straight-line. Now:
- refreshRoadDistancesIfNeeded() uses the last saved location when the live
  fix isn't set yet, so it can run immediately.
- It's called explicitly on launch (already-onboarded branch), on foreground
  activation, and on the existing location paths.
2026-08-20 12:56:16 +01:00
FuelBoard Contributor dc6d1fd724 Merge branch 'fix/onboarding-persistence' into test/road-onboarding 2026-08-20 12:50:38 +01:00
FuelBoard Contributor 10e3b54d0c onboarding: persist completed flag keychain-first so it doesn't re-show
The completed flag lived only in app-group UserDefaults
(UserDefaults(suiteName: appGroupSuite)). On free SideStore accounts the
app-group container isn't provisioned, so the suite read as nil: save was a
silent no-op and load always returned false -> onboarding re-appeared on
every launch even after finishing and granting all permissions.

Switch to saveString/loadString (keychain-first, app-group mirror) under
onboardingCompletedKey, matching favourites/distance-unit persistence which
survives reinstall and works without a provisioning group. loadString falls
back to the app-group mirror, so anyone who previously completed on a
provisioned group keeps their flag; users on unprovisioned groups complete
once more and then it sticks.
2026-08-20 12:27:09 +01:00
2 changed files with 19 additions and 8 deletions
+11 -4
View File
@@ -56,14 +56,16 @@ struct ContentView: View {
/// Kicks off a (throttled) Apple-Maps road-distance recompute for the /// Kicks off a (throttled) Apple-Maps road-distance recompute for the
/// stations around the current fix. The app owns routing — the widget and /// stations around the current fix. The app owns routing — the widget and
/// Live Activity only read the cached result. /// Live Activity only read the cached result. Falls back to the last saved
/// location so it can run before the first fresh GPS fix arrives.
private func refreshRoadDistancesIfNeeded() { private func refreshRoadDistancesIfNeeded() {
guard let location else { return } let origin = location ?? FuelStore.loadLocation()
guard let origin else { return }
Task { Task {
await RoadDistanceService.refreshIfNeeded( await RoadDistanceService.refreshIfNeeded(
stations: stations, stations: stations,
lat: location.lat, lat: origin.lat,
lng: location.lng lng: origin.lng
) )
} }
} }
@@ -332,6 +334,10 @@ struct ContentView: View {
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM) fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
monitor.setEnabled(alertsEnabled) monitor.setEnabled(alertsEnabled)
updateLiveActivity() updateLiveActivity()
// Compute road distances early (throttled; falls back to the
// last saved location) so distance surfaces are road-matched
// as soon as stations are available.
refreshRoadDistancesIfNeeded()
// Refresh only when the cache is stale (twice-a-day policy). // Refresh only when the cache is stale (twice-a-day policy).
// Skipped under the force-* hooks so the banner stays up. // Skipped under the force-* hooks so the banner stays up.
#if DEBUG #if DEBUG
@@ -377,6 +383,7 @@ struct ContentView: View {
monitor.update(stations: stations, favourites: refreshedFavourites, monitor.update(stations: stations, favourites: refreshedFavourites,
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM) fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
updateLiveActivity() updateLiveActivity()
refreshRoadDistancesIfNeeded()
// No network fetch on foreground — pull-to-refresh is the override. // No network fetch on foreground — pull-to-refresh is the override.
} else { } else {
locationManager.stopForegroundTracking() locationManager.stopForegroundTracking()
+8 -4
View File
@@ -934,15 +934,19 @@ struct FuelStore {
} }
// MARK: Onboarding — the app shows the intro screen on first launch only // MARK: Onboarding — the app shows the intro screen on first launch only
// (a test button in the Alerts tab re-opens it). Stored in the app group // (a test button in the Alerts tab re-opens it). Stored KEYCHAIN-FIRST
// so the widget can see it too if ever needed. // (with an app-group mirror) for the same reason as favourites/distance
// unit: free SideStore accounts don't provision the app-group container,
// so an app-group-only flag silently fails to save AND reloads as false,
// making onboarding re-appear on every launch. Keychain survives reinstall
// and is shared with the extension.
static func loadHasCompletedOnboarding() -> Bool { static func loadHasCompletedOnboarding() -> Bool {
UserDefaults(suiteName: appGroupSuite)?.bool(forKey: onboardingCompletedKey) ?? false (loadString(service: onboardingCompletedKey) ?? "0") == "1"
} }
static func saveHasCompletedOnboarding(_ completed: Bool) { static func saveHasCompletedOnboarding(_ completed: Bool) {
UserDefaults(suiteName: appGroupSuite)?.set(completed, forKey: onboardingCompletedKey) saveString(completed ? "1" : "0", service: onboardingCompletedKey)
} }
// MARK: Road distances (Apple-Maps-matched, computed by the app) // MARK: Road distances (Apple-Maps-matched, computed by the app)