Skip to content

Onboarding Multi-Step Obrigatório — Final Report

What Was Built

A mandatory 3-step onboarding flow that users must complete before accessing the application. The flow collects personal data (name, username), address (street, city, state, country), and preferences (theme, language, currency, timezone). Users who haven't completed onboarding are automatically redirected to /onboarding from any protected route.

Architecture

Backend (Django)

  • New field display_username on CustomUser (apps/users/models.py:64) — CharField(30), not unique at DB level (uniqueness validated in view to allow empty defaults for existing users)
  • New field is_onboarded on Membership (apps/communities/models/membership.py:35) — BooleanField, default=False
  • Updated SignupProfileUpdateRequestSerializer (apps/headless/schemas.py:1133) — accepts display_username, address, city, state, country, theme_preference, language, currency, timezone
  • Updated CurrentMemberSerializer and ProfileSerializer — return is_onboarded and display_username
  • Updated SignupProfileView.put (apps/headless/views.py:3012) — handles all new fields, validates display_username uniqueness, sets membership.is_onboarded = True on completion
  • Updated CommunityMemberView.get (apps/headless/views.py:162) — returns is_onboarded and display_username

Frontend (TanStack SPA)

  • /onboarding route (frontend/src/routes/onboarding.tsx) — dark gradient layout, no sidebar/topbar, 3-step wizard
  • Onboarding components (frontend/src/components/onboarding/):
    • StepPersonal.tsx — first_name, last_name, display_username with @ prefix
    • StepAddress.tsx — address, city, state, country (select)
    • StepPreferences.tsx — theme (button toggle), language, currency, timezone (selects)
    • OnboardingWizard.tsx — step container with progress bar, navigation, validation
  • Onboarding gate in __root.tsx:beforeLoadgetSession() now returns is_onboarded; redirects to /onboarding if false and route is not exempt
  • Exempt routes: /onboarding, /login, /register, /forgot-password, /confirm-email, /reset-password, /invite, /community-not-found

Data Flow

Register → /signup/profile (name/headline) → /onboarding (3 steps) → /
Login → beforeLoad checks is_onboarded → false → /onboarding → /

Design Decisions

  • display_username not unique at DB level: Existing users all have "" as default, which would violate a unique constraint. Uniqueness is enforced in the view layer instead.
  • is_onboarded on Membership, not CustomUser: Onboarding is per-community — a user could be onboarded in one community but not another.
  • Client-side gate in RootComponent, not server-side beforeLoad: The beforeLoad runs server-side during SSR where window and community slug headers aren't available. The client-side check in RootComponent is simpler and more reliable.
  • Dark gradient layout: Onboarding uses a standalone dark theme (from-gray-950 via-gray-900 to-gray-950) with glassmorphism card, independent of user's theme preference.

Usage

  1. New user registers → redirected to /signup/profile → then to /onboarding
  2. User completes 3 steps (Personal → Address → Preferences) → redirected to /
  3. Returning user with is_onboarded=false → automatically redirected to /onboarding
  4. User can update preferences later via Settings > Profile

Verification

  • Python lint: All modified files pass ruff check
  • TypeScript: Compiles with no new errors (pre-existing errors in unrelated files)
  • Django tests: headless (40/40 pass), users (2/2 pass), communities (3 pre-existing JWT auth failures unrelated to changes)
  • Migrations: 0006_add_display_username and 0009_add_is_onboarded applied successfully

Journey Log

  • [dead end] Initially set display_username as unique=True in the model — failed during migration because existing users all have "" as default, violating the unique constraint. Removed DB-level uniqueness, validate in view instead.
  • [lesson] The simple_history.admin module exports SimpleHistoryAdmin, not HistoryAdmin. Fixed pre-existing import bug in communities/admin.py and spaces/admin.py to unblock migrations.
  • [pivot] Originally planned server-side gate in beforeLoad, but SSR doesn't have access to window or community slug headers. Extended getSession() to return is_onboarded from the existing community_member API call (no extra HTTP request needed).

Strum — Documentação.