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:
@@ -321,7 +321,7 @@ struct ContentView: View {
|
|||||||
#else
|
#else
|
||||||
let shouldSkipOnboarding = false
|
let shouldSkipOnboarding = false
|
||||||
#endif
|
#endif
|
||||||
if FuelStore.loadHasCompletedOnboarding()
|
if !FuelStore.shouldShowOnboarding()
|
||||||
|| shouldSkipOnboarding {
|
|| shouldSkipOnboarding {
|
||||||
locationManager.startForegroundTracking()
|
locationManager.startForegroundTracking()
|
||||||
// Geofences and the Live Activity must follow the user even in
|
// Geofences and the Live Activity must follow the user even in
|
||||||
|
|||||||
@@ -671,3 +671,15 @@ final class RoadDistanceCacheTests: XCTestCase {
|
|||||||
XCTAssertEqual(km, s.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001)
|
XCTAssertEqual(km, s.distanceKM(to: 51.6, lng2: -0.1), accuracy: 0.0001)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Install identity
|
||||||
|
|
||||||
|
final class InstallIdentityTests: XCTestCase {
|
||||||
|
func testFreshInstallIsStableAfterFirstCall() {
|
||||||
|
// The first call seeds the common install id (local == keychain), so
|
||||||
|
// any subsequent call in the same process must report NOT-fresh. This
|
||||||
|
// holds regardless of persisted keychain/defaults state from prior runs.
|
||||||
|
_ = FuelStore.isFreshInstall()
|
||||||
|
XCTAssertFalse(FuelStore.isFreshInstall())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -949,6 +949,43 @@ struct FuelStore {
|
|||||||
saveString(completed ? "1" : "0", service: onboardingCompletedKey)
|
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)
|
// MARK: Road distances (Apple-Maps-matched, computed by the app)
|
||||||
|
|
||||||
/// Cached road/routed distances (metres) keyed by station ID, computed by
|
/// Cached road/routed distances (metres) keyed by station ID, computed by
|
||||||
|
|||||||
Reference in New Issue
Block a user