onboarding: replay on a fresh/reinstalled install, not every launch

The keychain-first completed flag persists across reinstalls (same bundle id),
so a new install was treated as already-completed and onboarding silently
skipped while permissions still prompted. Add install identity:
- Store an install id in BOTH the app-own container defaults (wiped on
  reinstall, persists across launches) and keychain (survives reinstall).
- isFreshInstall() is true on first-ever launch or when the local id is
  missing/different from keychain (i.e. reinstalled); it seeds a fresh common
  id so the next launch of the same install is not 'fresh' again.
- shouldShowOnboarding() = not completed || fresh install -> onboarding now
  replays once per fresh/reinstall, never on ordinary launches.
Works on free SideStore (no app-group), using the app's own defaults domain
as the same-install signal.
This commit is contained in:
FuelBoard Contributor
2026-08-20 13:12:53 +01:00
parent d73a64023f
commit 01517ba687
3 changed files with 50 additions and 1 deletions
+37
View File
@@ -949,6 +949,43 @@ struct FuelStore {
saveString(completed ? "1" : "0", service: onboardingCompletedKey)
}
// MARK: Install identity distinguish a genuinely fresh install (where
// onboarding should replay) from later launches of the same install.
/// Keychain copy of the install id (survives reinstall).
private static let installIDKey = "fuelboard.installID"
/// Local (app-own container) copy wiped on reinstall, persists across
/// normal launches. Free SideStore accounts have no app-group container,
/// so this app-own defaults domain is the reliable "same install" signal.
private static let localInstallIDKey = "fuelboard.installID.local"
/// True when this is a first-ever install OR the app was just reinstalled
/// (local install id missing/different from the keychain id). Seeds a fresh
/// id into both stores so the next launch within the same install is not a
/// "fresh install" any more.
static func isFreshInstall() -> Bool {
let local = UserDefaults.standard.string(forKey: localInstallIDKey)
let remote = loadString(service: installIDKey)
if let local, let remote, local == remote { return false }
// Fresh / mismatched install (or first launch). Pattern a common id so
// subsequent launches of this install are recognised as the same one.
let id = UUID().uuidString
UserDefaults.standard.set(id, forKey: localInstallIDKey)
saveString(id, service: installIDKey)
return true
}
/// Onboarding should present when it hasn't been completed in this install
/// OR this is a freshly-installed app (so the walkthrough replays on a new
/// install, e.g. after a SideStore reinstall, without re-showing on every
/// ordinary launch). The install check always runs (it seeds the id) rather
/// than short-circuiting, so a brand-new install is recorded before the
/// user ever fills in onboarding.
static func shouldShowOnboarding() -> Bool {
let fresh = isFreshInstall()
return !loadHasCompletedOnboarding() || fresh
}
// MARK: Road distances (Apple-Maps-matched, computed by the app)
/// Cached road/routed distances (metres) keyed by station ID, computed by