149 lines
9.9 KiB
Markdown
149 lines
9.9 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
||
|
||
# Capability Spec Delta: react-auth (add-react-auth-forms)
|
||
|
||
**Package**: `@suaveplan/react-auth`
|
||
**Status**: PROPOSED
|
||
|
||
> Every requirement carries a `**Verified by:**` line authored at draft time.
|
||
|
||
## ADDED Requirements
|
||
|
||
### Requirement: LoginForm renders a Zod-validated sign-in form with email, password, and remember-me
|
||
|
||
The package SHALL expose a `LoginForm` component that renders a card-layout sign-in form with email + password + remember-me fields. The form SHALL validate via `react-hook-form` + `zodResolver` against an internal `loginSchema` requiring email to be a non-empty valid email and password to be at least 8 characters. Submit SHALL invoke the caller-supplied `onSubmit({ email, password, rememberMe })`. The component SHALL render a destructive `Alert` when `error` is non-null, disable the submit button when `isLoading` or `formState.isSubmitting` is true, and accept a `translations` prop overriding the default `enGb.auth.login` strings.
|
||
|
||
#### Scenario: invalid email shows Zod error on blur
|
||
|
||
- **WHEN** the user types `not-an-email` into the email field and blurs out
|
||
- **THEN** the text "Invalid email address" appears
|
||
|
||
#### Scenario: short password rejected
|
||
|
||
- **WHEN** the user submits with password `"abc"`
|
||
- **THEN** the text "Password must be at least 8 characters" appears and `onSubmit` is NOT called
|
||
|
||
#### Scenario: happy path submit
|
||
|
||
- **WHEN** the user fills `alice@example.com` + `correcthorsebatterystaple` and clicks Sign in
|
||
- **THEN** `onSubmit({ email: "alice@example.com", password: "correcthorsebatterystaple", rememberMe: true })` is called
|
||
|
||
#### Scenario: error prop renders alert
|
||
|
||
- **WHEN** `error={new Error("Invalid credentials")}` is passed
|
||
- **THEN** an alert containing "Invalid credentials" is in the document
|
||
|
||
#### Scenario: translations prop overrides defaults
|
||
|
||
- **WHEN** `translations={{ submit: "Connexion" }}` is passed
|
||
- **THEN** the submit button displays "Connexion"
|
||
|
||
**Verified by:** `src/components/login-form/login-form.test.tsx::"shows Zod error when email is invalid"` AND `src/components/login-form/login-form.test.tsx::"shows Zod error when password is shorter than 8 characters"` AND `src/components/login-form/login-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/login-form/login-form.test.tsx::"renders the error prop in a destructive Alert"` AND `src/components/login-form/login-form.test.tsx::"overrides the submit copy when translations prop is supplied"`
|
||
|
||
---
|
||
|
||
### Requirement: RegisterForm renders a Zod-validated sign-up form with cross-field password match
|
||
|
||
The package SHALL expose a `RegisterForm` component rendering email, username, password, confirm-password, optional first-name, surname, date-of-birth, sex (4-option select), gender, company fields. The Zod schema SHALL enforce email validity, username `^[a-zA-Z0-9_]+$` with length 3–30, password ≥ 8 chars, and a `confirmPassword` cross-field refinement matching `password`. Submit SHALL fire only when validation passes. The component SHALL accept `error`, `isLoading`, and `translations` props mirroring `LoginForm` semantics.
|
||
|
||
#### Scenario: confirmPassword mismatch blocks submit
|
||
|
||
- **WHEN** password `"longenough1"` and confirmPassword `"longenough2"` are submitted
|
||
- **THEN** "Passwords don't match" appears under the confirm field and `onSubmit` is NOT called
|
||
|
||
#### Scenario: username regex enforced
|
||
|
||
- **WHEN** username `"has space"` is submitted
|
||
- **THEN** the Zod regex error appears and `onSubmit` is NOT called
|
||
|
||
#### Scenario: happy path submit
|
||
|
||
- **WHEN** every required field is valid and the form is submitted
|
||
- **THEN** `onSubmit` receives the full typed payload including the default `sex: "prefer-not-to-say"`
|
||
|
||
**Verified by:** `src/components/register-form/register-form.test.tsx::"blocks submit when passwords do not match"` AND `src/components/register-form/register-form.test.tsx::"rejects usernames that do not match the regex"` AND `src/components/register-form/register-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/register-form/register-form.test.tsx::"renders the error prop in a destructive Alert"`
|
||
|
||
---
|
||
|
||
### Requirement: ForgotPasswordForm renders an email-only Zod-validated form with success view
|
||
|
||
The package SHALL expose a `ForgotPasswordForm` component rendering an email-only form. When `isSuccess` is true the component SHALL switch to a confirmation card displaying `emailSent`, a "send another" button wired to `onReset`, and a "back to login" link. Otherwise it SHALL render the email form with Zod validation, a destructive alert when `error` is non-null, and a submit button disabled while `isLoading` or `isSubmitting`. A `translations` prop SHALL override `enGb.auth.forgotPassword` defaults.
|
||
|
||
#### Scenario: invalid email blocked
|
||
|
||
- **WHEN** the user submits "not-email"
|
||
- **THEN** "Invalid email address" appears and `onSubmit` is NOT called
|
||
|
||
#### Scenario: success view rendered
|
||
|
||
- **WHEN** `isSuccess` is true and `emailSent="alice@example.com"`
|
||
- **THEN** the document contains "alice@example.com" and a button whose click calls `onReset`
|
||
|
||
**Verified by:** `src/components/forgot-password-form/forgot-password-form.test.tsx::"blocks submit when email is invalid"` AND `src/components/forgot-password-form/forgot-password-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/forgot-password-form/forgot-password-form.test.tsx::"renders the success view with emailSent and a working onReset button"` AND `src/components/forgot-password-form/forgot-password-form.test.tsx::"renders the error prop in a destructive Alert"`
|
||
|
||
---
|
||
|
||
### Requirement: ResetPasswordForm renders new+confirm password with success and invalid-token views
|
||
|
||
The package SHALL expose a `ResetPasswordForm` component supporting three view modes: form (default), success (`isSuccess=true`), and invalid token (`hasToken=false`). The schema SHALL require both `password` and `confirmPassword` ≥ 8 chars with a cross-field match refinement. The success view SHALL render a "sign in" CTA wired to `onNavigateToLogin`; the invalid-token view SHALL render a "request new link" CTA wired to `onNavigateToForgotPassword`. The form view SHALL accept `error`, `isLoading`, and `translations` props.
|
||
|
||
#### Scenario: password mismatch blocks submit
|
||
|
||
- **WHEN** password `"longenough1"` and confirm `"longenough2"` are submitted
|
||
- **THEN** "Passwords don't match" appears and `onSubmit` is NOT called
|
||
|
||
#### Scenario: success view renders CTA
|
||
|
||
- **WHEN** `isSuccess=true`
|
||
- **THEN** clicking the Sign in CTA calls `onNavigateToLogin`
|
||
|
||
#### Scenario: invalid token view renders CTA
|
||
|
||
- **WHEN** `hasToken=false`
|
||
- **THEN** clicking the Request new reset link CTA calls `onNavigateToForgotPassword`
|
||
|
||
**Verified by:** `src/components/reset-password-form/reset-password-form.test.tsx::"blocks submit when passwords do not match"` AND `src/components/reset-password-form/reset-password-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/reset-password-form/reset-password-form.test.tsx::"renders the success view with a working onNavigateToLogin CTA"` AND `src/components/reset-password-form/reset-password-form.test.tsx::"renders the invalid-token view with a working onNavigateToForgotPassword CTA"`
|
||
|
||
---
|
||
|
||
### Requirement: EmailVerificationView renders four status modes with caller-supplied translations
|
||
|
||
The package SHALL expose an `EmailVerificationView` component accepting `status: "verifying" | "success" | "expired" | "error"`. Each status SHALL render a Card with an icon, title, description, and (where applicable) a CTA wired to one of `onContinueToLogin` / `onRegisterAgain` / `onTryAgain`. The `error` status SHALL render a destructive Alert; the `error` retry CTA SHALL be disabled while `isLoading`. A `translations` prop SHALL override `enGb.auth.emailVerification` defaults; an optional `message` prop SHALL override the default status description.
|
||
|
||
#### Scenario: verifying state renders spinner and copy
|
||
|
||
- **WHEN** `status="verifying"`
|
||
- **THEN** a `role="status"` spinner is present and the default English "Verifying email..." copy is visible
|
||
|
||
#### Scenario: success CTA fires onContinueToLogin
|
||
|
||
- **WHEN** `status="success"` and the CTA is clicked
|
||
- **THEN** `onContinueToLogin` is called once
|
||
|
||
#### Scenario: error CTA fires onTryAgain and respects isLoading
|
||
|
||
- **WHEN** `status="error"` and `isLoading=false`
|
||
- **THEN** the retry CTA is enabled and clicking it calls `onTryAgain`
|
||
|
||
#### Scenario: expired CTA fires onRegisterAgain
|
||
|
||
- **WHEN** `status="expired"` and the CTA is clicked
|
||
- **THEN** `onRegisterAgain` is called once
|
||
|
||
**Verified by:** `src/components/email-verification-view/email-verification-view.test.tsx::"renders the verifying status with a spinner"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"fires onContinueToLogin from the success CTA"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"fires onTryAgain from the error CTA"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"fires onRegisterAgain from the expired CTA"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"overrides the default copy when translations prop is supplied"`
|
||
|
||
---
|
||
|
||
### Requirement: Form components are reachable from the root barrel
|
||
|
||
The `@suaveplan/react-auth` root entry SHALL re-export the 5 Wave 2 form components as named exports (`LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `EmailVerificationView`) with no `export *`. Each form SHALL also be reachable via a dedicated subpath export (`./components/login-form`, etc.) for tree-shaking.
|
||
|
||
#### Scenario: root barrel surface
|
||
|
||
- **WHEN** the root-barrel test enumerates `Object.keys(import("@suaveplan/react-auth"))`
|
||
- **THEN** every Wave 2 form symbol is present alongside the Wave 1 surface
|
||
|
||
**Verified by:** `src/index.test.ts::"the root barrel exports exactly the documented public surface"`
|
||
|
||
<!-- markdownlint-enable MD024 -->
|