Skip to content

v1.8 — Onboarding Wizard Overhaul

Replace the modal-based onboarding wizard with a full-page, persona-aware, slide-driven flow that lifts activation, hits a ≤60s time-to-first-goal budget, and ships as a reusable surface — without breaking any existing user.

Summary

The pre-v1.8 onboarding was a single hardcoded modal: every new user saw the same five slides, completion was tracked client-side via a localStorage flag, and the wizard could not branch by persona, source, or stated goal type. Activation analytics were anecdotal because there was no funnel to measure.

v1.8 rebuilt the experience as a full-page route at /welcome/:slideId, refactored the slide list into a configurable SlideRegistry<TState> consumed by a generic SlideWizardLayout.vue, moved completion state to a server-authoritative UserDetail.data['onboarding'] JSONB blob updated through a StoreOnboardingState mutation, and instrumented every slide step as a UserAction plus PostHog event so activation lift can be measured against the pre-v1.8 baseline.

The framework was deliberately separated from the onboarding surface so it could be reused. Phase 34 proved that out by shipping a profile-completion mini-wizard at /profile/complete that consumes SlideWizardLayout verbatim.

The milestone shipped in 6 phases, 19 plans, and 44 tasks — all 32 requirements satisfied, 10/10 cross-phase contracts verified, every phase nyquist_compliant: true. Two phases carry human_needed flags for explicit post-deploy manual checks (live PostHog ramp validation, physical-device a11y testing) — tracked in HUMAN-UAT files, not as gaps.

Goal

Transform the existing modal-based OnboardingWizard into a modern, full-page, persona-aware onboarding flow that measurably lifts activation, hits a ≤60s time-to-first-goal budget, and ships as a reusable slide-driven framework — without breaking the experience for any of the existing user base.

Scope — What Shipped

Server-authoritative onboarding state (Phase 32 — SAFE-* / SYNC-*)

  • Nested UserDetail.data['onboarding'] JSONB schema (status, current_slide, persona, goal_type, source attribution).
  • StoreOnboardingState mutation with deep-merge semantics; status enum strictly limited to complete | skip.
  • Idempotent backfill inside SyncUser retroactively marks every pre-v1.8 user complete on next sign-in.
  • SYNC-03 regression suite proves: double-call returns identical payloads; produces exactly one UserAction.first_sign_in row; backfills at most once; concurrent ClerkUserSync converges to a single User row via RecordNotUnique rescue.

Apollo + first-run gate (Phase 32 — SYNC-04 / SYNC-05)

  • UserFragment extended to include onboarding status; status flows into the Apollo cache.
  • Fail-closed PostHog flag wrapper at src/lib/featureFlags.ts — unknown flags default to false.
  • New useOnboardingGate composable is the single source of truth for /welcome gating.
  • Router guard invokes useOnboardingGate via a Phase-33-ready stub so the gate is ready when the new flow lands.

Legacy bridge (Phase 32)

  • The pre-v1.8 OnboardingWizard.vue modal now atomically commits skip/complete to the server through StoreOnboardingState, fires fire-and-forget progress on every slide advance, and resumes mid-flow users from cached onboardingStatus.progress.currentSlide on mount.
  • Backed by 12 green Vitest examples (6 exit + 6 resume); flips Phase 32's nyquist_compliant: true.

Full-page route shell (Phase 33)

  • /welcome/:slideId route under IonPage full-page chrome with progress bar, back, skip, next.
  • Module-scoped useOnboardingFlow composable holds in-flight answers across browser navigation.
  • Phase 32 router-guard flip redirects first-run users to WelcomeSlide.
  • Placeholder slide registry (welcome / name / preview / done) and SFCs with exact UI-SPEC copy + class strings + a11y markup; DoneSlide commits completion atomically via the Phase 32 mutation with non-blocking failure handling.
  • Two integration test surfaces — welcomeFlow.integration + welcomeGate — assert the route shell + gate contract.

Reusable slide framework (Phase 34)

  • SlideDefinition<TState> types, three pure resolver functions with fail-closed hook wrapping and cycle detection, and a reactive useSlideFramework(registry, state, currentSlideIdRef) composable. Reactive validation gating + resolver delegation, 35 passing unit tests.
  • Phase 33's WelcomeLayout.vue chrome extracted into a generic, framework-driven SlideWizardLayout.vue; original WelcomeLayout refactored to a ~105-LOC onboarding-specific wrapper that supplies only the StoreOnboardingState skip mutation and terminal-advance delegation.
  • placeholderSlides.ts retyped as SlideRegistry<OnboardingAnswers> with explicit validate: () => true hooks.
  • Profile-completion mini-wizard at /profile/complete consumes SlideWizardLayout verbatim — second-surface proof of reusability.

Persona-aware branching (Phase 35)

  • Six Coach personas extracted to a typed constant; persona-tuned copy map for post-persona slides; defensive URL/referrer attribution parser.
  • PersonaSlide (2x3 picker that commits via UPDATE_COACHING_PREFERENCES_MUTATION).
  • GoalTypeSlide (GraphQL category picker + explicit skip).
  • SourceSlide (4-option self-report; auto-skipped when URL attribution exists).
  • DoneSlide rewritten to render persona-tuned closing copy via getPersonaCopy.
  • WelcomeLayout's onMounted calls captureSourceAttribution and seeds flow.answers.sourceAttribution once per session.
  • 7 integration tests cover PATH-01 through PATH-04 end-to-end.

Post-wizard activation + funnel analytics (Phase 36)

  • Pre-v1.8 GettingStartedCard (225 lines of inline goal-create form) replaced with a lightweight glass-card "next moves" tile chooser surfacing 3–5 persona-tuned activation paths.
  • recordUserAction(action: String!, metadata: JSON) GraphQL mutation extended to back the new wizard_* enum values.
  • wizard_started, wizard_slide_viewed, wizard_slide_advanced, wizard_slide_skipped, wizard_completed, wizard_abandoned events instrumented through both UserAction and PostHog.

Visual polish + accessibility (Phase 37)

  • prefers-reduced-motion support extracted into a shared module.
  • Glassmorphism, design tokens, Outfit/Inter, motion aligned with the current design system.
  • Dark/light parity verified at all iPhone viewports.
  • aria-live announcers on slide transitions; focus traps on modal-style skip confirmations; 44px tap targets across the flow.

Phases

PhaseNameStatusPlansHighlights
32Safety rails + first-run plumbingShipped5Server-authoritative onboarding state, idempotent backfill, fail-closed flag wrapper, legacy modal bridge
33Full-page route shellShipped3/welcome/:slideId, useOnboardingFlow, placeholder registry, integration test surfaces
34Slide-driven frameworkShipped3SlideDefinition<TState> + SlideWizardLayout + profile-completion proof of reuse
35Persona-aware branching pathsShipped2Persona/goal-type/source slides, attribution parser, 7-test PATH suite
36Post-wizard activation + funnel analyticsShipped3Tile chooser replaces inline form, wizard_* UserActions + PostHog events
37Visual polish + accessibilityShipped3Reduced-motion, design-system alignment, dark/light parity, a11y

Key Decisions

  • Server-authoritative onboarding status. Pre-v1.8 used localStorage; multi-device users could re-trigger the wizard. Moving status to UserDetail.data['onboarding'] makes the gate honest across devices and reload survives.
  • Status enum strictly complete | skip. StoreOnboardingState refuses any other status. Prevents drift where the UI invents intermediate states the gate doesn't understand.
  • Slide framework is surface-agnostic. SlideDefinition<TState> is generic; the onboarding registry is one of many. Phase 34's profile-completion wizard validates that the abstraction is real, not aspirational.
  • Funnel events fire from both UserAction and PostHog. UserAction survives PostHog outages and lets gamification badges trigger off the same signal.
  • Safety phases first (Phase 32 before any UI). SAFE + SYNC ship before slide work so every subsequent slide lives behind a feature flag and cannot regress existing users.
  • POLISH deferred to Phase 37. Polishing every slide once content has stabilized beats polishing-then-re-polishing as branching paths land.

Requirements Coverage

32 / 32 requirements satisfied; 10/10 cross-phase contracts verified; all phases nyquist_compliant: true.

CategoryCountStatus
SAFE-*4All satisfied
SYNC-*5All satisfied
PAGE-*4All satisfied
FRAMEWORK-*5All satisfied
PATH-*4All satisfied
METRIC-*6All satisfied (live PostHog ramp = HUMAN-UAT)
POLISH-*4All satisfied (physical-device a11y = HUMAN-UAT)

Outcomes

  • New users land on /welcome/welcome with a persona-aware branching path, and existing users are silently backfilled to complete on next sign-in.
  • The slide framework powers two surfaces today (onboarding + profile-completion) and is ready to power any future multi-step flow.
  • Activation funnel is measurable end-to-end in PostHog; wizard_* events plus the post-wizard tile-chooser conversions are the lift signal v1.8 was scoped against.
  • The pre-v1.8 modal flow is removed entirely; Apollo cache + router guard are now the single source of truth for first-run.

Tech Debt

  • (Phase 32) Pitfall #2 uniqueness gap documented as a pending xit with Option B escape-hatch reference — surfaces only under simultaneous duplicate sign-ins, mitigation is the RecordNotUnique rescue.
  • (Phase 36) Live PostHog activation-lift validation flagged human_needed; numeric lift can only be measured after a real ramp window post-deploy.
  • (Phase 37) Physical-device a11y verification flagged human_needed; emulator-only coverage is insufficient for VoiceOver / TalkBack rotor-navigation edge cases.
  • 825b909b — v1.8 — Onboarding Wizard Overhaul (full-page /welcome flow + persona-aware branching + funnel analytics) (#331)
  • 0c5463e0 — fix(auth+onboarding): clear stale fallback token on logout; mark onboarding complete reactively
  • ea884f5d — test(welcome): add setOnboardingCompletedAt to useClerkSync mock

Last updated: 2026-05-22

Loading…