diff --git a/.planning/phases/07-setup-wizard/07-RESEARCH.md b/.planning/phases/07-setup-wizard/07-RESEARCH.md new file mode 100644 index 0000000..ac2e003 --- /dev/null +++ b/.planning/phases/07-setup-wizard/07-RESEARCH.md @@ -0,0 +1,433 @@ +# 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)