feat(api): publish the setup state on the account payload, and read it

Additive: `onboarding` carries the nine steps with their status, the answers as
given, the flow's own completion stamp and the one task it offered. Always
present — a record that never entered the flow reads as empty answers with nine
pending steps, so a client branches on the statuses rather than on whether the
field exists, and an unknown step id is skipped rather than refused.

It is resolved for the CALLER's own record rather than for the active one,
unlike the two module fields beside it. The three write routes are actor-only,
and publishing a setup state the browser cannot write would be a read it could
act on and never complete.

Both ends land here, because a field with no reader is the failure this
repository keeps rediscovering. The getting-started checklist orders its rows
from the answers — medication first for somebody who takes one daily, the
data-source row first for somebody who named a wearable — and it stays on screen
while the flow is unfinished. That is the second half: five readings were never
evidence that the setup finished, and now a pending step, an unconfirmed set of
answers or an offered task that produced nothing keeps the list where it is.

The ordering only applies once the answers are confirmed. Re-ordering the
dashboard under somebody who is still mid-question would be movement they did
not ask for.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 11:03:47 +02:00
1 parent 021ea8f1ff
commit dbfad0f28e
7 files changed
+852 -35

No files matched your search

+17
View File
@@ -23,6 +23,7 @@ import { isDateFormatPreference, storeDateFormat } from "@/lib/date-format";
import { storeTimezone } from "@/lib/timezone-mirror";
import type { ModuleKey } from "@/lib/modules/registry";
import type { ModuleAccessState } from "@/lib/sharing/module-disclosure";
import type { OnboardingStateDto } from "@/lib/onboarding/needs";
import type { TourProgress } from "@/lib/onboarding/tour-progress";
import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister";
import {
@@ -81,6 +82,18 @@ export interface AuthUser {
* `onboardingTourCompleted` boolean, which stays the auto-launch gate.
*/
onboardingTourProgress: AuthTourProgress | null;
/**
* v1.39 (C1) — the needs-based setup flow's state for this record: the nine
* ordered steps with their `pending | done | skipped` status, the answers as
* given, the flow's own completion stamp (distinct from
* `onboardingCompletedAt` above, which stays the first-run redirect's gate)
* and the one task the flow offered.
*
* Optional in the type so a stale /me payload — an older server image, or a
* test fixture written before the field existed — coerces to "no flow" in
* the mapper below rather than failing the shape.
*/
onboarding?: OnboardingStateDto | null;
/**
* v1.18.6 (DISC-02) — ISO timestamp of the one-time medical-disclaimer
* acknowledgment, or null when never acknowledged. The onboarding welcome
@@ -386,6 +399,10 @@ export async function fetchMe(): Promise<AuthUser> {
// v1.18.6 — coerce against a stale /me payload (older server image
// without the field) to null so the tour starts from the top.
onboardingTourProgress: data.onboardingTourProgress ?? null,
// v1.39 (C1) — coerce against a stale /me payload (an older server image
// without the field) to null, which every reader treats as "no flow was
// ever run" rather than as an unfinished one.
onboarding: data.onboarding ?? null,
// v1.7.0 — coerce against a stale /me payload (older server image
// without the field) so the display defaults to metric.
unitPreference: data.unitPreference === "imperial" ? "imperial" : "metric",