Files
prowler/ui/lib/onboarding/README.md
2026-06-16 09:32:37 +02:00

75 lines
4.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Onboarding system (developer guide)
The onboarding system runs short, anchored driver.js tours and orchestrates a
cross-route **guided sequence** after a user connects their first provider.
Everything lives in client state and localStorage — there is **zero backend
coupling**.
## Building blocks
| Concern | File |
| -------------------------------------- | --------------------------------------------------------------------- |
| Flow registry (single source of truth) | `ui/lib/onboarding/registry.ts` |
| Flow type (`OnboardingFlow`) | `ui/lib/onboarding/onboarding-types.ts` |
| Tour definitions (`*.tour.ts`) | `ui/lib/tours/` |
| Driver primitive (`useDriverTour`) | `ui/lib/tours/use-driver-tour.ts` |
| Per-route trigger | `ui/components/onboarding/onboarding-trigger.tsx` |
| Ephemeral sequence slice | `ui/store/onboarding-sequence.ts` |
| Checkpoint watcher + dialog | `ui/components/onboarding/onboarding-checkpoint-{watcher,dialog}.tsx` |
| Mandatory new-user gate | `ui/components/onboarding/onboarding-gate.tsx` |
| Manual replay list | `ui/components/ui/user-nav/user-nav.tsx` |
## How the guided sequence works
1. The `(prowler)/layout.tsx` derives a tri-state `hasProviders` on every
navigation and mounts `<OnboardingCheckpointWatcher />` (sibling to the gate).
2. When the watcher observes a concrete `false → true` `hasProviders` flip (the
user actually connected a provider), it opens the checkpoint dialog **once**.
An `undefined → true` (user already had providers) never fires. A localStorage
marker (`prowler.onboarding.checkpoint`) prevents re-appearance.
3. "Continue the tour" calls `startSequence(nextFlowId)` on the ephemeral
`useOnboardingSequenceStore` and navigates to that flow's route.
4. Each route mounts an `<OnboardingTrigger flow={...} />`. The trigger force
starts the flow when `slice.currentFlowId === flow.id` (sequence) **or** when
the `?onboarding=<id>` param matches (replay). The StrictMode-safe latch /
keyed runner / empty-deps force-start is preserved verbatim.
5. On tour close, `useDriverTour`'s `onClosed(state)` reports the outcome:
`completed``advance()` (navigate to the next flow), `skipped`/`dismissed`
`stop()` (the sequence ends; closing any tour ends the sequence).
6. The slice is **ephemeral** (plain Zustand `create`, no `persist`). It carries
`currentFlowId` across client navigations but resets on a hard reload, so a
mid-sequence refresh never re-fires.
`attack-paths` is special: its page already owns a driver, so its registry entry
sets `ownsAutoOpen: true`, the trigger does **not** mount a runner for it, and
the page wires `onClosed` to the slice itself (single-fire).
## Add a new flow (the extensibility contract)
A new flow is **one registry entry + one tour file + its anchors + a trigger
mount** — no gate, modal, or nav edits.
1. **Tour file**`ui/lib/tours/<flow-id>.tour.ts` via `defineTour<Target>` and
the `assets/tour-template.ts`. Keep it shallow: a centered welcome step plus
12 anchored steps. `coversFiles` scopes the drift check.
2. **Anchors** — add `data-tour-id="<flow-id>-<target>"` on the page-specific
**client** component for each anchored step (never the shared Navbar).
The tour file and its anchors MUST ship in the SAME PR (`tour:check`
hard-fails a tour target with no matching anchor).
3. **Registry entry** — add `{ id, order, title, description, route, tour }` to
`onboardingFlows` in `registry.ts`. Ordering is data (`order`).
4. **Trigger mount** — render `<OnboardingTrigger flow={getFlowById("<id>")!} />`
inside the route's client host. Pass `stepHandlers`/`configOverrides` only if
the flow needs them (e.g. add-provider opens the wizard).
The avatar "Product tour" submenu and `advance()` both derive from
`getOrderedFlows()`, so a new flow appears in the replay list and participates in
the sequence automatically.
## CI gates
- `pnpm run tour:check` (`ui/scripts/check-tour-alignment.mjs`) — every tour
`target` must resolve to a real `data-tour-id` anchor within its `coversFiles`.
- `pnpm exec vitest run --project unit` — pure logic (slice, helpers, registry,
tour shapes). The driver primitive short-circuits in `NODE_ENV==="test"`.