--- phase: 07-setup-wizard plan: 02 type: execute wave: 2 depends_on: [07-01] files_modified: - src/components/setup/ReviewStep.tsx - src/pages/SetupPage.tsx - src/App.tsx - src/pages/DashboardPage.tsx autonomous: true requirements: [SETUP-01, SETUP-03, SETUP-05] must_haves: truths: - "ReviewStep shows read-only summary of income + selected items grouped by type + totals" - "/setup route is protected and renders SetupPage outside AppLayout" - "DashboardPage redirects first-run users to /setup via useFirstRunState" - "Completing wizard creates categories then template items and redirects to dashboard" - "Skip setup clears localStorage, marks setup_completed=true, redirects to dashboard" - "Complete button is disabled during API calls to prevent double-submit" artifacts: - path: "src/components/setup/ReviewStep.tsx" provides: "Read-only summary of wizard selections" exports: ["ReviewStep"] - path: "src/App.tsx" provides: "/setup route registration" contains: "path=\"/setup\"" - path: "src/pages/DashboardPage.tsx" provides: "First-run redirect to /setup" contains: "useFirstRunState" key_links: - from: "src/pages/DashboardPage.tsx" to: "src/hooks/useFirstRunState.ts" via: "useFirstRunState() -> isFirstRun -> Navigate to /setup" pattern: "isFirstRun.*Navigate.*setup" - from: "src/pages/SetupPage.tsx" to: "src/hooks/useCategories.ts" via: "create.mutateAsync on wizard completion" pattern: "create\\.mutateAsync" - from: "src/pages/SetupPage.tsx" to: "src/hooks/useTemplate.ts" via: "createItem.mutateAsync on wizard completion" pattern: "createItem\\.mutateAsync" - from: "src/App.tsx" to: "src/pages/SetupPage.tsx" via: "Route path=/setup element=SetupPage" pattern: "path=.*/setup" --- Add the ReviewStep component, wire wizard completion logic (creating categories + template items), add skip functionality, register the /setup route, and add first-run redirect in DashboardPage. Purpose: Completes the wizard end-to-end — a new user is redirected to /setup, can navigate all 3 steps, and either completes (creating template data) or skips. Output: Fully functional setup wizard accessible via /setup with working completion and skip flows. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-setup-wizard/07-CONTEXT.md @.planning/phases/07-setup-wizard/07-RESEARCH.md @.planning/phases/07-setup-wizard/07-PATTERNS.md @.planning/phases/07-setup-wizard/07-UI-SPEC.md @.planning/phases/07-setup-wizard/07-01-SUMMARY.md From src/hooks/useWizardState.ts (created in Plan 01): ```typescript export interface WizardState { currentStep: 1 | 2 | 3 income: number selectedItems: Record } export function useWizardState(userId: string): { state: WizardState setStep: (step: 1 | 2 | 3) => void setIncome: (income: number) => void toggleItem: (slug: string) => void setItemAmount: (slug: string, amount: number) => void clearState: () => void } ``` From src/hooks/useCategories.ts: ```typescript // create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category { id, ... } ``` From src/hooks/useTemplate.ts: ```typescript // createItem.mutateAsync({ category_id: string, item_tier: "fixed"|"variable", budgeted_amount: number }) => TemplateItem // Template auto-creates on first useTemplate() query ``` From src/hooks/useFirstRunState.ts: ```typescript export function useFirstRunState(): { isFirstRun: boolean; loading: boolean } ``` From src/data/presets.ts: ```typescript export const PRESETS: PresetItem[] // 19 items with slug, type, defaultAmount, item_tier ``` Task 1: Create ReviewStep + wire completion/skip logic into SetupPage - src/pages/SetupPage.tsx - src/hooks/useCategories.ts - src/hooks/useTemplate.ts - src/hooks/useWizardState.ts - src/data/presets.ts - src/lib/supabase.ts src/components/setup/ReviewStep.tsx, src/pages/SetupPage.tsx **1. Create `src/components/setup/ReviewStep.tsx`:** Props: `income: number`, `selectedItems: Record`, `currency: string` Layout: - Income row: `flex justify-between py-2` — left: `t("setup.step3.incomeLabel")` (14px/600), right: formatted income (14px/600) - `` - Group checked items by type (same order: income, bill, variable_expense, debt, saving, investment). Only show groups that have at least one checked item. - For each group: render CategoryGroupHeader (import from `./CategoryGroupHeader`), then for each checked item in group: `
{t(\`presets.${type}.${slug}\`)}{formattedAmount}
` - `` - Totals: `space-y-1 pt-2` - Total expenses row: `flex justify-between` with label `t("setup.step3.totalLabel")` (14px/600) and sum of checked amounts - Remaining row: `flex justify-between` with label `t("setup.step3.remainingLabel")` (16px/600), value colored `text-on-budget` if >= 0, `text-destructive` if < 0 - If no items checked: show `

{t("setup.step3.empty")}

` instead of groups Use `Intl.NumberFormat` with `style: "currency"` and the `currency` prop for all amount formatting. Import PRESETS from `@/data/presets` to map slugs to types for grouping. **2. Update `src/pages/SetupPage.tsx` to add:** a) Import ReviewStep, useCategories, useTemplate, useQueryClient, toast (from sonner), supabase (from @/lib/supabase), Navigate (from react-router-dom), useNavigate b) Add `completing` state: `const [completing, setCompleting] = useState(false)` c) Replace the step 3 placeholder with `` d) Change the right-side button on step 3 from "Next Step" to "Complete Setup" (`t("setup.complete")`), disabled when `completing` e) Add completion handler `handleComplete`: ```typescript async function handleComplete() { setCompleting(true) try { const queryClient = useQueryClient() // already declared at top const checkedItems = PRESETS.filter(p => state.selectedItems[p.slug]?.checked) // 1. Determine unique category types needed const typesNeeded = [...new Set(checkedItems.map(i => i.type))] // 2. Create one category per type (use i18n label as name) const categoryMap: Record = {} for (const type of typesNeeded) { try { const cat = await create.mutateAsync({ name: t(`categories.types.${type}`), type }) categoryMap[type] = cat.id } catch (e: any) { // Unique constraint violation (23505) = category already exists, fetch it if (e?.code === "23505" || e?.message?.includes("duplicate")) { const existing = categories.find(c => c.type === type) if (existing) categoryMap[type] = existing.id } else { throw e } } } // 3. If categories were fetched from cache but not found, refetch if (Object.keys(categoryMap).length < typesNeeded.length) { await queryClient.invalidateQueries({ queryKey: ["categories"] }) // Remaining types need fresh data — handled by the existing entries } // 4. Create template items for each checked preset let partialFailure = false for (const item of checkedItems) { try { await createItem.mutateAsync({ category_id: categoryMap[item.type], item_tier: item.item_tier, budgeted_amount: state.selectedItems[item.slug].amount, }) } catch { partialFailure = true } } // 5. Mark setup complete await supabase.from("profiles").update({ setup_completed: true }).eq("id", user!.id) // 6. Clear wizard state clearState() // 7. Invalidate queries to prevent redirect loop await queryClient.invalidateQueries({ queryKey: ["categories"] }) await queryClient.invalidateQueries({ queryKey: ["template-items"] }) // 8. Toast and redirect if (partialFailure) { toast.error(t("setup.toast.partialError")) } else { toast.success(t("setup.toast.success")) } navigate("/", { replace: true }) } catch { toast.error(t("setup.toast.error")) } finally { setCompleting(false) } } ``` f) Add skip handler `handleSkipSetup`: ```typescript async function handleSkipSetup() { clearState() await supabase.from("profiles").update({ setup_completed: true }).eq("id", user!.id) navigate("/", { replace: true }) } ``` g) Wire "Skip setup" button below card to call `handleSkipSetup` h) Wire "Skip Step" on step 3 to also call `handleSkipSetup` (per UI-SPEC: "On step 3 skip: same as Skip setup") i) Disable Go Back, Skip Step, and Complete Setup buttons when `completing` is true j) Add `useCategories()` call at component top level to get `create` mutation and `categories` array. Add `useTemplate()` to ensure template row exists before completion (Pitfall 5). k) Get `useQueryClient()` at component top level (not inside handler).
grep -q "ReviewStep" src/components/setup/ReviewStep.tsx && grep -q "handleComplete" src/pages/SetupPage.tsx && grep -q "setup_completed.*true" src/pages/SetupPage.tsx && grep -q "clearState" src/pages/SetupPage.tsx && npx tsc --noEmit 2>&1 | head -20 - src/components/setup/ReviewStep.tsx contains `t("setup.step3.incomeLabel")` and `Intl.NumberFormat` - src/components/setup/ReviewStep.tsx contains `text-on-budget` and `text-destructive` conditional - src/pages/SetupPage.tsx contains `async function handleComplete` - src/pages/SetupPage.tsx contains `create.mutateAsync` (category creation) - src/pages/SetupPage.tsx contains `createItem.mutateAsync` (template item creation) - src/pages/SetupPage.tsx contains `setup_completed: true` (profile update) - src/pages/SetupPage.tsx contains `handleSkipSetup` function - src/pages/SetupPage.tsx contains `setCompleting(true)` (double-click prevention) - src/pages/SetupPage.tsx contains `toast.success` and `toast.error` - src/pages/SetupPage.tsx contains `invalidateQueries.*categories` (redirect loop prevention) - TypeScript compiles without errors ReviewStep renders read-only grouped summary. Completion creates categories + template items, handles duplicates, clears state, marks setup_completed, toasts, and redirects. Skip exits without creating data.
Task 2: Register /setup route in App.tsx + add first-run redirect in DashboardPage - src/App.tsx - src/pages/DashboardPage.tsx - src/hooks/useFirstRunState.ts src/App.tsx, src/pages/DashboardPage.tsx **1. Update `src/App.tsx`:** Add import at top: ```typescript import SetupPage from "@/pages/SetupPage" ``` Add the /setup route AFTER the public routes (login, register) and BEFORE the AppLayout route. It must be inside a ProtectedRoute but NOT inside AppLayout: ```typescript } /> ``` Insert this between the `` closing the register route (around line 46) and the `}>` line (around line 47). **2. Update `src/pages/DashboardPage.tsx`:** Add imports at top (after existing imports): ```typescript import { useFirstRunState } from "@/hooks/useFirstRunState" import { Navigate } from "react-router-dom" ``` Note: `Navigate` may already be imported — check before adding a duplicate. Inside the DashboardPage component function, add early returns BEFORE any existing logic (before the existing useState/useMemo calls): ```typescript const { isFirstRun, loading: firstRunLoading } = useFirstRunState() if (firstRunLoading) return if (isFirstRun) return ``` This uses the existing `DashboardSkeleton` component (already imported) as the loading fallback. grep -q 'path="/setup"' src/App.tsx && grep -q "SetupPage" src/App.tsx && grep -q "useFirstRunState" src/pages/DashboardPage.tsx && grep -q 'Navigate to="/setup"' src/pages/DashboardPage.tsx && npx tsc --noEmit 2>&1 | head -20 - src/App.tsx contains `import SetupPage from "@/pages/SetupPage"` - src/App.tsx contains `path="/setup"` within a `` wrapper - src/App.tsx has /setup route BEFORE the AppLayout route (not nested inside it) - src/pages/DashboardPage.tsx contains `import { useFirstRunState } from "@/hooks/useFirstRunState"` - src/pages/DashboardPage.tsx contains `const { isFirstRun, loading: firstRunLoading } = useFirstRunState()` - src/pages/DashboardPage.tsx contains `if (isFirstRun) return ` - src/pages/DashboardPage.tsx contains `if (firstRunLoading) return ` - TypeScript compiles without errors /setup route registered as protected standalone page. First-run users are redirected from dashboard to /setup. Existing users with categories/template data see the normal dashboard. Complete 3-step setup wizard with income entry, recurring items checklist (19 presets), review step, completion logic (category + template creation), skip functionality, and first-run redirect from dashboard. 1. Create a new test user (or clear an existing user's categories and template items in Supabase) 2. Log in with the test user — should be redirected to `/setup` 3. Step 1: Verify income input shows 3000 pre-filled with EUR suffix. Try Next with empty/0 value (should show validation error). Enter 4000, click Next. 4. Step 2: Verify Bills (4) and Variable Expenses (5) are pre-checked. Verify allocation bar shows "Remaining to allocate: X" (4000 - sum of checked). Uncheck an item — verify amount updates. Check an item — verify it enables the amount input. 5. Step 3: Verify read-only summary shows income, grouped checked items, totals, and remaining. 6. Click "Complete Setup" — verify toast appears, redirected to dashboard, template page shows created items. 7. Refresh dashboard — should NOT redirect back to /setup (setup_completed=true, and categories exist). 8. Test "Skip setup" link — create another fresh user, click "Skip setup" from step 1. Verify redirect to dashboard with no data created. 9. Test localStorage persistence: start wizard, enter income, advance to step 2, refresh page — verify wizard resumes at step 2 with entered income. Type "approved" or describe any issues found
## Trust Boundaries | Boundary | Description | |----------|-------------| | client -> Supabase | Category and template item inserts on completion | | localStorage -> component | Wizard state restoration (validated in Plan 01) | | React Query cache -> redirect logic | isFirstRun depends on cache freshness | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-07-04 | Denial of Service | handleComplete double-click | mitigate | `completing` state disables button immediately on click. DB unique constraints on categories prevent duplicate rows. | | T-07-05 | Tampering | Supabase profile update (setup_completed) | accept | RLS policy restricts updates to own profile row. Client can only set own setup_completed. | | T-07-06 | Elevation of Privilege | /setup route access | mitigate | Route wrapped in ProtectedRoute — unauthenticated users redirected to /login. | | T-07-07 | Denial of Service | Redirect loop (dashboard <-> /setup) | mitigate | After completion, invalidate ["categories"] and ["template-items"] queries before redirect. useFirstRunState will read fresh data showing isFirstRun=false. | - `npx tsc --noEmit` passes - Navigating to /setup while unauthenticated redirects to /login - A user with zero categories sees /setup after login - Completing wizard creates categories + template items in database - Skipping wizard sets setup_completed=true without creating data - Refreshing mid-wizard restores state from localStorage - No redirect loop after completion - New user flow: login -> auto-redirect to /setup -> 3 steps -> complete -> dashboard with template - Skip flow: /setup -> skip -> dashboard (no data created, setup_completed=true) - Existing user flow: login -> dashboard (no redirect to /setup) - No duplicate categories on repeated completion attempts - No infinite redirect loop between dashboard and /setup After completion, create `.planning/phases/07-setup-wizard/07-02-SUMMARY.md`