# Phase 7: Setup Wizard - Research **Researched:** 2026-04-20 **Domain:** React multi-step wizard UI with localStorage persistence, Supabase writes **Confidence:** HIGH ## Summary Phase 7 builds a 3-step setup wizard that guides first-run users through budget template creation. The phase is primarily a frontend UI task: no new DB schema changes are needed (Phase 6 delivered `setup_completed`, presets, and `useFirstRunState`). The wizard must render a new `/setup` route outside the AppLayout (standalone page like login/register), persist state to localStorage, and on completion create categories + template items via existing hooks. The codebase already provides every data layer primitive needed: `useFirstRunState` for redirect gating, `useCategories().create` for category creation, `useTemplate().createItem` for template item creation, `PRESETS` array with 19 items, and full i18n keys for preset names. The work is purely component authoring, routing, and orchestration. **Primary recommendation:** Build the wizard as a standalone page component with internal step state management using React useState + localStorage sync. No additional libraries needed -- the existing stack (React 19, React Router 7, shadcn/ui, Tailwind, React Query, Supabase) handles everything. ## User Constraints (from CONTEXT.md) ### Locked Decisions - Numbered horizontal stepper (1-2-3 bar at top) -- minimal, clear progress indicator - Centered card layout (max-w-2xl) on clean background -- consistent with auth pages - Next/Back buttons at bottom + step indicator is clickable for going back - Single "monthly net income" number input pre-filled with 3000, required > 0 - Full checklist: all 19 PRESETS shown, grouped by category type, with editable amounts - Bills (4) + variable_expense (5) pre-checked by default - Sticky bar: "Remaining to allocate: Income - sum(checked) = X" -- turns red when negative - Read-only review step with grouped summary - "Complete" creates categories + template items. Does NOT create first month's budget. - After completion: redirect to `/` with success toast - "Skip" on each step + global "Skip setup" to exit entirely - localStorage keyed by user_id for persistence across refresh - On complete or skip: clear localStorage, mark profiles.setup_completed = true - No animated transitions between steps (instant swap) ### Claude's Discretion - Exact component decomposition (how many sub-components for the wizard) - Animation/transition between steps (decided: none) - Exact styling of category badges and grouping headers - Toast message wording variations - Error handling UX for failed API calls during completion ### Deferred Ideas (OUT OF SCOPE) None -- discussion stayed within phase scope. ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | SETUP-01 | New user is guided through a 3-step wizard: income, recurring items, review | useFirstRunState hook for redirect gating, /setup route outside AppLayout | | SETUP-02 | User sees pre-filled common budget items with sensible default amounts (~15-20 items) | PRESETS array (19 items) with defaultAmount + i18n keys already exist | | SETUP-03 | User can skip any wizard step or the entire wizard | Skip Step per step + Skip setup global link, marks setup_completed=true | | SETUP-04 | User sees a live "remaining to allocate" balance updating as items are selected | Computed from useState: income - sum(checked items amounts), re-renders on change | | SETUP-05 | User's template is created from wizard selections on completion | useCategories().create + useTemplate().createItem mutations in sequence | ## Architectural Responsibility Map | Capability | Primary Tier | Secondary Tier | Rationale | |------------|-------------|----------------|-----------| | Wizard UI & navigation | Browser / Client | -- | Pure client-side multi-step form, no SSR | | State persistence | Browser / Client | -- | localStorage for mid-session persistence | | First-run redirect | Browser / Client | -- | useFirstRunState reads cached React Query data | | Category + template creation | API / Backend (Supabase) | Browser / Client | Supabase handles insert; client calls via existing hooks | | setup_completed flag update | API / Backend (Supabase) | Browser / Client | Direct Supabase update from client | ## Standard Stack ### Core (already installed) | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | React | ^19.2.4 | Component rendering | Project framework [VERIFIED: package.json] | | React Router DOM | ^7.13.1 | /setup route, Navigate redirect | Project router [VERIFIED: package.json] | | @tanstack/react-query | ^5.90.21 | Data fetching/mutations via hooks | Project data layer [VERIFIED: package.json] | | @supabase/supabase-js | ^2.99.1 | DB writes (categories, template_items, profiles) | Project backend [VERIFIED: package.json] | | react-i18next | ^16.5.8 | Bilingual copy (EN/DE) | Project i18n [VERIFIED: package.json] | | sonner | ^2.0.7 | Toast notifications on completion/error | Project toast library [VERIFIED: package.json] | | lucide-react | ^0.577.0 | Check icon for stepper | Project icon library [VERIFIED: package.json] | ### Supporting (already installed) | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | radix-ui | ^1.4.3 | Checkbox primitive (via shadcn) | Step 2 item selection | | tailwind-merge + clsx + cva | various | Conditional styling | All components | ### New shadcn Component to Install | Component | Purpose | |-----------|---------| | checkbox | Item selection in step 2 (not yet in /src/components/ui/) | **Installation:** ```bash npx shadcn@latest add checkbox ``` ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | useState + localStorage | react-hook-form + persist | Overkill for 2 fields (income + selections); useState simpler | | Custom stepper | @shadcn/stepper or third-party | Not available in shadcn registry; custom is trivial for 3 steps | | Zustand for wizard state | useState + localStorage | No global state needed; wizard is single-page isolated | ## Architecture Patterns ### System Architecture Diagram ``` [First Login] --> [AppLayout / DashboardPage] | useFirstRunState() --> isFirstRun? | | NO YES | | Dashboard | [SetupWizard Page] | localStorage read --> restore step & data | +----------+----------+----------+ | | | | Step 1 Step 2 Step 3 Skip Setup (Income) (Items) (Review) | | | | | +----------+----------+ | | | [Complete] [Skip] | | create categories clear localStorage create template items set setup_completed=true clear localStorage redirect to / set setup_completed=true redirect to / with toast ``` ### Recommended Project Structure ``` src/ ├── pages/ │ └── SetupPage.tsx # Page component, orchestrates wizard ├── components/ │ └── setup/ │ ├── WizardStepper.tsx # Horizontal 1-2-3 stepper bar │ ├── IncomeStep.tsx # Step 1: income input │ ├── RecurringItemsStep.tsx # Step 2: checklist with amounts │ ├── ReviewStep.tsx # Step 3: read-only summary │ ├── AllocationBar.tsx # Sticky remaining balance │ ├── PresetItemRow.tsx # Single checkbox row │ └── CategoryGroupHeader.tsx # Section divider with colored dot ``` ### Pattern 1: Wizard State in localStorage **What:** Single useState object synced to localStorage on every change **When to use:** Multi-step forms that must survive page refresh **Example:** ```typescript // Source: project convention + React docs interface WizardState { currentStep: 1 | 2 | 3 income: number selectedItems: Record } const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}` function useWizardState(userId: string) { const [state, setState] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY(userId)) if (saved) return JSON.parse(saved) return getDefaultState() // builds from PRESETS defaults }) useEffect(() => { localStorage.setItem(STORAGE_KEY(userId), JSON.stringify(state)) }, [state, userId]) return [state, setState] as const } ``` ### Pattern 2: Completion Sequence (Category + Template Item Creation) **What:** Sequential creation of categories then template items, handling duplicates **When to use:** Wizard completion step **Example:** ```typescript // Source: existing useCategories + useTemplate hooks async function completeWizard( selectedItems: SelectedItem[], income: number, createCategory: MutateAsync, createItem: MutateAsync, ) { // 1. Determine unique category types needed const typesNeeded = [...new Set(selectedItems.map(i => i.type))] // 2. Create categories (skip if already exist -- DB has unique constraint) const categoryMap: Record = {} for (const type of typesNeeded) { try { const cat = await createCategory({ name: categoryLabel(type), type }) categoryMap[type] = cat.id } catch (e) { // Unique constraint violation = category exists, fetch it // Handle gracefully } } // 3. Create template items for each selected preset for (const item of selectedItems) { await createItem({ category_id: categoryMap[item.type], item_tier: item.item_tier, budgeted_amount: item.amount, }) } } ``` ### Pattern 3: Route Structure (Standalone Page Outside AppLayout) **What:** /setup route as a sibling to login/register, not nested in AppLayout **When to use:** Wizard must be full-screen without sidebar nav **Example:** ```typescript // Source: existing App.tsx routing pattern {/* Public routes */} } /> } /> {/* Setup wizard -- protected but outside AppLayout */} } /> {/* Main app layout */} }> } /> ... ``` ### Anti-Patterns to Avoid - **Nested wizard inside AppLayout:** Wizard must be standalone (no sidebar). Keep it as a separate protected route. - **Creating template without categories first:** Template items require `category_id` -- categories must be created first in completion flow. - **Using mutateAsync without error handling:** Each Supabase insert can fail. Wrap in try/catch, show partial-error toast if some items fail. - **Reading profile.setup_completed for redirect:** Use `useFirstRunState` (checks categories/template data), not setup_completed flag. The flag is set on completion, not read for gating. ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Checkbox UI | Custom div with click handlers | shadcn Checkbox (Radix) | Keyboard accessibility, aria states, focus ring | | Toast notifications | Custom alert/banner | sonner (already integrated) | Consistent with app, auto-dismiss, queue | | Number formatting | Manual toFixed/locale | Intl.NumberFormat with profile currency | Handles EUR/USD/CHF, locale-aware separators | | i18n key resolution | String concatenation | `t(\`presets.${type}.${slug}\`)` | Already set up in en.json/de.json | **Key insight:** Every UI primitive and data operation is already available in the project. This phase is purely about composition and orchestration. ## Common Pitfalls ### Pitfall 1: Redirect Loop on Dashboard **What goes wrong:** User completes wizard, lands on dashboard, `useFirstRunState` still returns `isFirstRun=true` because React Query cache is stale. **Why it happens:** Categories/template items just created but query cache not invalidated yet. **How to avoid:** After completion, invalidate both `["categories"]` and `["template-items"]` query keys before redirecting. Or use `setup_completed` flag for the redirect condition (check profiles table). **Warning signs:** User bounces between dashboard and /setup infinitely. ### Pitfall 2: Category Name Collision on Completion **What goes wrong:** User already has a "Bills" category (edge case: skipped wizard, created manually, re-enters). DB unique constraint rejects the insert. **Why it happens:** Phase 6 added `UNIQUE(user_id, name)` constraint on categories. **How to avoid:** Use upsert or catch constraint violation errors and fetch existing category ID instead. The completion logic must handle "category already exists" gracefully. **Warning signs:** Wizard completion fails silently or shows generic error. ### Pitfall 3: localStorage Orphan After Logout **What goes wrong:** User starts wizard, logs out, different user logs in -- sees previous user's wizard state. **Why it happens:** localStorage key must include user_id. **How to avoid:** Key is `setup-wizard-${userId}` (already specified in CONTEXT). Always validate userId matches on load. **Warning signs:** Wrong income/selections appearing for a different user. ### Pitfall 4: Race Condition in Completion **What goes wrong:** User double-clicks "Complete Setup" and creates duplicate template items. **Why it happens:** Button not disabled during async operation. **How to avoid:** Disable Complete button immediately on click, show spinner. Use a `completing` state flag. **Warning signs:** Duplicate rows in template_items table. ### Pitfall 5: useTemplate() Needs Template to Exist Before createItem **What goes wrong:** `createItem` throws "Template not loaded" because `templateId` is undefined. **Why it happens:** `useTemplate()` auto-creates a template row on first query, but the query must complete before `createItem` works. **How to avoid:** Ensure `useTemplate()` is called early in the wizard (mount time) so the template row exists by completion time. Or call `getOrCreateTemplate()` directly in the completion flow. **Warning signs:** "Template not loaded" error on wizard completion. ## Code Examples ### Redirect Logic in Dashboard (or AppLayout) ```typescript // Source: existing useFirstRunState pattern + CONTEXT decisions import { useFirstRunState } from "@/hooks/useFirstRunState" import { Navigate } from "react-router-dom" // Inside DashboardPage or a redirect wrapper: const { isFirstRun, loading } = useFirstRunState() if (loading) return // or null if (isFirstRun) return ``` ### Updating setup_completed on Skip/Complete ```typescript // Source: existing SettingsPage pattern for profile updates import { supabase } from "@/lib/supabase" async function markSetupComplete(userId: string) { await supabase .from("profiles") .update({ setup_completed: true }) .eq("id", userId) } ``` ### Live Allocation Calculation ```typescript // Source: derived from CONTEXT decisions function computeRemaining(income: number, items: Record) { const totalExpenses = Object.values(items) .filter(i => i.checked) .reduce((sum, i) => sum + i.amount, 0) return income - totalExpenses } ``` ### Default Selected Items State ```typescript // Source: CONTEXT + PRESETS data structure import { PRESETS } from "@/data/presets" function getDefaultSelectedItems(): Record { const result: Record = {} for (const preset of PRESETS) { result[preset.slug] = { checked: preset.type === "bill" || preset.type === "variable_expense", amount: preset.defaultAmount, } } return result } ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | Multi-page wizard with URL-per-step | Single-page with internal state | Standard for SPA wizards | Simpler routing, better state control | | Form library (react-hook-form) for wizards | useState for simple forms | When forms have < 5 fields | Less boilerplate for simple cases | | Redux/Zustand for wizard state | localStorage + useState | For isolated flows | No global store pollution | ## Assumptions Log | # | Claim | Section | Risk if Wrong | |---|-------|---------|---------------| | A1 | shadcn checkbox installs via `npx shadcn@latest add checkbox` | Standard Stack | Low -- standard shadcn CLI pattern, easily correctable | | A2 | Category names for auto-creation should use i18n labels (e.g., t("categories.types.bill")) | Patterns | Medium -- if hardcoded English names used, DE users get English category names | ## Open Questions 1. **Category naming on creation** - What we know: Categories need a `name` field. Presets are grouped by `type`. - What's unclear: Should each preset create its own category (19 categories) or one category per type (6 categories)? - Recommendation: One category per type (6 max). The CONTEXT says "Create categories (from checked item types that don't already exist)" -- this confirms one-per-type. Multiple template items share the same category. 2. **Where to place the first-run redirect** - What we know: `useFirstRunState` returns `isFirstRun` based on categories/template data. - What's unclear: Should redirect live in DashboardPage, AppLayout, or a wrapper component? - Recommendation: Place in DashboardPage (the index route) since that's where first-run users land. Avoids affecting other routes. ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | None detected (no test config or test files found) | | Config file | none -- Wave 0 must create | | Quick run command | N/A | | Full suite command | N/A | ### Phase Requirements to Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | SETUP-01 | First-run user sees wizard, 3 steps | manual | Manual browser test | N/A | | SETUP-02 | 19 pre-filled items with amounts | manual | Visual verification | N/A | | SETUP-03 | Skip step/wizard works | manual | Click through wizard | N/A | | SETUP-04 | Remaining balance updates live | manual | Check/uncheck items | N/A | | SETUP-05 | Template created from selections | manual | Complete wizard, check /template | N/A | ### Wave 0 Gaps No test framework exists in this project. All verification is manual/visual. This is acceptable per the project's established pattern -- no test files exist anywhere in `src/`. ## Security Domain ### Applicable ASVS Categories | ASVS Category | Applies | Standard Control | |---------------|---------|-----------------| | V2 Authentication | no | Already handled by ProtectedRoute | | V3 Session Management | no | Supabase session management | | V4 Access Control | yes | RLS policies on categories/template_items (already in place) | | V5 Input Validation | yes | Income > 0 validation, amounts must be positive numbers | | V6 Cryptography | no | No crypto needed | ### Known Threat Patterns for This Phase | Pattern | STRIDE | Standard Mitigation | |---------|--------|---------------------| | localStorage tampering | Tampering | Validate localStorage data shape on load; sanitize numbers | | Mass insert abuse (spam completion) | Denial of Service | setup_completed flag prevents re-entry; RLS limits to own user | | XSS via preset slug injection | Spoofing | Presets are hardcoded in source, not user-input; i18n keys are safe | ## Sources ### Primary (HIGH confidence) - Project source code: `src/hooks/useFirstRunState.ts`, `src/hooks/useCategories.ts`, `src/hooks/useTemplate.ts`, `src/data/presets.ts`, `src/App.tsx`, `src/lib/types.ts` -- verified via direct file read - `package.json` -- verified all dependency versions - Phase 6 outputs: `supabase/migrations/007_setup_completed.sql` -- confirms DB schema ready ### Secondary (MEDIUM confidence) - UI-SPEC.md (07-UI-SPEC.md) -- authored design contract for this phase - CONTEXT.md (07-CONTEXT.md) -- user decisions from discuss phase ## Metadata **Confidence breakdown:** - Standard stack: HIGH - all dependencies verified in package.json, no new installs except shadcn checkbox - Architecture: HIGH - follows exact patterns established in existing pages (Login, Register, Settings) - Pitfalls: HIGH - derived from reading actual hook implementations and understanding race conditions **Research date:** 2026-04-20 **Valid until:** 2026-05-20 (stable -- no moving targets, all deps are pinned)