diff --git a/messages/de.json b/messages/de.json
index bc23d2f08..a7c65acce 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "Immer an: {modules} und die Kernwerte (Gewicht, Blutdruck, Puls).",
"everythingElse": "Alles andere bleibt einen Klick entfernt unter",
"modulesLink": "Einstellungen, Module",
+ "switchesOff": "Nicht mehr in der Navigation: {modules}. Jederzeit unter Einstellungen, Module wieder einschaltbar.",
"glucoseUnit": "Blutzucker",
"unitPreference": "Einheiten",
"language": "Sprache",
diff --git a/messages/en.json b/messages/en.json
index dc66aa61b..158f6180d 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "Always on: {modules}, and the core readings (weight, blood pressure, pulse).",
"everythingElse": "Everything else stays one click away under",
"modulesLink": "Settings, Modules",
+ "switchesOff": "Out of the navigation: {modules}. You can switch any of them back on under Settings, Modules.",
"glucoseUnit": "Blood glucose",
"unitPreference": "Units",
"language": "Language",
diff --git a/messages/es.json b/messages/es.json
index e5fddd66f..e631287b2 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "Siempre activos: {modules} y las mediciones básicas (peso, presión arterial, pulso).",
"everythingElse": "Todo lo demás queda a un clic en",
"modulesLink": "Ajustes, Módulos",
+ "switchesOff": "Fuera de la navegación: {modules}. Puedes volver a activarlos cuando quieras en Ajustes, Módulos.",
"glucoseUnit": "Glucosa en sangre",
"unitPreference": "Unidades",
"language": "Idioma",
diff --git a/messages/fr.json b/messages/fr.json
index 1640aa7e9..4ea236315 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "Toujours actifs : {modules}, et les mesures de base (poids, tension, pouls).",
"everythingElse": "Tout le reste reste à un clic dans",
"modulesLink": "Réglages, Modules",
+ "switchesOff": "Hors de la navigation : {modules}. Tu peux les réactiver à tout moment dans Réglages, Modules.",
"glucoseUnit": "Glycémie",
"unitPreference": "Unités",
"language": "Langue",
diff --git a/messages/it.json b/messages/it.json
index d965b3827..647b502a0 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "Sempre attivi: {modules} e le misurazioni di base (peso, pressione, polso).",
"everythingElse": "Tutto il resto resta a un clic in",
"modulesLink": "Impostazioni, Moduli",
+ "switchesOff": "Fuori dalla navigazione: {modules}. Puoi riattivarli quando vuoi in Impostazioni, Moduli.",
"glucoseUnit": "Glicemia",
"unitPreference": "Unità",
"language": "Lingua",
diff --git a/messages/ko.json b/messages/ko.json
index 4dabc847b..2f6d640e2 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "항상 켜짐: {modules}, 그리고 기본 측정값(체중, 혈압, 맥박).",
"everythingElse": "나머지는 한 번의 클릭으로 열 수 있습니다:",
"modulesLink": "설정, 모듈",
+ "switchesOff": "탐색에서 빠집니다: {modules}. 설정, 모듈에서 언제든 다시 켤 수 있습니다.",
"glucoseUnit": "혈당",
"unitPreference": "단위",
"language": "언어",
diff --git a/messages/pl.json b/messages/pl.json
index 9769c0838..972235a6a 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -4661,6 +4661,7 @@
"alwaysOn": "Zawsze włączone: {modules} oraz podstawowe pomiary (waga, ciśnienie, tętno).",
"everythingElse": "Cała reszta jest o jedno kliknięcie w",
"modulesLink": "Ustawienia, Moduły",
+ "switchesOff": "Znika z nawigacji: {modules}. W każdej chwili możesz je włączyć w Ustawienia, Moduły.",
"glucoseUnit": "Glukoza we krwi",
"unitPreference": "Jednostki",
"language": "Język",
diff --git a/src/components/onboarding/__tests__/confirm-screen.test.tsx b/src/components/onboarding/__tests__/confirm-screen.test.tsx
new file mode 100644
index 000000000..bef1dc2ae
--- /dev/null
+++ b/src/components/onboarding/__tests__/confirm-screen.test.tsx
@@ -0,0 +1,86 @@
+/**
+ * v1.39 (Wave C) — what the confirm screen is allowed to claim.
+ *
+ * C4 (research I6): the screen listed what the answers switch ON and said
+ * everything else "stays one click away under Settings, Modules" — true of
+ * reachability, false of the navigation entry. A module the answers do not
+ * ask for leaves the navigation, and nothing said so.
+ *
+ * Rendered server-side, this repo's component-test convention.
+ */
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it, vi } from "vitest";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import type { AuthUser } from "@/hooks/use-auth";
+import {
+ defaultOnboardingSteps,
+ emptyOnboardingNeeds,
+ type OnboardingStateDto,
+} from "@/lib/onboarding/needs";
+
+let currentUser: AuthUser | null = null;
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
+}));
+
+vi.mock("@/hooks/use-auth", () => ({
+ useAuth: () => ({ user: currentUser }),
+ useAccountOnceMounted: () => currentUser,
+}));
+
+const { ConfirmScreen } = await import("../confirm-screen");
+
+function state(
+ overrides: Partial
+ {t("onboarding.flow.confirm.switchesOff", { + modules: moduleNames(wouldSwitchOff), + })} +
+ ) : null}
{t("onboarding.flow.confirm.alwaysOn", {
modules: moduleNames(alwaysOn),
diff --git a/src/lib/onboarding/__tests__/confirm-summary.test.ts b/src/lib/onboarding/__tests__/confirm-summary.test.ts
index 3ced0603a..88acc03dd 100644
--- a/src/lib/onboarding/__tests__/confirm-summary.test.ts
+++ b/src/lib/onboarding/__tests__/confirm-summary.test.ts
@@ -66,3 +66,62 @@ describe("confirmedModules", () => {
).toContain("vaccinations");
});
});
+
+describe("confirmedModules — what leaves the navigation (I6)", () => {
+ const noMedication = {
+ ...emptyOnboardingNeeds(),
+ recordTarget: "me" as const,
+ areas: ["sleep" as const],
+ medication: "no" as const,
+ visit: "no" as const,
+ };
+
+ it("names a module the answers do not ask for and the record shows today", () => {
+ const { wouldSwitchOff } = confirmedModules(noMedication, {
+ medications: true,
+ doctorReport: true,
+ });
+ expect(wouldSwitchOff).toContain("medications");
+ expect(wouldSwitchOff).toContain("doctorReport");
+ });
+
+ it("never names a module the answers switch on", () => {
+ const { chosen, wouldSwitchOff } = confirmedModules(
+ { ...noMedication, medication: "yes" },
+ { medications: true },
+ );
+ expect(chosen).toContain("medications");
+ expect(wouldSwitchOff).not.toContain("medications");
+ });
+
+ it("never names a module the record does not show today", () => {
+ const { wouldSwitchOff } = confirmedModules(noMedication, {
+ medications: false,
+ });
+ expect(wouldSwitchOff).not.toContain("medications");
+ // Nothing is known about a module the payload does not carry, so the
+ // screen says nothing about it.
+ expect(confirmedModules(noMedication, {}).wouldSwitchOff).toEqual([]);
+ });
+
+ it("never names an always-on module, whatever the answers say", () => {
+ const current = Object.fromEntries(
+ ONBOARDING_ALWAYS_ON_MODULES.map((key) => [key, true]),
+ );
+ const { wouldSwitchOff } = confirmedModules(noMedication, current);
+ for (const key of ONBOARDING_ALWAYS_ON_MODULES) {
+ expect(wouldSwitchOff).not.toContain(key);
+ }
+ });
+
+ it("is in registry order, like the list beside it", () => {
+ const current = Object.fromEntries(
+ OWNED_MODULE_KEYS.map((key) => [key, true]),
+ );
+ const { wouldSwitchOff } = confirmedModules(noMedication, current);
+ expect(wouldSwitchOff).toEqual(
+ OWNED_MODULE_KEYS.filter((key) => wouldSwitchOff.includes(key)),
+ );
+ expect(wouldSwitchOff.length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/lib/onboarding/confirm-summary.ts b/src/lib/onboarding/confirm-summary.ts
index 1dfb28c9b..260b2faef 100644
--- a/src/lib/onboarding/confirm-summary.ts
+++ b/src/lib/onboarding/confirm-summary.ts
@@ -15,6 +15,7 @@
*/
import {
deriveOnboardingModuleDefaults,
+ mergeDerivedModulePreferences,
MODULE_KEYS,
ONBOARDING_ALWAYS_ON_MODULES,
type ModuleKey,
@@ -27,9 +28,24 @@ export interface ConfirmedModules {
chosen: ModuleKey[];
/** On whatever the answers say. */
alwaysOn: ModuleKey[];
+ /**
+ * v1.39 (Wave C, C4) — what the derivation takes OUT of the navigation:
+ * modules this record shows today that the answers do not ask for
+ * (research I6). Empty until the account payload has resolved, because a
+ * module the payload says nothing about is a module this screen knows
+ * nothing about.
+ */
+ wouldSwitchOff: ModuleKey[];
}
-export function confirmedModules(needs: OnboardingNeeds): ConfirmedModules {
+/**
+ * @param current the module map the record shows today (`user.modules`), so
+ * the screen names only modules that are actually in the navigation now.
+ */
+export function confirmedModules(
+ needs: OnboardingNeeds,
+ current: Partial