Onboarding — QA Plan
Pre-requisites
- A brand-new Clerk identity (no FitKit memberships).
- A second Clerk identity that already has an active membership in some org (for the server-side guard test).
- Clerk + DB + R2 + Redis reachable.
- A pending invitation against a third email (for the auto-accept guard test).
Golden paths
G1 — Happy path
| Step | Action | Expected |
|---|---|---|
| 1 | New user signs up, lands on /{lang} → automatically redirects to /{lang}/onboarding. | Wizard renders on the org step. |
| 2 | Enter “Acme Fitness” + select physical. Next. | LocalStorage updated. PostHog event emitted. |
| 3 | Read terms, accept all checkboxes. | legalConsentsAccepted set. |
| 4 | Reach done, click “Finish”. | API chain runs: org created, consents posted, onboarding-complete (welcome email sent). React Query invalidated. |
| 5 | Redirected to /{lang}/dashboard/overview. | Dashboard loads with owner persona. |
| 6 | DB verify: users, organizations, memberships(role=owner,status=active), legal_consents (3 rows). Clerk: org exists. |
G2 — Resume after refresh
| Step | Action | Expected |
|---|---|---|
| 1 | Mid-wizard (on legal step). Hard-refresh page. | Wizard restores to legal step with orgName/orgType populated. |
| 2 | After 8 days (TTL expiry). | LocalStorage entry stale-cleaned; wizard starts fresh. |
G3 — Server guard (existing memberships)
| Step | Action | Expected |
|---|---|---|
| 1 | User with an active membership navigates to /{lang}/onboarding. | Server-side redirect() to /{lang}/dashboard/overview. Wizard never renders. |
| 2 | Same but role=member only. | Redirect to /{lang} (root). |
| 3 | User with only pending_invitation memberships (no active). | Redirect to /{lang} (lets RoleRouter handle the state). |
G4 — Existing user with pending invitation
| Step | Action | Expected |
|---|---|---|
| 1 | Pending invitation for email x@y.com. User signs up with same email. | Webhook auto-accepts; membership becomes active. |
| 2 | After sign-up, navigate to /{lang}/onboarding. | Server guard redirects away (memberships exist). |
Edge cases
E1 — API failure mid-handleFinish
| Step | Action | Expected |
|---|---|---|
| 1 | Mock POST /organizations to fail. | setError; wizard stays on done. User can retry. No org / consents / membership created. |
| 2 | Mock POST /legal/consents to fail after org succeeds. | Org exists but no consents. setError shown. User retries Finish → org re-create fails (slug collision unlikely) or org already exists with no FK from consents. (TODO: verify retry path — currently the wizard does NOT skip already-created steps.) |
| 3 | Mock POST /:id/onboarding-complete to fail. | Org + consents persist. Wizard shows error. Manual retry POSTs onboarding-complete again. (No idempotency key.) |
E2 — Direct URL access without auth
| Step | Action | Expected |
|---|---|---|
| 1 | Logged-out user navigates to /{lang}/onboarding. | Middleware/Clerk redirects to sign-in. |
E3 — LocalStorage corrupted
| Step | Action | Expected |
|---|---|---|
| 1 | Manually set localStorage['fitkit:setup-wizard-v2'] to invalid JSON. Refresh wizard. | OnboardJS falls back to initial context. (TODO: verify by inspecting OnboardJS error handling — currently no observable fallback path in our code.) |
E4 — Skipping legal step
| Step | Action | Expected |
|---|---|---|
| 1 | Try to click “Next” without accepting legal. | Step’s “next” button disabled until legalConsentsAccepted=true (UI-enforced; service-side, POST /legal/consents simply wouldn’t be called and a guard elsewhere would redirect to /{lang}/accept-terms). |
E5 — Empty org name
| Step | Action | Expected |
|---|---|---|
| 1 | Try to submit org step with empty name. | OnboardJS step validation blocks navigation (TODO: verify — step component has no explicit validator, relies on next button state). |
E6 — Cross-language sticky locale
| Step | Action | Expected |
|---|---|---|
| 1 | Run wizard in he. Refresh to en URL. | LocalStorage state carries; UI shows English. orgName may include Hebrew characters which the API accepts. |
Cross-persona
- Members and coaches never see onboarding — they arrive via invitation and have memberships from the moment they sign in.
- The owner-of-multiple-orgs case is implicitly out of scope: the server guard sends them to dashboard. Org creation from within the app (post-onboarding) happens via a different surface (TODO: verify whether there is a ”+ Create another org” affordance —
/organizationsPOST is reachable but no UI route obviously exposes it).
i18n
| Lang | Strings to verify |
|---|---|
| en | onboarding.form.gymName, onboarding.form.gymType, onboarding.summary.ready, onboarding.done.title, onboarding.doneStep.whatsNext. |
| he | Same keys, Hebrew translations. RTL form layout. |
| ru | Same keys, Russian. |
| Welcome email subject | Welcome to FitKit — {orgName} is ready! (hard-coded English subject in OrganizationsService.completeOnboarding; TODO: verify whether i18n localization of the email is on roadmap). |
Expected vs actual
- After wizard finish: DB matches the 6-step writes above. Welcome email landed at the owner’s email (check mailpit/SES logs).
- LocalStorage
fitkit:setup-wizard-v2cleared after success (OnboardJS does this by default ononFlowComplete— TODO: verify). - PostHog:
onboarding_step_completedevents per step,onboarding_flow_completedevent at the end. - Tour: after onboarding, the guided tour overlay launches on
/dashboard/overview. Completion setsusers.guided_tour_completed_at.