Onboarding — Code Map
Web (the setup sheet and the setup guide live entirely in the web app)
Routes
| File | Description |
|---|---|
apps/web/src/app/[lang]/(protected)/onboarding/page.tsx | Server component. Calls getCurrentUserServer. Without user.canCreateOrganization: no memberships → redirect to /{lang}/invitation-required; memberships → redirect to /{lang} (member, or no active membership) or /{lang}/dashboard/overview (staff). With a claimable owner grant the membership redirects are skipped. Otherwise renders <OnboardingContent />. |
apps/web/src/app/[lang]/(protected)/invitation-required/page.tsx (+ driver + int.spec) | Client page for signed-in users with no memberships and no owner grant: signed-in email (invitation-required-email), mailto:[email protected] contact (invitation-required-contact), sign-out (invitation-required-sign-out). Root testid invitation-required. |
apps/web/src/app/[lang]/(protected)/onboarding/onboarding-content.tsx | Client component. Wraps OnboardingProvider (OnboardJS) with the single engine step (SETUP_SHEET_STEP_ID), the PostHog plugin and localStorage persistence (taikan:setup-sheet-v5, 7 days; flowId='setup-sheet', flowVersion='5.0.0'). A 403 OWNER_INVITATION_REQUIRED from POST /organizations is shown as onboarding.sheet.errorInvitationRequired. Holds handleSubmit (the API call chain), the pending/error state passed to the sheet, and the createdOrgIdRef / consentsRecordedRef refs that let a retry resume instead of re-creating the org. |
apps/web/src/app/[lang]/(protected)/onboarding/loading.tsx | Skeleton that mirrors the sheet’s geometry so the form lands where the placeholders were. |
The sheet
| File | Purpose |
|---|---|
apps/web/src/components/onboarding/setup-sheet.tsx (+ driver + int.spec) | SetupSheet: header (lockup, guest prefs, sign out), headline with the typed gym name, the three sections on a rail, and the sticky footer (missing-items sentence or inline error, plus the single submit button). Owns focusMissing, the Enter-key routing, marker-state derivation and the section-settled analytics. Props: isPending, error, onSubmit(flowData). |
apps/web/src/components/onboarding/step-marker.tsx | StepMarker: the rail marker (settled / current / upcoming), a ring that closes into a check. Shared with the dashboard setup guide. |
apps/web/src/lib/onboarding/setup-sheet-state.ts (+ unit.spec) | Pure validity: getMissingSetupItems, isSectionSettled, unnamedModes, MISSING_ITEM_SECTION, and the SetupSectionId / SetupMissingItem types. The only place that decides whether the sheet can be submitted. |
Section components
apps/web/src/components/onboarding/steps/
| File | Section id | Reads/writes |
|---|---|---|
org-step.tsx | org | Reads flowData.orgName; writes via updateContext. Name input (org-name, autofocus) and its hint. |
train-step.tsx (+ driver + int.spec) | train | Reads flowData.deliveryModes/programNames; writes via updateContext. Renders DeliveryModePicker with the two schedule/coaching tiles, no Courses tile, no default selection. |
legal-step.tsx | legal | Embeds LegalAcceptanceForm (standalone={false}) and mirrors its onAcceptedChange into legalConsentsAccepted. Local accepted state starts false, so a value restored from persistence is overwritten. |
Shared UI under apps/web/src/components/onboarding/
| File | Purpose |
|---|---|
delivery-mode-picker.tsx | Multi-select tiles (aria-pressed buttons, testids {prefix}-{mode} and {prefix}-{mode}-name) with an inline required name input per ticked tile. Used by the train section and by Train’s “add a way to train” sheet. |
address-autocomplete.tsx | Google Places autocomplete. Not part of the sheet; used by the schedule location dialog. |
invite-members-form.tsx (+ driver + int.spec) | Email-list invite UI. Not rendered by the sheet. |
day-selector.tsx, create-org-form.tsx | Not rendered by the sheet and not imported elsewhere in apps/web. |
Legal (shared with the legal module)
| File | Purpose |
|---|---|
apps/web/src/components/legal/legal-acceptance-form.tsx | In embedded mode (standalone={false}) reports every change of the all-boxes-ticked state through the optional onAcceptedChange(accepted), including the initial unticked one, and still calls onAccepted() when all boxes are ticked. The terms checkbox is legal-core-checkbox. |
Setup guide (dashboard)
| File | Purpose |
|---|---|
apps/web/src/components/overview/setup-guide.tsx (+ driver + int.spec) | SetupGuide: one promoted next step (heading, reason, button) beside a rail of all steps; locked steps as Pro upsells; the all-done line; the dismissed “Resume setup” link. |
apps/web/src/hooks/use-getting-started.ts | useGettingStarted(lang): builds the step list per org shape, derives completion from class types / plans / members / payment config / workouts / programs, marks tier-locked steps (FEATURE_BY_STEP), and owns the taikan:getting-started-dismissed flag. |
apps/web/src/components/overview/v2/overview-v2.tsx | Renders <SetupGuide /> on Overview, under the page guide. |
apps/web/src/app/[lang]/(protected)/dashboard/home/page.tsx | Renders <SetupGuide /> for owner/admin on the optional, direct-only Home landing. |
apps/web/src/lib/session-reset.ts | Clears taikan:getting-started-dismissed (and the unused taikan:guided-tour-completed) when the signed-in identity changes. |
Routing (grant-aware)
| File | Purpose |
|---|---|
apps/web/src/app/[lang]/(protected)/layout.tsx | Server guard: new users go to /onboarding when canCreateOrganization, else /library when ownsCourses, otherwise /invitation-required; a non-new user on /onboarding without a grant is sent to role home; /invitation-required is unreachable once the user has a membership or a grant. Course-viewer paths exempt. |
apps/web/src/components/role-router.tsx | Client mirror of the layout rules. A pending membership with invitationHeld renders invitation-held copy instead of the spinner (see memberships). |
apps/web/src/hooks/use-current-user.ts | Exposes canCreateOrganization and invitationHeld (both false while loading). |
Providers and config
| File | Purpose |
|---|---|
apps/web/src/providers/onboarding-provider.tsx | Exports createPostHogPlugin<T>() (OnboardJS plugin that forwards engine hooks to PostHog under eventPrefix), captureOnboardingEvent(eventName, properties) for the section-level and flow-completed events the single-step engine cannot see, and SETUP_SHEET_VARIANT ('v5-sheet'). |
apps/web/src/config/onboarding-steps.ts | Exports the SetupWizardContext type, TrainDeliveryMode, SETUP_SHEET_STEP_ID ('sheet'), SETUP_WIZARD_INITIAL_CONTEXT (no deliveryModes default). |
Test IDs
| Surface | Test IDs |
|---|---|
| Sheet | setup-sheet-headline, setup-sheet-status, setup-sheet-submit, setup-sheet-error, setup-section-org|train|legal (each carries data-state) |
| Sheet fields | org-name, train-mode-schedule|coaching, train-mode-{mode}-name, legal-core-checkbox |
| Invitation required | invitation-required, invitation-required-email, invitation-required-contact, invitation-required-sign-out |
| Setup guide | getting-started-checklist (the card), setup-guide-next, setup-guide-cta, dismiss-getting-started, resume-getting-started, getting-started-complete |
i18n
Dictionaries: libs/shared/src/lib/i18n/dictionaries/{en,he,ru}.json.
| Namespace | Used by |
|---|---|
onboarding.sheet.* | The sheet: headline, headlineFallback, sub, nameLabel, nameHint, typeLabel, sections.{org,train,legal}.{title,description}, settled, stillNeeded, missing.{name,train,programName,legal}, ready, open, openNamed, opening, errorTitle, errorBody. |
onboarding.sheet.errorInvitationRequired | Inline error when the owner grant is gone. |
auth.invitationRequired{Title,Body,Contact} | The /invitation-required page (Body takes {email}). |
onboarding.train.* | Tile titles, descriptions, name label, placeholders, note. |
onboarding.orgTypes.* | Type labels, used by Settings → General only; the sheet does not ask for a type. |
onboarding.form.orgNamePlaceholder | Gym name placeholder. |
gettingStarted.* | The setup guide: items.*, why.*, cta.*, upsell.*, progress, upgradeCta, resume, dismiss, stepsLabel, doneLabel, allDoneNamed, lockedTitle (the label over the tier-locked group). |
API
The sheet hits these endpoints in order:
| Method | Path | Module |
|---|---|---|
| POST | /organizations | organizations. Claims an owner grant (owner_invitations) in one transaction: 403 OWNER_INVITATION_REQUIRED, 409 OWNER_INVITATION_ALREADY_USED; a retry by the same user with the same gym name after a consumed grant returns the existing org |
| POST | /legal/consents | legal (out of bucket) |
| POST | /organizations/:id/onboarding-complete | organizations. Body carries { deliveryModes, programNames } from the train section |
| GET | /users/me | users-auth (refetched after the invalidate). Carries canCreateOrganization |
OnboardingCompleteDto (apps/api/src/organizations/dto/onboarding-complete.dto.ts) and OrganizationsService.completeOnboarding (apps/api/src/organizations/organizations.service.ts) own the “one starter program per delivery mode, named from programNames[mode] or a locale default” logic. ONBOARDING_DEFAULTS in that service holds the per-locale fallback names.
Program creation after onboarding (Train’s ”+ Add a way to train” row) calls the programs API directly instead (POST /organizations/:orgId/programs, apps/api/src/programs/), since it isn’t part of the onboarding-completion bookkeeping. See dashboard-shell/code-map.md.
Owner grants: OwnerInvitationsModule (apps/api/src/owner-invitations/) — OwnerInvitationsService.resolveClaim / canCreateOrganization / lockPending / markConsumed, consumed by OrganizationsService.create. Admin side (create/resend/revoke) is documented in admin-app; ops in docs/runbooks/owner-invitations.md. The table is owner_invitations (users-auth data model).
The setup guide reads GET /organizations/:orgId/{class-types,plans,plans?active=false,members,payment-config,workouts,programs} and writes nothing. plans?active=false and payment-config feed useBilling() (apps/web/src/hooks/use-billing.ts), which decides connectPayments and whether the payment steps are optional. plans?active=false is requested only when the payment config is readable and not active and there are no active plans; the overview page prefetches it on the server under that same condition, so the guide never waits on a client round-trip for it.
PATCH /users/me still accepts guidedTourCompletedAt (apps/api/src/users/dto/update-profile.dto.ts) and writes users.guided_tour_completed_at. No web code sends it.
Server-side guard uses getCurrentUserServer(lang) from apps/web/src/lib/api.ts.
Tests
| File | What it covers |
|---|---|
apps/web/src/lib/onboarding/setup-sheet-state.unit.spec.ts | getMissingSetupItems (untouched sheet, missing context, whitespace-only name, program name vs mode, a name for every ticked mode, empty when complete) and isSectionSettled. |
apps/web/src/components/onboarding/setup-sheet.int.spec.tsx (+ driver) | The sheet: starts closed and names what is missing, gym name in headline and button, marker states, program-name prompt, opens only when all three sections are answered and submits the flow data, closes again when the terms are unticked, inline error. |
apps/web/src/components/onboarding/steps/train-step.int.spec.tsx (+ driver) | train section: no default selection, tile toggle reveals/hides its name input, flow data updates. |
apps/web/src/components/overview/setup-guide.int.spec.tsx (+ driver) | Setup guide: promotes the first unfinished step with its own action, moves on once earlier steps are done, offers the payment steps as optional to an org that bills nobody, counts a cash-only gym with plans as connected, never promotes a tier-locked step, collapses to one line when every actionable step is done, can be hidden and brought back. |
apps/web/src/components/onboarding/invite-members-form.int.spec.tsx | Invitations form. |
apps/web/src/app/[lang]/(protected)/invitation-required/invitation-required.int.spec.tsx, apps/web/src/components/role-router.int.spec.tsx | Invitation-required page; grant-aware routing. |
apps/api/src/organizations/organizations.service.create.unit.spec.ts, apps/api/src/owner-invitations/owner-invitations.service.unit.spec.ts | Grant claim, race, rollback, retry; grant create/resolve rules. |
apps/web/e2e/specs/onboarding.spec.ts (onboarding-driver.ts) | End-to-end run of the single sheet (the e2e newUser persona is seeded with an owner grant by apps/api/src/testing/testing.service.ts): asserts the submit stays disabled until the name, a named schedule tile and the terms are all in, then submits and lands on /dashboard/overview. |
apps/web/e2e/fixtures/onboarding-chrome.ts | Not a test. Every Playwright fixture calls it to pre-dismiss the setup guide, page guides and cookie banner so they do not shift layouts under other specs. |