Best Value: announce the page to existing users, once per announcement version
Users who finished onboarding before Best Value existed are "completed, not fresh" forever, so the first-run gate never fires again for them and the feature stayed invisible unless they went looking in Settings. They now get the Best Value page on its own, once per announcement version — deliberately NOT a replay of the onboarding flow, which would re-run permission pages they answered long ago and has no skip. - FuelStore: "fuelboard.bestValueIntroVersion", stored through the same saveString/loadString path as the onboarding flag and compared against a CODE-SIDE "currentBestValueIntroVersion". Deliberately not CFBundleShortVersionString: keying on the bundle version would re-announce on every bugfix release. Bump the constant to re-announce on purpose. - FuelStore.shouldAnnounceBestValue(settings:hasSeen:onboardingDue:isSuppressed:) holds the rule, with every fact injectable so it is unit-tested instead of being driven through a simulator. It declines when onboarding is due or on screen, when this announcement version has been seen, and when the user has already configured Best Value themselves. - BestValueAnnouncementHost wraps the app's content and owns the presentation state, so ContentView's change is a single wrapper and the whole feature lives in the new file. It reloads the stored settings on dismiss, because the page writes straight to the store rather than through ContentView's binding. - The page is now ONE view, BestValueIntroContent, used by first-run onboarding (page 5 of 6) and by the announcement, so the copy, the sliders and the "declining is a decision" rule cannot drift. OnboardingView drops its private duplicate of the page, its bindings and its read-outs, and finish() marks the announcement seen so a new user never sees the page twice. QA hooks (DEBUG): -showBestValueIntro forces the announcement, and -skipOnboarding suppresses it. Worth knowing now that a returning install announces on a PLAIN launch, so tab screenshots need -skipOnboarding. Verified on the simulator (OCR of the rendered screens): a plain launch of an install that has completed onboarding shows the single announcement page — Best Value title, explanation, "Turn on Best Value", the "Settings, under Fuel Savings" note and a "Not now" button, with no page dots; -skipOnboarding shows no cover at all; -showBestValueIntro forces it. 180 tests, 0 failures (8 new).
This commit is contained in:
@@ -1206,6 +1206,60 @@ struct FuelStore {
|
||||
return !loadHasCompletedOnboarding() || fresh
|
||||
}
|
||||
|
||||
// MARK: Best Value announcement (existing users who updated into it)
|
||||
|
||||
/// Users who finished onboarding before Best Value existed are
|
||||
/// `completed == true, fresh == false` for good, so the first-run gate above
|
||||
/// never fires again for them and the feature would stay invisible unless
|
||||
/// they went looking in Settings. This flag drives a single-page
|
||||
/// announcement instead of replaying the whole flow (which would re-run
|
||||
/// permission pages they have already answered).
|
||||
///
|
||||
/// Stores the announcement version SEEN, compared against
|
||||
/// `currentBestValueIntroVersion` below — deliberately not the app version,
|
||||
/// so a bugfix release never re-announces anything.
|
||||
static let bestValueIntroVersionKey = "fuelboard.bestValueIntroVersion"
|
||||
|
||||
/// Bump this when the Best Value announcement should be shown again to
|
||||
/// everyone — e.g. the feature changes in a way worth re-explaining. Anyone
|
||||
/// whose stored value differs (including someone who dismissed an earlier
|
||||
/// announcement) sees it once more on the next launch.
|
||||
static let currentBestValueIntroVersion = 1
|
||||
|
||||
static func hasSeenBestValueIntro() -> Bool {
|
||||
(loadString(service: bestValueIntroVersionKey) ?? "")
|
||||
== String(currentBestValueIntroVersion)
|
||||
}
|
||||
|
||||
static func markBestValueIntroSeen() {
|
||||
saveString(String(currentBestValueIntroVersion), service: bestValueIntroVersionKey)
|
||||
}
|
||||
|
||||
/// Whether the standalone Best Value announcement should be offered.
|
||||
///
|
||||
/// The rule lives here rather than inside the view so it can be unit-tested:
|
||||
/// a view-embedded gate can only be checked by driving a simulator through
|
||||
/// every combination, which is exactly how a rule like this rots. The facts
|
||||
/// it depends on are injectable for the same reason.
|
||||
///
|
||||
/// It declines in four cases, each for a different reason:
|
||||
/// - `isSuppressed` — the first-run onboarding cover is up and must not be
|
||||
/// fought for the screen;
|
||||
/// - `onboardingDue` — a new install is about to run the full flow, which
|
||||
/// already contains this page (`OnboardingView.finish` also marks it seen);
|
||||
/// - `hasSeen` — this announcement version has already been shown;
|
||||
/// - the user has Best Value configured — never announce to someone who set
|
||||
/// it up themselves.
|
||||
static func shouldAnnounceBestValue(settings: FuelSavingsSettings,
|
||||
hasSeen: Bool = hasSeenBestValueIntro(),
|
||||
onboardingDue: Bool = shouldShowOnboarding(),
|
||||
isSuppressed: Bool = false) -> Bool {
|
||||
guard !isSuppressed else { return false }
|
||||
guard !onboardingDue else { return false }
|
||||
guard !hasSeen else { return false }
|
||||
return !settings.isConfigured
|
||||
}
|
||||
|
||||
// MARK: Road distances (Apple-Maps-matched, computed by the app)
|
||||
|
||||
/// Cached road/routed distances (metres) keyed by station ID, computed by
|
||||
|
||||
Reference in New Issue
Block a user