Add the setup flow's confirm screen

What the answers switch on is read off the registry's derivation and
never computed here, so the list the person confirms is the list the
completion route writes. Units, language and time zone are shown once
beside it, changeable in Settings, never asked again.

The profile half keeps the baseline form and its per-field refusals:
the fields are seeded from the account so a value it holds is confirmed
rather than typed again, and a refused field still stops the flow. For
"someone I look after" the managed-profile form takes the profile's
place, because that record is created here, after the questions; an
account without a second factor can finish without it and create it
later under Settings.

Confirming posts the completion, passes the first-result step outright
when there is no task to offer, and goes where the step machine says.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 15:19:48 +02:00
1 parent 7b279aec72
commit 58bc91a0a3
8 files changed
+459 -57

No files matched your search

@@ -0,0 +1,68 @@
/**
* v1.39 (C2) — the confirm screen's list is the registry's derivation and
* nothing else.
*/
import { describe, expect, it } from "vitest";
import {
deriveOnboardingModuleDefaults,
ONBOARDING_ALWAYS_ON_MODULES,
OWNED_MODULE_KEYS,
} from "@/lib/modules/registry";
import { confirmedModules } from "../confirm-summary";
import { emptyOnboardingNeeds } from "../needs";
describe("confirmedModules", () => {
it("lists exactly what the derivation switches on, the always-on trio aside", () => {
const needs = {
...emptyOnboardingNeeds(),
recordTarget: "me" as const,
areas: ["glucose" as const, "sleep" as const],
medication: "yes" as const,
visit: "within-a-month" as const,
};
const derived = deriveOnboardingModuleDefaults(needs);
const expected = OWNED_MODULE_KEYS.filter(
(key) =>
derived.preferences[key] && !ONBOARDING_ALWAYS_ON_MODULES.includes(key),
);
const { chosen, alwaysOn } = confirmedModules(needs);
expect(chosen).toEqual(expected);
expect(chosen).toContain("medications");
expect(chosen).toContain("doctorReport");
// Registry order, so the two lists read in the same order everywhere.
expect(alwaysOn).toEqual(
OWNED_MODULE_KEYS.filter((key) =>
ONBOARDING_ALWAYS_ON_MODULES.includes(key),
),
);
});
it("names cycle when the area was ticked, though the map never owns it", () => {
const { chosen } = confirmedModules({
...emptyOnboardingNeeds(),
recordTarget: "me",
areas: ["cycle"],
});
expect(chosen).toContain("cycle");
});
it("reads a skipped question as the route does — nothing switched on for it", () => {
const { chosen } = confirmedModules({
...emptyOnboardingNeeds(),
recordTarget: "me",
});
expect(chosen).not.toContain("medications");
expect(chosen).not.toContain("doctorReport");
});
it("switches on the immunization log for a record somebody else runs", () => {
expect(
confirmedModules({
...emptyOnboardingNeeds(),
recordTarget: "someone-else",
}).chosen,
).toContain("vaccinations");
});
});
+52
View File
@@ -0,0 +1,52 @@
/**
* v1.39 (C2) — what the confirm screen says is switched on.
*
* A READ of the registry's derivation, never a second mapping: the list the
* person confirms is `deriveOnboardingModuleDefaults` applied to the same
* answers `POST /api/onboarding/complete` will apply it to, with the same
* conservative defaults for a skipped question. A wizard that computed its
* own list would drift from the one the route actually writes, and the
* confirm screen would then be confirming something else.
*
* What the route writes on top of this — the merge that never retracts a
* module switched on by hand or one holding data — only ever ADDS to the
* list, which is why the sentence beside it says "everything else stays
* under Settings" rather than "is off".
*/
import {
deriveOnboardingModuleDefaults,
MODULE_KEYS,
ONBOARDING_ALWAYS_ON_MODULES,
type ModuleKey,
} from "@/lib/modules/registry";
import type { OnboardingNeeds } from "./needs";
export interface ConfirmedModules {
/** What the answers switch on, in registry order — the always-on trio aside. */
chosen: ModuleKey[];
/** On whatever the answers say. */
alwaysOn: ModuleKey[];
}
export function confirmedModules(needs: OnboardingNeeds): ConfirmedModules {
const derived = deriveOnboardingModuleDefaults({
// Q1 is required and a confirm screen is never reached without it; the
// fallback only keeps the type total.
recordTarget: needs.recordTarget ?? "me",
areas: needs.areas,
// The same defaults the route applies to a skipped question.
medication: needs.medication ?? "no",
visit: needs.visit ?? "no",
});
const on = new Set<ModuleKey>();
for (const [key, value] of Object.entries(derived.preferences)) {
if (value) on.add(key as ModuleKey);
}
if (derived.cycleTracking) on.add("cycle");
const alwaysOn = new Set<ModuleKey>(ONBOARDING_ALWAYS_ON_MODULES);
return {
chosen: MODULE_KEYS.filter((key) => on.has(key) && !alwaysOn.has(key)),
alwaysOn: MODULE_KEYS.filter((key) => alwaysOn.has(key)),
};
}