diff --git a/messages/en.json b/messages/en.json
index 68091e25d..5534da01c 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -4771,6 +4771,11 @@
"open": "Open {target}",
"markDone": "I have made the entry"
}
+ },
+ "done": {
+ "tourCta": "Take a short tour",
+ "openRecord": "Open the record of {name}",
+ "createProfile": "Create the profile for the person you look after"
}
}
},
diff --git a/src/app/onboarding/[step]/page.tsx b/src/app/onboarding/[step]/page.tsx
index 4dd9f08df..73f4e8efe 100644
--- a/src/app/onboarding/[step]/page.tsx
+++ b/src/app/onboarding/[step]/page.tsx
@@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation";
import { OnboardingShell } from "@/components/onboarding/onboarding-shell";
import { ConfirmScreen } from "@/components/onboarding/confirm-screen";
+import { DoneScreen } from "@/components/onboarding/done-screen";
import { FirstResultScreen } from "@/components/onboarding/first-result-screen";
import { QuestionScreen } from "@/components/onboarding/question-screen";
import { UnitsScreen } from "@/components/onboarding/units-screen";
@@ -106,5 +107,5 @@ function renderScreen(screen: OnboardingStepId, state: OnboardingStateDto) {
if (screen === "first-result") {
return ;
}
- notFound();
+ return ;
}
diff --git a/src/components/onboarding/__tests__/done-screen.test.tsx b/src/components/onboarding/__tests__/done-screen.test.tsx
index 72038c37b..628d87837 100644
--- a/src/components/onboarding/__tests__/done-screen.test.tsx
+++ b/src/components/onboarding/__tests__/done-screen.test.tsx
@@ -17,12 +17,22 @@ const aiProviderState = vi.hoisted(() => ({
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: aiProviderState.data }),
+ useMutation: () => ({ mutate: () => {}, isPending: false }),
+ useQueryClient: () => ({ invalidateQueries: async () => {} }),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: () => {}, replace: () => {} }),
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ user: { id: "u1" } }),
}));
+vi.mock("@/hooks/use-account-switch", () => ({
+ useAccountSwitch: () => ({ mutate: () => {}, isPending: false }),
+}));
+
vi.mock("@/components/onboarding/tour-launcher", () => ({
setTourReferrer: () => {},
}));
@@ -42,12 +52,28 @@ vi.mock("next/link", () => ({
),
}));
+import {
+ defaultOnboardingSteps,
+ emptyOnboardingNeeds,
+ type OnboardingStateDto,
+} from "@/lib/onboarding/needs";
import { DoneScreen } from "../done-screen";
-function render() {
+function state(
+ recordTarget: OnboardingStateDto["needs"]["recordTarget"] = "me",
+): OnboardingStateDto {
+ return {
+ steps: defaultOnboardingSteps(),
+ needs: { ...emptyOnboardingNeeds(), recordTarget },
+ completedAt: "2026-09-10T08:00:00.000Z",
+ firstResult: null,
+ };
+}
+
+function render(recordTarget?: OnboardingStateDto["needs"]["recordTarget"]) {
return renderToStaticMarkup(
-
+ ,
);
}
@@ -71,7 +97,18 @@ describe(" AI panel", () => {
const html = render();
expect(html).toContain('href="/settings/integrations"');
expect(html).toContain('href="/measurements"');
- expect(html).toContain('href="/"');
+ expect(html).toContain('data-slot="onboarding-open-dashboard"');
+ });
+
+ it("offers the tour rather than launching it", () => {
+ aiProviderState.data = undefined;
+ expect(render()).toContain('data-slot="onboarding-take-tour"');
+ });
+
+ it("offers the managed profile only to an answer that asked for it later", () => {
+ aiProviderState.data = undefined;
+ expect(render("both")).toContain('href="/settings/access"');
+ expect(render("me")).not.toContain('href="/settings/access"');
});
it("shows the shared-key note only when the operator key serves the user", () => {
diff --git a/src/components/onboarding/done-screen.tsx b/src/components/onboarding/done-screen.tsx
index bb8c56689..c4a2c43e2 100644
--- a/src/components/onboarding/done-screen.tsx
+++ b/src/components/onboarding/done-screen.tsx
@@ -1,25 +1,33 @@
"use client";
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import {
CheckCircle2,
+ Compass,
FileUp,
PlusCircle,
Plug,
Settings2,
Sparkles,
+ UserPlus,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
import { SampleBriefingCard } from "@/components/onboarding/sample-briefing-card";
+import { useOnboardingAnswer } from "@/components/onboarding/use-onboarding-flow";
import { useAuth } from "@/hooks/use-auth";
+import { useAccountSwitch } from "@/hooks/use-account-switch";
import { setTourReferrer } from "@/components/onboarding/tour-launcher";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
+import { markChecklistExpanded } from "@/lib/onboarding/checklist-storage";
+import type { OnboardingStateDto } from "@/lib/onboarding/needs";
+import { accountLabel } from "@/lib/sharing/account-access-view";
interface AiProviderStatus {
/** Origin of the provider that would serve this user, if any. */
@@ -27,33 +35,70 @@ interface AiProviderStatus {
}
/**
- * v1.4.25 W14b-Content — onboarding step 4 (done).
+ * v1.39 (C2) — the setup flow's exit.
*
- * Confirms the user has finished onboarding. `onboardingCompletedAt`
- * was flipped server-side by the baseline form's `POST step:4` write,
- * so this page is purely presentational — no further mutation, no
- * cookie flips, no API calls.
+ * The dashboard is one click away, already ordered, and the checklist opens
+ * on arrival (design spec §The screens, 5). The tour is an OFFER here rather
+ * than an automatic launch: pressing "Take a short tour" sets the referrer
+ * the shell-level `` reads, and nothing else does, so somebody
+ * who wants the dashboard gets the dashboard.
*
- * The single CTA returns the user to the dashboard. The link goes
- * through `next/link` so the regular client-side navigation kicks in
- * (the proxy redirect was cleared with the same step-API write that
- * landed the user here).
+ * Two record-shaped exits ride the answer to Q1: "both" is offered the
+ * profile for the person they look after, and "someone I look after" — whose
+ * profile was created on the confirm screen — is offered that record, through
+ * the same switch the account menu uses, so their first entry lands in the
+ * right record.
*
- * v1.18.6 — on mount we write the per-user wizard-return marker so the
- * shell-level `` auto-opens the module tour immediately
- * on the next dashboard arrival (sequenced after the post-wizard
- * grace), instead of waiting out the 24 h fallback. Whichever exit the
- * user picks from this screen, the next `/` mount finds the marker.
+ * The step is marked done on arrival, once. Everything else on this screen
+ * is presentational: the module map was derived on confirm, the cookie
+ * cleared there too.
+ *
+ * The AI panel leads with the payoff (a static, clearly-labelled SAMPLE
+ * briefing — no model call, no egress, no consent), then the honest
+ * local-first ladder and the "useful without AI" release valve. Value-first,
+ * never a gate: every exit below stays, and setup is a single optional
+ * deep-link.
*/
-export function DoneScreen() {
+export function DoneScreen({ state }: { state: OnboardingStateDto }) {
const { t } = useTranslations();
+ const router = useRouter();
const { user } = useAuth();
const [sampleOpen, setSampleOpen] = useState(false);
+ const answer = useOnboardingAnswer();
+ const accountSwitch = useAccountSwitch();
+ // Mark the step once. The write is idempotent, but a strict-mode double
+ // mount would still send it twice; the ref keeps it to one.
+ const marked = useRef(false);
+ const donePending =
+ state.steps.find((step) => step.id === "done")?.status === "pending";
useEffect(() => {
+ if (!donePending || marked.current) return;
+ marked.current = true;
+ answer.mutate({ step: "done" });
+ // `answer` is a fresh object every render; the mutation it wraps is not.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [donePending]);
+
+ const recordTarget = state.needs.recordTarget;
+ const managedRecord =
+ recordTarget === "someone-else"
+ ? (user?.accountAccess?.accounts.find(
+ (entry) => entry.recordKind === "managed",
+ ) ?? null)
+ : null;
+
+ function openDashboard() {
+ markChecklistExpanded();
+ router.push("/");
+ }
+
+ function takeTour() {
if (user?.id) setTourReferrer(user.id);
- }, [user?.id]);
+ markChecklistExpanded();
+ router.push("/");
+ }
// The shared-key note is the ONLY honest divergence in the panel: on a
// deployment that ships an operator key, insights already work for the
@@ -208,7 +253,55 @@ export function DoneScreen() {
-
{/* 2026-07-17 UX-onboarding audit M5 — the module system (default-on