Files
SimpleFinanceDash/.planning/phases/07-setup-wizard/07-RESEARCH.md
2026-04-20 20:51:44 +02:00

21 KiB

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>

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. </user_constraints>

<phase_requirements>

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
</phase_requirements>

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:

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         <Navigate to="/setup">
                                            |
                                    [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
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:

// Source: project convention + React docs
interface WizardState {
  currentStep: 1 | 2 | 3
  income: number
  selectedItems: Record<string, { checked: boolean; amount: number }>
}

const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}`

function useWizardState(userId: string) {
  const [state, setState] = useState<WizardState>(() => {
    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:

// 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<string, string> = {}
  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:

// Source: existing App.tsx routing pattern
<Routes>
  {/* Public routes */}
  <Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
  <Route path="/register" element={<PublicRoute><RegisterPage /></PublicRoute>} />

  {/* Setup wizard -- protected but outside AppLayout */}
  <Route path="/setup" element={<ProtectedRoute><SetupPage /></ProtectedRoute>} />

  {/* Main app layout */}
  <Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
    <Route index element={<DashboardPage />} />
    ...
  </Route>
</Routes>

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)

// 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 <Skeleton /> // or null
if (isFirstRun) return <Navigate to="/setup" replace />

Updating setup_completed on Skip/Complete

// 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

// Source: derived from CONTEXT decisions
function computeRemaining(income: number, items: Record<string, { checked: boolean; amount: number }>) {
  const totalExpenses = Object.values(items)
    .filter(i => i.checked)
    .reduce((sum, i) => sum + i.amount, 0)
  return income - totalExpenses
}

Default Selected Items State

// Source: CONTEXT + PRESETS data structure
import { PRESETS } from "@/data/presets"

function getDefaultSelectedItems(): Record<string, { checked: boolean; amount: number }> {
  const result: Record<string, { checked: boolean; amount: number }> = {}
  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)