Files
HealthLog/src/hooks/use-auth.ts
T
Marc-André BombeckandClaude Opus 4.7 8215e25876 feat(onboarding): auto-launch tour for new users on first dashboard load
v1.4.15 Phase B5 — third commit. Wires the spotlight tour
(`<OnboardingTour>` from previous commit) to auto-fire for new users
the first time they land on the dashboard, persists completion to
the DB, and exposes a window-event hook for replay.

Pieces:

  - `<TourLauncher>` (`src/components/onboarding/tour-launcher.tsx`)
    — gates and mounts the tour. Self-decides via:
      * `user.onboardingTourCompleted` (DB flag from migration 0030)
      * sessionStorage dismiss guard (defensive against in-flight
        `/api/auth/me` refetches re-opening the tour after Skip)
      * post-wizard 1500ms grace window (avoids the wizard's
        `doneToast` and the spotlight stacking on the same z-layer)
      * a `ready` prop the dashboard sets once analytics has
        resolved (so the spotlight cutout doesn't snap to a 0×0
        placeholder before tiles render)
    Listens for `healthlog:tour-restart` to force-reopen.

  - `POST /api/onboarding/tour` — single endpoint with body
    `{ completed: boolean, outcome?: "completed" | "skipped" }`.
    Flips `users.onboarding_tour_completed`. Dispatches a Wide
    Event annotation with the outcome so Loki can split completion
    from dismiss without a second column.

  - `/api/auth/me` now returns `onboardingTourCompleted`. The
    `AuthUser` type in `src/hooks/use-auth.ts` gains the field;
    no consumer outside the launcher reads it today.

  - `data-tour-id` attributes added to:
      * dashboard tile-strip wrapper (`dashboard-tile-strip`)
      * dashboard "Add" dropdown trigger (`dashboard-quick-add`)
      * sidebar nav items (`nav-insights`, `nav-settings`,
        `nav-achievements`, plus the rest for symmetry —
        targeted attribute, not class names, so renames in
        Tailwind classes don't silently break the spotlight)

  - The `/onboarding` wizard's `persistAndExit()` writes
    `sessionStorage["healthlog-tour-referrer"] = "1"` immediately
    before navigating to `/`. The launcher consumes-and-clears that
    on first mount via a `useState` lazy initialiser (presence-only
    flag — no `Date.now()` so the render-phase decision stays pure
    under `react-hooks/purity`).

ESLint compliance:

  - `react-hooks/set-state-in-effect` — no synchronous setState in
    a useEffect body. The launch decision happens in the render
    phase using the React-recommended "previous input id" guard
    (mirrors `account-section.tsx`'s `seededUserId`), and the
    post-wizard timer's setState lives inside a setTimeout
    callback (an event handler, not the effect body).
  - `react-hooks/purity` — render-phase setState only branches on
    prop values + sessionStorage-presence helpers. No `Date.now()`
    / `Math.random()` / fetch in render.

Tests: my 39 unit tests pass (was 27 — +12 across tour-state and
tour SSR shape). Pre-existing test failures in
`doctor-report-data.test.ts` are uncommitted B6 work in the
working tree, not B5. Lint clean for B5 files; one pre-existing
B6 lint error in `doctor-report-dialog.tsx` is out of scope.
Typecheck has the same pre-existing `dashboard-layout.test.ts`
errors documented in STATE.md (A4 / phase-D scope).

Restart UX (Settings → Account → "Restart tour") lands in the next
commit (B5-4) so this commit stays orthogonal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:28:51 +02:00

66 lines
1.6 KiB
TypeScript

"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
export interface AuthUser {
id: string;
username: string;
email: string | null;
role: string;
heightCm: number | null;
dateOfBirth: string | null;
gender: string | null;
timezone: string;
onboardingCompletedAt: string | null;
/**
* v1.4.15 Phase B5: whether the user has finished or dismissed
* the spotlight tour overlaid on the dashboard. Distinct from
* `onboardingCompletedAt` (the wizard at /onboarding) — see the
* `<TourLauncher>` component for the gating logic.
*/
onboardingTourCompleted: boolean;
gravatarUrl: string | null;
glucoseUnit: string | null;
}
async function fetchMe(): Promise<AuthUser> {
const res = await fetch("/api/auth/me");
if (!res.ok) throw new Error("Not authenticated");
const json = await res.json();
return json.data;
}
export function useAuth() {
const query = useQuery({
queryKey: ["auth", "me"],
queryFn: fetchMe,
retry: false,
staleTime: 5 * 60 * 1000,
});
return {
user: query.data ?? null,
isLoading: query.isLoading,
isAuthenticated: !!query.data,
error: query.error,
refetch: query.refetch,
};
}
export function useLogout() {
const queryClient = useQueryClient();
const router = useRouter();
return useMutation({
mutationFn: async () => {
await fetch("/api/auth/logout", { method: "POST" });
},
onSuccess: () => {
queryClient.setQueryData(["auth", "me"], null);
queryClient.invalidateQueries({ queryKey: ["auth"] });
router.push("/auth/login");
},
});
}