diff --git a/.claude/scripts/enforce-tools.sh b/.claude/scripts/enforce-tools.sh new file mode 100755 index 00000000..4e2edc1c --- /dev/null +++ b/.claude/scripts/enforce-tools.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# PreToolUse hook: Block Bash commands that should use dedicated tools. +# Runs before every Bash call (main agent + subagents). +# Exit 0 = allow, Exit 2 = block with message. + +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# Strip leading whitespace and env vars (e.g. FOO=bar cat file) +CLEAN=$(echo "$COMMAND" | sed 's/^[[:space:]]*//' | sed 's/^[A-Za-z_][A-Za-z_0-9]*=[^ ]* *//') + +# Extract the first word (the actual command) +FIRST=$(echo "$CLEAN" | awk '{print $1}' | sed 's|.*/||') + +case "$FIRST" in + cat) + echo "Use the Read tool instead of cat." >&2 + exit 2 + ;; + head|tail) + echo "Use the Read tool (with offset/limit) instead of $FIRST." >&2 + exit 2 + ;; + ls) + echo "Use the Glob tool instead of ls." >&2 + exit 2 + ;; + find) + echo "Use the Glob tool instead of find." >&2 + exit 2 + ;; + grep|rg) + echo "Use the Grep tool instead of $FIRST." >&2 + exit 2 + ;; + sed|awk) + echo "Use the Edit tool instead of $FIRST." >&2 + exit 2 + ;; + echo) + # Block echo used for file writing (echo > file, echo >> file) + if echo "$COMMAND" | grep -qE '>\s*\S'; then + echo "Use the Write or Edit tool instead of echo redirection." >&2 + exit 2 + fi + ;; +esac + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..545bd52b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": ".claude/scripts/enforce-tools.sh" + } + ] + } + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 5f5b763d..f8b038ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,211 +1,57 @@ # CLAUDE.md -Instructions for Claude Code working in this repository. - ---- - ## Agent Behavior -**Always use `pnpm`** — never use `npm`, `yarn`, or `npx`: +**Always use `pnpm`** — never `npm`, `yarn`, or `npx`. Use `pnpm exec` for local binaries, `pnpm dlx` for one-off execution. -- Use `pnpm exec` to run local binaries (e.g., `pnpm exec prisma migrate status`) -- Use `pnpm dlx` for one-off package execution (e.g., `pnpm dlx ts-prune`) +**Never run long-running processes** (dev servers, watchers, Docker) without explicit permission. -**Do NOT** run long-running processes without explicit permission: +**Read relevant docs before implementing** — never guess endpoint behavior or payload shapes. -- `pnpm dev`, `pnpm dev:start`, or any dev server commands -- Any command that starts servers, watchers, or blocking processes +**Use dedicated tools, not Bash** — Read (not `cat`/`head`/`tail`), Glob (not `find`/`ls`), Grep (not `grep`/`rg`), Edit (not `sed`/`awk`). This applies to all agents and subagents. -**Always ask first** before: - -- Starting development servers -- Running Docker containers -- Executing build watchers -- Any process that won't terminate on its own - -**Before coding**: Read relevant docs. Never guess endpoint behavior or payload shapes. - ---- - -## Quick Reference +## Commands ```bash -# Build domain (required after domain changes) -pnpm domain:build - -# Type check & lint +pnpm domain:build # Required after domain changes pnpm type-check pnpm lint - -# Database pnpm db:migrate pnpm db:generate - -# Tests -pnpm test -pnpm --filter @customer-portal/bff test +pnpm test # All tests +pnpm --filter @customer-portal/bff test # BFF tests only ``` -**Ports**: Frontend :3000 | Backend :4000/api | Prisma Studio :5555 - ---- +Ports: Frontend :3000 | Backend :4000/api | Prisma Studio :5555 ## Architecture -### Monorepo - ``` apps/ ├── portal/ # Next.js 15 (React 19, Tailwind, shadcn/ui) └── bff/ # NestJS 11 (Prisma, BullMQ, Zod) - packages/ -└── domain/ # Shared contracts, schemas, provider mappers +└── domain/ # Shared contracts, Zod schemas, provider mappers ``` -### Three-Layer Boundary +**Systems of record**: WHMCS (billing, subscriptions, invoices, addresses) | Salesforce (CRM, orders) | Portal (UI + BFF orchestration) -| Layer | Location | Purpose | -| ------ | ------------------ | -------------------------------------------------- | -| Domain | `packages/domain/` | Contracts, Zod schemas, provider mappers | -| BFF | `apps/bff/` | HTTP boundary, orchestration, integrations | -| Portal | `apps/portal/` | UI layer, thin route wrappers over feature modules | +## Key Conventions -### Systems of Record +**Imports** (ESLint enforced): -- **WHMCS**: Billing, subscriptions, invoices, authoritative addresses -- **Salesforce**: CRM (Accounts, Contacts, Cases), order snapshots -- **Portal**: UI + BFF orchestration +- Import from module index: `@customer-portal/domain/billing` — never root or deep paths +- Provider imports (`/providers`) are BFF-only, forbidden in Portal ---- +**Domain**: Each module has `contract.ts`, `schema.ts`, `index.ts`, and `providers/` (BFF-only mappers). Map once in mappers, use domain types everywhere. -## Import Rules (ESLint Enforced) +**Portal**: Pages in `app/` are thin shells — all logic lives in `features/` modules. No API calls in `app/`. Use `@/core/logger` not `console.log`. -```typescript -// Allowed (Portal + BFF) -import type { Invoice } from "@customer-portal/domain/billing"; -import { invoiceSchema } from "@customer-portal/domain/billing"; -import { Formatting } from "@customer-portal/domain/toolkit"; +**BFF**: Integration services fetch, transform via domain mappers, return domain types. Controllers are thin — use `createZodDto(schema)` + `ZodValidationPipe`. Use `nestjs-pino` logger not `console.log`. -// Allowed (BFF only) -import { Whmcs } from "@customer-portal/domain/billing/providers"; +**Validation**: Zod-first. Schemas in domain, derive types with `z.infer`. Use `z.coerce.*` for query params. -// Forbidden everywhere -import { Billing } from "@customer-portal/domain"; // root import -import { Invoice } from "@customer-portal/domain/billing/contract"; // deep import - -// Forbidden in Portal -import { Whmcs } from "@customer-portal/domain/billing/providers"; // provider adapters -``` - ---- - -## Domain Package - -Each module follows: - -``` -packages/domain// -├── contract.ts # Normalized types (provider-agnostic) -├── schema.ts # Zod validation -├── index.ts # Public exports -└── providers/ # Provider-specific (BFF-only) - └── whmcs/ - ├── raw.types.ts # Raw API response types - └── mapper.ts # Transform raw → domain -``` - -**Key principle**: Map once in domain mappers, use domain types everywhere. - ---- - -## Portal Structure - -``` -apps/portal/src/ -├── app/ # Next.js App Router (thin shells, NO API calls) -├── components/ # Atomic: atoms/ molecules/ organisms/ templates/ -├── core/ # Infrastructure: api/, logger/, providers/ -├── features/ # Feature modules (api/, stores/, hooks/, views/) -└── shared/ # Cross-feature: hooks/, utils/, constants/ -``` - -### Feature Module Pattern - -``` -features// -├── api/ # Data fetching (uses core/api/apiClient) -├── stores/ # Zustand state -├── hooks/ # React Query hooks -├── components/ # Feature UI -├── views/ # Page-level views -└── index.ts # Public exports (barrel) -``` - -**Rules**: - -- Pages are thin wrappers importing views from features -- No API calls in `app/` directory -- No business logic in frontend; use services and APIs - ---- - -## BFF Patterns - -### Integration Layer - -``` -apps/bff/src/integrations/{provider}/ -├── services/ -│ ├── {provider}-connection.service.ts -│ └── {provider}-{entity}.service.ts -└── utils/ - └── {entity}-query-builder.ts -``` - -### Data Flow - -``` -External API → Integration Service → Domain Mapper → Domain Type → Use Directly - (fetch + query) (transform once) (return) -``` - -**Integration services**: - -1. Build queries (SOQL, API params) -2. Execute API calls -3. Use domain mappers to transform -4. Return domain types -5. NO additional mapping or business logic - -### Controllers - -- Thin: no business logic, no Zod imports -- Use `createZodDto(schema)` + global `ZodValidationPipe` - ---- - -## Validation (Zod-First) - -- Schemas in domain: `packages/domain//schema.ts` -- Derive types: `export type X = z.infer` -- Query params: use `z.coerce.*` for URL strings - ---- - -## Code Standards - -- No `any` in public APIs -- No `console.log` (use logger: `nestjs-pino` for BFF, `@/core/logger` for Portal) -- Avoid `V2` suffix in service names -- No unsafe assertions -- Reuse existing types and functions; extend when needed - ---- - -## Documentation - -Read before implementing: +## Docs | Topic | Location | | ------------------- | ---------------------------------------------- | diff --git a/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts index 8c1f0ece..c492fd00 100644 --- a/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts @@ -693,7 +693,7 @@ export class GetStartedWorkflowService { * 1. Validate session has WHMCS client ID * 2. Verify WHMCS client still exists * 3. Find Salesforce account (by email or customer number) - * 4. Update WHMCS client with new password + DOB + gender + * 4. Update WHMCS client with new password (DOB/gender only if provided) * 5. Create portal user with hashed password * 6. Create ID mapping * 7. Update Salesforce portal flags @@ -757,7 +757,7 @@ export class GetStartedWorkflowService { // Hash password for portal storage const passwordHash = await argon2.hash(password); - // Update WHMCS client with new password + DOB + gender + // Update WHMCS client with new password (DOB/gender only if provided) await this.updateWhmcsClientForMigration(whmcsClient.id, password, dateOfBirth, gender); // Create portal user and ID mapping @@ -875,15 +875,15 @@ export class GetStartedWorkflowService { private async updateWhmcsClientForMigration( clientId: number, password: string, - dateOfBirth: string, - gender: string + dateOfBirth?: string, + gender?: string ): Promise { const dobFieldId = this.config.get("WHMCS_DOB_FIELD_ID"); const genderFieldId = this.config.get("WHMCS_GENDER_FIELD_ID"); const customfieldsMap: Record = {}; - if (dobFieldId) customfieldsMap[dobFieldId] = dateOfBirth; - if (genderFieldId) customfieldsMap[genderFieldId] = gender; + if (dobFieldId && dateOfBirth) customfieldsMap[dobFieldId] = dateOfBirth; + if (genderFieldId && gender) customfieldsMap[genderFieldId] = gender; const updateData: Record = { password2: password, diff --git a/apps/bff/src/modules/auth/infra/workflows/signup-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/signup-workflow.service.ts index 21ff3cb6..ded3b934 100644 --- a/apps/bff/src/modules/auth/infra/workflows/signup-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/signup-workflow.service.ts @@ -2,6 +2,7 @@ import { ConflictException, Inject, Injectable } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import * as argon2 from "argon2"; import type { Request } from "express"; +import { DistributedLockService } from "@bff/infra/cache/distributed-lock.service.js"; import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js"; import { UsersService } from "@bff/modules/users/application/users.service.js"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; @@ -50,6 +51,7 @@ export class SignupWorkflowService { private readonly signupValidation: SignupValidationService, private readonly whmcsSignup: SignupWhmcsService, private readonly userCreation: SignupUserCreationService, + private readonly lockService: DistributedLockService, @Inject(Logger) private readonly logger: Logger ) {} @@ -104,104 +106,113 @@ export class SignupWorkflowService { const passwordHash = await argon2.hash(password); - try { - // Step 1: Check for existing WHMCS client before provisioning in Salesforce - await this.whmcsSignup.checkExistingClient(email.toLowerCase().trim()); + const lockKey = `signup:${email.toLowerCase().trim()}`; + return await this.lockService.withLock( + lockKey, + async () => { + try { + // Step 1: Check for existing WHMCS client before provisioning in Salesforce + await this.whmcsSignup.checkExistingClient(email.toLowerCase().trim()); - // Step 2: Validate WHMCS data requirements - this.whmcsSignup.validateAddressData(signupData); + // Step 2: Validate WHMCS data requirements + this.whmcsSignup.validateAddressData(signupData); - // Step 3: Resolve or create Salesforce account - const { snapshot: accountSnapshot, customerNumber: customerNumberForWhmcs } = - await this.accountResolver.resolveOrCreate(signupData); + // Step 3: Resolve or create Salesforce account + const { snapshot: accountSnapshot, customerNumber: customerNumberForWhmcs } = + await this.accountResolver.resolveOrCreate(signupData); - const normalizedCustomerNumber = this.accountResolver.normalizeCustomerNumber( - signupData.sfNumber - ); - if (normalizedCustomerNumber) { - const existingMapping = await this.mappingsService.findBySfAccountId(accountSnapshot.id); - if (existingMapping) { - throw new ConflictException( - "You already have an account. Please use the login page to access your existing account." + const normalizedCustomerNumber = this.accountResolver.normalizeCustomerNumber( + signupData.sfNumber ); + if (normalizedCustomerNumber) { + const existingMapping = await this.mappingsService.findBySfAccountId( + accountSnapshot.id + ); + if (existingMapping) { + throw new ConflictException( + "You already have an account. Please use the login page to access your existing account." + ); + } + } + + // Step 4: Create WHMCS client + // Address fields are validated by validateAddressData, safe to assert non-null + const whmcsClient = await this.whmcsSignup.createClient({ + firstName, + lastName, + email, + password, + ...(company ? { company } : {}), + phone: phone, + address: { + address1: address!.address1!, + ...(address?.address2 ? { address2: address.address2 } : {}), + city: address!.city!, + state: address!.state!, + postcode: address!.postcode!, + country: address!.country!, + }, + customerNumber: customerNumberForWhmcs, + ...(dateOfBirth ? { dateOfBirth } : {}), + ...(gender ? { gender } : {}), + ...(nationality ? { nationality } : {}), + }); + + // Step 5: Create user and mapping in database + const { userId } = await this.userCreation.createUserWithMapping({ + email, + passwordHash, + whmcsClientId: whmcsClient.clientId, + sfAccountId: accountSnapshot.id, + }); + + // Step 6: Fetch fresh user and generate tokens + const freshUser = await this.usersService.findByIdInternal(userId); + if (!freshUser) { + throw new Error("Failed to load created user"); + } + + await this.auditService.logAuthEvent( + AuditAction.SIGNUP, + userId, + { email, whmcsClientId: whmcsClient.clientId }, + request, + true + ); + + const profile = mapPrismaUserToDomain(freshUser); + const tokens = await this.tokenService.generateTokenPair({ + id: profile.id, + email: profile.email, + }); + + // Step 7: Update Salesforce portal flags (non-blocking) + await this.updateSalesforcePortalFlags( + accountSnapshot.id, + PORTAL_SOURCE_NEW_SIGNUP, + whmcsClient.clientId + ); + + return { + user: profile, + tokens, + }; + } catch (error) { + await this.auditService.logAuthEvent( + AuditAction.SIGNUP, + undefined, + { email, error: extractErrorMessage(error) }, + request, + false, + extractErrorMessage(error) + ); + + this.logger.error("Signup error", { error: extractErrorMessage(error) }); + throw error; } - } - - // Step 4: Create WHMCS client - // Address fields are validated by validateAddressData, safe to assert non-null - const whmcsClient = await this.whmcsSignup.createClient({ - firstName, - lastName, - email, - password, - ...(company ? { company } : {}), - phone: phone, - address: { - address1: address!.address1!, - ...(address?.address2 ? { address2: address.address2 } : {}), - city: address!.city!, - state: address!.state!, - postcode: address!.postcode!, - country: address!.country!, - }, - customerNumber: customerNumberForWhmcs, - ...(dateOfBirth ? { dateOfBirth } : {}), - ...(gender ? { gender } : {}), - ...(nationality ? { nationality } : {}), - }); - - // Step 5: Create user and mapping in database - const { userId } = await this.userCreation.createUserWithMapping({ - email, - passwordHash, - whmcsClientId: whmcsClient.clientId, - sfAccountId: accountSnapshot.id, - }); - - // Step 6: Fetch fresh user and generate tokens - const freshUser = await this.usersService.findByIdInternal(userId); - if (!freshUser) { - throw new Error("Failed to load created user"); - } - - await this.auditService.logAuthEvent( - AuditAction.SIGNUP, - userId, - { email, whmcsClientId: whmcsClient.clientId }, - request, - true - ); - - const profile = mapPrismaUserToDomain(freshUser); - const tokens = await this.tokenService.generateTokenPair({ - id: profile.id, - email: profile.email, - }); - - // Step 7: Update Salesforce portal flags (non-blocking) - await this.updateSalesforcePortalFlags( - accountSnapshot.id, - PORTAL_SOURCE_NEW_SIGNUP, - whmcsClient.clientId - ); - - return { - user: profile, - tokens, - }; - } catch (error) { - await this.auditService.logAuthEvent( - AuditAction.SIGNUP, - undefined, - { email, error: extractErrorMessage(error) }, - request, - false, - extractErrorMessage(error) - ); - - this.logger.error("Signup error", { error: extractErrorMessage(error) }); - throw error; - } + }, + { ttlMs: 60_000 } + ); } /** diff --git a/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx b/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx index 8e9b020d..da53d17c 100644 --- a/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx +++ b/apps/portal/src/components/templates/AuthLayout/AuthLayout.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; -import { Logo } from "@/components/atoms/logo"; export interface AuthLayoutProps { children: React.ReactNode; @@ -27,7 +26,7 @@ export function AuthLayout({ const maxWidth = wide ? "max-w-xl" : "max-w-md"; return ( -
+
{showBackButton && (
@@ -42,11 +41,6 @@ export function AuthLayout({ )}
-
-
- -
-

{title}

{subtitle && (

@@ -56,13 +50,13 @@ export function AuthLayout({

-
+
{children}
{/* Trust indicator */} -
+

Secure login protected by SSL encryption diff --git a/apps/portal/src/features/auth/components/PasswordRequirements.tsx b/apps/portal/src/features/auth/components/PasswordRequirements.tsx index 7f3c2a3f..62c5a2ba 100644 --- a/apps/portal/src/features/auth/components/PasswordRequirements.tsx +++ b/apps/portal/src/features/auth/components/PasswordRequirements.tsx @@ -30,7 +30,7 @@ export function PasswordRequirements({ checks, showHint = false }: PasswordRequi if (showHint) { return (

- At least 8 characters with uppercase, lowercase, and numbers + At least 8 characters with uppercase, lowercase, numbers, and a special character

); } @@ -41,6 +41,7 @@ export function PasswordRequirements({ checks, showHint = false }: PasswordRequi +
); } diff --git a/apps/portal/src/features/auth/hooks/usePasswordValidation.ts b/apps/portal/src/features/auth/hooks/usePasswordValidation.ts index 94e440fb..149363a5 100644 --- a/apps/portal/src/features/auth/hooks/usePasswordValidation.ts +++ b/apps/portal/src/features/auth/hooks/usePasswordValidation.ts @@ -5,6 +5,7 @@ export interface PasswordChecks { hasUppercase: boolean; hasLowercase: boolean; hasNumber: boolean; + hasSpecialChar: boolean; } export interface PasswordValidation { @@ -19,6 +20,7 @@ export function validatePasswordRules(password: string): string | undefined { if (!/[A-Z]/.test(password)) return "Password must contain an uppercase letter"; if (!/[a-z]/.test(password)) return "Password must contain a lowercase letter"; if (!/[0-9]/.test(password)) return "Password must contain a number"; + if (!/[^A-Za-z0-9]/.test(password)) return "Password must contain a special character"; return undefined; } @@ -29,10 +31,15 @@ export function usePasswordValidation(password: string): PasswordValidation { hasUppercase: /[A-Z]/.test(password), hasLowercase: /[a-z]/.test(password), hasNumber: /[0-9]/.test(password), + hasSpecialChar: /[^A-Za-z0-9]/.test(password), }; const isValid = - checks.minLength && checks.hasUppercase && checks.hasLowercase && checks.hasNumber; + checks.minLength && + checks.hasUppercase && + checks.hasLowercase && + checks.hasNumber && + checks.hasSpecialChar; const error = validatePasswordRules(password); return { checks, isValid, error }; diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx index 13f0266f..bd885fed 100644 --- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx @@ -147,10 +147,6 @@ export function AccountStatusStep() {

What you'll add:

    -
  • - - Date of birth -
  • New portal password diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx index 1dde844f..748ef27c 100644 --- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx @@ -16,6 +16,7 @@ import { useRouter } from "next/navigation"; import { PrefilledUserInfo, NewCustomerFields, + AddressFields, PersonalInfoFields, PasswordSection, useCompleteAccountForm, @@ -45,6 +46,17 @@ export function CompleteAccountStep() { const isNewCustomer = accountStatus === "new_customer"; const hasPrefill = !!(prefill?.firstName || prefill?.lastName); + // Show address form if new customer OR if SF-unmapped with incomplete address + const prefillAddress = prefill?.address; + const hasCompleteAddress = !!( + prefillAddress?.address1 && + prefillAddress?.city && + prefillAddress?.state && + prefillAddress?.postcode + ); + const isSfUnmappedWithIncompleteAddress = accountStatus === "sf_unmapped" && !hasCompleteAddress; + const needsAddress = isNewCustomer || isSfUnmappedWithIncompleteAddress; + const form = useCompleteAccountForm({ initialValues: { firstName: formData.firstName || prefill?.firstName, @@ -56,6 +68,7 @@ export function CompleteAccountStep() { marketingConsent: formData.marketingConsent, }, isNewCustomer, + needsAddress, updateFormData, }); @@ -93,6 +106,14 @@ export function CompleteAccountStep() { /> )} + {isSfUnmappedWithIncompleteAddress && ( + + )} + (formData.gender); const [acceptTerms, setAcceptTerms] = useState(formData.acceptTerms); const [marketingConsent, setMarketingConsent] = useState(formData.marketingConsent); const [localErrors, setLocalErrors] = useState({}); @@ -73,14 +70,6 @@ export function MigrateAccountStep() { errors.confirmPassword = "Passwords do not match"; } - if (!dateOfBirth) { - errors.dateOfBirth = "Date of birth is required"; - } - - if (!gender) { - errors.gender = "Please select a gender"; - } - if (!acceptTerms) { errors.acceptTerms = "You must accept the terms of service"; } @@ -96,11 +85,7 @@ export function MigrateAccountStep() { return; } - // Update form data (except password/confirmPassword - they're security-sensitive - // and should not be persisted in the store) updateFormData({ - dateOfBirth, - gender: gender as "male" | "female" | "other", acceptTerms, marketingConsent, }); @@ -113,7 +98,7 @@ export function MigrateAccountStep() { } }; - const canSubmit = password && confirmPassword && dateOfBirth && gender && acceptTerms; + const canSubmit = password && confirmPassword && acceptTerms; return (
    @@ -180,55 +165,6 @@ export function MigrateAccountStep() { )}
    - {/* Date of Birth */} -
    - - { - setDateOfBirth(e.target.value); - setLocalErrors(prev => ({ ...prev, dateOfBirth: undefined })); - }} - disabled={loading} - error={localErrors.dateOfBirth} - max={new Date().toISOString().split("T")[0]} - /> - {localErrors.dateOfBirth && ( -

    {localErrors.dateOfBirth}

    - )} -
    - - {/* Gender */} -
    - -
    - {(["male", "female", "other"] as const).map(option => ( - - ))} -
    - {localErrors.gender &&

    {localErrors.gender}

    } -
    - {/* Password */}