mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
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
- The
(prowler)/layout.tsxderives a tri-statehasProviderson every navigation and mounts<OnboardingCheckpointWatcher />(sibling to the gate). - When the watcher observes a concrete
false → truehasProvidersflip (the user actually connected a provider), it opens the checkpoint dialog once. Anundefined → true(user already had providers) never fires. A localStorage marker (prowler.onboarding.checkpoint) prevents re-appearance. - "Continue the tour" calls
startSequence(nextFlowId)on the ephemeraluseOnboardingSequenceStoreand navigates to that flow's route. - Each route mounts an
<OnboardingTrigger flow={...} />. The trigger force starts the flow whenslice.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. - On tour close,
useDriverTour'sonClosed(state)reports the outcome:completed→advance()(navigate to the next flow),skipped/dismissed→stop()(the sequence ends; closing any tour ends the sequence). - The slice is ephemeral (plain Zustand
create, nopersist). It carriescurrentFlowIdacross 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.
- Tour file —
ui/lib/tours/<flow-id>.tour.tsviadefineTour<Target>and theassets/tour-template.ts. Keep it shallow: a centered welcome step plus 1–2 anchored steps.coversFilesscopes the drift check. - 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:checkhard-fails a tour target with no matching anchor). - Registry entry — add
{ id, order, title, description, route, tour }toonboardingFlowsinregistry.ts. Ordering is data (order). - Trigger mount — render
<OnboardingTrigger flow={getFlowById("<id>")!} />inside the route's client host. PassstepHandlers/configOverridesonly 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 tourtargetmust resolve to a realdata-tour-idanchor within itscoversFiles.pnpm exec vitest run --project unit— pure logic (slice, helpers, registry, tour shapes). The driver primitive short-circuits inNODE_ENV==="test".