# Add @suaveplan/react-auth — forms (Wave 2) **Package**: `@suaveplan/react-auth` **Status**: PROPOSED **Type**: feature - **Spec delta**: ./specs/react-auth/spec.md ## Why Wave 1 shipped the `@suaveplan/react-auth` core (client, provider, context, hooks, guard, translations data) but explicitly deferred the form components to Wave 2. Downstream apps need shipping login/register/forgot-password/reset-password/email-verification surfaces to wire up the Better Auth flow. Wave 2 layers the five pure-UI form components on top of Wave 1's core, consuming the existing `enGb`/`cyGb`/`zhCn` translation packs (which Wave 1 sized for this exact purpose) and `@suaveplan/react-ui` primitives. No Wave 1 surface is modified — these forms are purely additive. The upstream enterprise audit (`../suaveplan/enterprise/react-auth.md` §2.32, §3.4, §5.5) called out the absence of `.test.tsx` coverage for these five components as the dominant test gap in the package; Wave 2 closes that gap by shipping 100% covered, accessible, schema-validated forms. ## What changes - ADDED: `LoginForm` — pure UI form with email/password/remember-me, password show/hide, react-hook-form + Zod, "forgot password?" + "sign up" links. - ADDED: `RegisterForm` — pure UI form with email/username/password+confirm/optional name/dob/sex/gender/company, password show/hide on both fields, react-hook-form + Zod with cross-field `confirmPassword` refinement. - ADDED: `ForgotPasswordForm` — pure UI form with email field, success view (with "send another" + "back to login"), react-hook-form + Zod. - ADDED: `ResetPasswordForm` — pure UI form with new password + confirm, success view, invalid-token view, react-hook-form + Zod with cross-field refinement. - ADDED: `EmailVerificationView` — pure UI status view rendering `verifying` / `success` / `expired` / `error` states with appropriate icon, copy, CTA, and `back-to-login` link. - ADDED: Form-scoped subpath exports under `./components/
` for every form module, mirroring Wave 1's per-component subpath convention. - ADDED: New translation keys consumed by the forms — every form accepts an optional `translations?: Partial` prop defaulting to the Wave 1 `enGb.auth.` table so existing `cyGb` / `zhCn` packs work end-to-end with zero new strings (the Wave 1 packs were sized to cover Wave 2 form copy). ## Out of scope - Page components (`Login`, `Register`, `ForgotPassword`, `ResetPassword`, `EmailVerify`, `Profile`) — deferred to Wave 3. - `PermissionGate` component — deferred to Wave 3. - MFA / passkey / magic-link flows — enterprise audit §4 enhancements, not in any current wave. ### Excluded from this migration - **Page components** (`pages/login`, `pages/register`, `pages/forgot-password`, `pages/reset-password`, `pages/email-verification`, `pages/profile`) — Deferred to Wave 3 — scope-bounded migration to keep ship velocity at 100/100/100/100 quality. Pages wrap the Wave 2 forms with `useAuth` action wiring + `useNavigate` redirects. - **`PermissionGate` component** — Deferred to Wave 3 — scope-bounded migration; ships next to the Wave 3 pages because the page surfaces are its primary consumer. - **MFA / passkey / magic-link surfaces** (`MfaChallengePage`, `MfaEnrolmentPage`, `PasskeyRegistrationPage`, `PasskeyLoginPage`) — Enterprise audit §4 enhancements, never part of upstream — out of scope for every current wave. ## Source mapping This change derives from `../suaveplan/enterprise/react-auth.md` (§1, §2.25, §2.30–32, §3.4, §5.5) and `../suaveplan/packages/react/react-auth/src/components/`. | Requirement | Source lines | Notes | |---|---|---| | `LoginForm` shape | `components/login-form/login-form.tsx:1-210` | Verbatim semantics; `resolver: zodResolver(...) as any` cast removed via typed resolver narrowing — no `any` survives §2. | | `RegisterForm` shape | `components/register-form/register-form.tsx:1-366` | Verbatim semantics; same `any` removal; split into `register-form.tsx` + `register-form-fields.tsx` to stay under the 300-line hard cap (§3). | | `ForgotPasswordForm` shape | `components/forgot-password-form/forgot-password-form.tsx:1-190` | Verbatim semantics; same `any` removal. | | `ResetPasswordForm` shape | `components/reset-password-form/reset-password-form.tsx:1-277` | Verbatim semantics; same `any` removal. | | `EmailVerificationView` shape | `components/email-verification-view/email-verification-view.tsx:1-220` | Reworded: upstream consumed `useTranslation` from `@suaveplan/react-translate`; Wave 1 settled this package as no-react-translate (`enterprise/react-auth.md §5.5` migration impact note in Wave 1 proposal). Wave 2 follows that precedent — the view accepts a `translations?: Partial` prop defaulting to `enGb.auth.emailVerification`. Callers wanting `cyGb` / `zhCn` pass that pack's slice. | | 5 sibling `.test.tsx` files | `enterprise/react-auth.md §3.4 / §5.5` | Upstream gap — these tests do not exist in upstream and are added in this wave per the audit. | ### Requirements with no source line | Requirement | Justification | |---|---| | Zod-validated `Props` boundary on every form | Genesis rule §0a — Zod boundary on every public option bundle. Upstream had `Props` interfaces but no runtime validation. | | `react-hook-form` typed resolver, no `as any` cast | Genesis rule §2 — `any` is banned. Upstream's `resolver: zodResolver(schema) as any` is replaced with a typed wrapper. | | 100% statement / branch / function / line coverage | Genesis-wide policy, not package-specific. | | Sibling `.md` per implementation file | Genesis rule §4. | | Sibling `.stories.tsx` per component | CLAUDE.md: "Every React component needs `.stories.tsx`." Upstream ships stories for pages / guards but not for these forms. | | `translations?: Partial<...>` prop on every form, defaulting to `enGb.auth.` | Genesis-specific — Wave 1 dropped `@suaveplan/react-translate` and parked translations as static packs. Wave 2 wires those packs in via prop injection to preserve the three-locale surface without re-introducing the dropped dep. | ## Verification approach Every form gets a sibling `.test.tsx` that uses `@suaveplan/testing/runner` and `@testing-library/react` with real `react-hook-form` + real Zod. No mocks. Each test file covers: - `src/components/login-form/login-form.test.tsx` — submit happy path, Zod validation errors (empty email, short password, bad email), password show/hide toggle, `error` prop renders alert, `isLoading` disables submit, `translations` prop overrides English defaults. - `src/components/register-form/register-form.test.tsx` — same matrix + username regex, `confirmPassword` mismatch, optional field flow, sex select changes. - `src/components/forgot-password-form/forgot-password-form.test.tsx` — submit happy path, Zod email error, `isSuccess` view renders email + `onReset` button, `error` alert, `translations` override. - `src/components/reset-password-form/reset-password-form.test.tsx` — submit happy path, short password Zod error, `confirmPassword` mismatch, `isSuccess` view, `hasToken=false` invalid-token view with `onNavigateToForgotPassword` CTA, password toggle, `translations` override. - `src/components/email-verification-view/email-verification-view.test.tsx` — `verifying` / `success` / `error` / `expired` branches, custom `message` overrides default copy, `onTryAgain` fires on error CTA, `isLoading` disables retry, `translations` override. - `src/index.test.ts` — extended to assert the 5 new symbols (`LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `EmailVerificationView`) appear in the root barrel. ## Migration impact - `package.json` gains three new dependencies — `@suaveplan/react-ui` (`workspace:^`), `react-hook-form` and `@hookform/resolvers` (both `catalog:forms`). The five form subpath exports are appended to the `exports` block. Vite `lib.entry` gains five new entries; rollup `external` gains `@suaveplan/react-ui`, `react-hook-form`, `@hookform/resolvers`. - No Wave 1 file is modified except `src/index.ts` (named re-exports for the 5 new components added — strictly additive) and `src/index.test.ts` (asserts the new symbols are present). - No downstream consumers yet — `@suaveplan/react-auth-whitelabel` is the only sibling and it ships independently in parallel. Verified by `bunx turbo ls --filter=...@suaveplan/react-auth` (empty list).