Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
plugins/document-navigator/docs/ui/react/react-auth/README.md
Normal file
126
plugins/document-navigator/docs/ui/react/react-auth/README.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# @suaveplan/react-auth - React authentication core: Better-Auth client wrapper, AuthProvider context, AuthGuard, useAuthGuard/usePermission hooks, locale packs.
|
||||
|
||||
React authentication core for Suaveplan apps — a typed wrapper around `better-auth/client`, an `AuthProvider` React context, an `AuthGuard` route component, `useAuthGuard` and `usePermission` hooks, a `betterAuthAdapter` response bridge, OTel instruments, a `REACT_AUTH_CODES` error registry, and shipped translation packs for `en-GB`, `cy-GB`, and `zh-CN`. Wave 1 (this package) ships the foundational core that downstream packages (the parallel `react-auth-whitelabel`, and consumer apps) build on; form components (Wave 2) and page components (Wave 3) layer on top without changing this surface.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [client](./src/client/auth-client.md) — `auth-client.ts` ships a typed `AuthClient` class plus the `createAuthClient(config)` factory that wraps `better-auth/c…
|
||||
- [auth-client](./src/client/auth-client.md) — `auth-client.ts` ships a typed `AuthClient` class plus the `createAuthClient(config)` factory that wraps `better-auth/c…
|
||||
- [components](./src/components/test-wrap.md) — Internal test helper consumed only by the Wave 2 form `.test.tsx` files in `src/components/<form>/`. The helper wraps a…
|
||||
- [auth-guard](./src/components/auth-guard/auth-guard.md) — `<AuthGuard>` is the declarative route gate. It reads the current auth state from the surrounding `AuthProvider`, evalu…
|
||||
- [auth-guard](./src/components/auth-guard/auth-guard.md) — `<AuthGuard>` is the declarative route gate. It reads the current auth state from the surrounding `AuthProvider`, evalu…
|
||||
- [auth-provider](./src/components/auth-provider/auth-provider.md) — `auth-provider.tsx` is a thin back-compatibility re-export shim that forwards `AuthContext`, `AuthProvider`, `useAuth`,…
|
||||
- [auth-provider](./src/components/auth-provider/auth-provider.md) — `auth-provider.tsx` is a thin back-compatibility re-export shim that forwards `AuthContext`, `AuthProvider`, `useAuth`,…
|
||||
- [email-verification-view](./src/components/email-verification-view/email-verification-view.md) — `<EmailVerificationView>` is the pure-UI surface that renders the four states of an email-verification flow — `verifyin…
|
||||
- [email-verification-view](./src/components/email-verification-view/email-verification-view.md) — `<EmailVerificationView>` is the pure-UI surface that renders the four states of an email-verification flow — `verifyin…
|
||||
- [forgot-password-form](./src/components/forgot-password-form/forgot-password-form.md) — `<ForgotPasswordForm>` is the pure-UI surface for requesting a password-reset email. It owns the email input, the Zod v…
|
||||
- [forgot-password-form](./src/components/forgot-password-form/forgot-password-form.md) — `<ForgotPasswordForm>` is the pure-UI surface for requesting a password-reset email. It owns the email input, the Zod v…
|
||||
- [login-form](./src/components/login-form/login-form.md) — `<LoginForm>` is a pure-UI sign-in form used by the Wave 3 `<Login>` page (and any custom consumer). It owns the render…
|
||||
- [login-form](./src/components/login-form/login-form.md) — `<LoginForm>` is a pure-UI sign-in form used by the Wave 3 `<Login>` page (and any custom consumer). It owns the render…
|
||||
- [permission-gate](./src/components/permission-gate/permission-gate.md) — `<PermissionGate>` is a declarative conditional renderer that consumes the Wave 1 `usePermission()` hook and decides wh…
|
||||
- [permission-gate](./src/components/permission-gate/permission-gate.md) — `<PermissionGate>` is a declarative conditional renderer that consumes the Wave 1 `usePermission()` hook and decides wh…
|
||||
- [register-form](./src/components/register-form/register-form.md) — `<RegisterForm>` is the pure-UI surface for account creation. It renders an identity grid (email + username + password…
|
||||
- [register-form-fields](./src/components/register-form/register-form-fields.md) — `<RegisterFormFields>` is an internal helper that owns the responsive identity-grid layout used by `<RegisterForm>`. It…
|
||||
- [register-form](./src/components/register-form/register-form.md) — `<RegisterForm>` is the pure-UI surface for account creation. It renders an identity grid (email + username + password…
|
||||
- [reset-password-form](./src/components/reset-password-form/reset-password-form.md) — `<ResetPasswordForm>` is the pure-UI surface for setting a new password after the user clicks a reset link. It owns thr…
|
||||
- [reset-password-form](./src/components/reset-password-form/reset-password-form.md) — `<ResetPasswordForm>` is the pure-UI surface for setting a new password after the user clicks a reset link. It owns thr…
|
||||
- [test-wrap](./src/components/test-wrap.md) — Internal test helper consumed only by the Wave 2 form `.test.tsx` files in `src/components/<form>/`. The helper wraps a…
|
||||
- [context](./src/context/auth-context.md) — `auth-context.tsx` exposes the React Context surface for the auth flow. It defines `AuthContext`, the `AuthProvider` co…
|
||||
- [auth-context](./src/context/auth-context.md) — `auth-context.tsx` exposes the React Context surface for the auth flow. It defines `AuthContext`, the `AuthProvider` co…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` defines the stable error-code registry for `@suaveplan/react-auth` plus the ten typed `ReactAuthError`…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` defines the stable error-code registry for `@suaveplan/react-auth` plus the ten typed `ReactAuthError`…
|
||||
- [use-auth-guard](./src/hooks/use-auth-guard.md) — `useAuthGuard(options)` is the imperative counterpart to the declarative `<AuthGuard>` component. It reads the current…
|
||||
- [use-permission](./src/hooks/use-permission.md) — `usePermission()` exposes a bitwise permission-check API over the current `user.roles` bitmask. The package ships a clo…
|
||||
- [lib](./src/lib/better-auth-adapter.md) — `better-auth-adapter.ts` provides the response-narrowing bridge between `better-auth/client` (which returns a `BetterAu…
|
||||
- [better-auth-adapter](./src/lib/better-auth-adapter.md) — `better-auth-adapter.ts` provides the response-narrowing bridge between `better-auth/client` (which returns a `BetterAu…
|
||||
- [observability](./src/observability/react-auth-metrics.md) — `react-auth-metrics.ts` builds the OTel instrument set for `@suaveplan/react-auth`. Instruments are constructed lazily…
|
||||
- [react-auth-metrics](./src/observability/react-auth-metrics.md) — `react-auth-metrics.ts` builds the OTel instrument set for `@suaveplan/react-auth`. Instruments are constructed lazily…
|
||||
- [email-verification](./src/pages/email-verification/email-verification.md) — `<EmailVerificationPage>` is the routing-aware adapter that wraps the Wave 2 `<EmailVerificationView>` for the `?token=…
|
||||
- [email-verification](./src/pages/email-verification/email-verification.md) — `<EmailVerificationPage>` is the routing-aware adapter that wraps the Wave 2 `<EmailVerificationView>` for the `?token=…
|
||||
- [forgot-password](./src/pages/forgot-password/forgot-password.md) — `<ForgotPasswordPage>` is the routing-aware adapter that wraps the Wave 2 `<ForgotPasswordForm>` with the navigation an…
|
||||
- [forgot-password](./src/pages/forgot-password/forgot-password.md) — `<ForgotPasswordPage>` is the routing-aware adapter that wraps the Wave 2 `<ForgotPasswordForm>` with the navigation an…
|
||||
- [login](./src/pages/login/login.md) — `<LoginPage>` is the routing-aware adapter that wraps the Wave 2 `<LoginForm>` with the navigation and auth-context wir…
|
||||
- [login](./src/pages/login/login.md) — `<LoginPage>` is the routing-aware adapter that wraps the Wave 2 `<LoginForm>` with the navigation and auth-context wir…
|
||||
- [profile](./src/pages/profile/profile.md) — `<ProfilePage>` is the routing-aware profile-settings surface for an authenticated user. It renders three tabs — Profil…
|
||||
- [profile-account-section](./src/pages/profile/profile-account-section.md) — `profile-account-section.tsx` houses the delete-account tab renderer for `<ProfilePage>`. It is split out of `profile-s…
|
||||
- [profile-sections](./src/pages/profile/profile-sections.md) — `profile-sections.tsx` houses the two read/write tab renderers for `<ProfilePage>`: `<ProfileSection>` (name + email up…
|
||||
- [profile](./src/pages/profile/profile.md) — `<ProfilePage>` is the routing-aware profile-settings surface for an authenticated user. It renders three tabs — Profil…
|
||||
- [register](./src/pages/register/register.md) — `<RegisterPage>` is the routing-aware adapter that wraps the Wave 2 `<RegisterForm>` with the navigation and auth-conte…
|
||||
- [register](./src/pages/register/register.md) — `<RegisterPage>` is the routing-aware adapter that wraps the Wave 2 `<RegisterForm>` with the navigation and auth-conte…
|
||||
- [reset-password](./src/pages/reset-password/reset-password.md) — `<ResetPasswordPage>` is the routing-aware adapter that wraps the Wave 2 `<ResetPasswordForm>` for the `?token=…`-beari…
|
||||
- [reset-password](./src/pages/reset-password/reset-password.md) — `<ResetPasswordPage>` is the routing-aware adapter that wraps the Wave 2 `<ResetPasswordForm>` for the `?token=…`-beari…
|
||||
- [cy-gb](./src/translations/cy-gb.md) — `cy-gb.ts` ships the Welsh (Cymraeg, `cy-GB`) translation pack for the auth surface. The file conforms structurally to…
|
||||
- [en-gb](./src/translations/en-gb.md) — `en-gb.ts` ships the British-English string table consumed by the Wave 2 forms and Wave 3 page components. The file als…
|
||||
- [zh-cn](./src/translations/zh-cn.md) — `zh-cn.ts` ships the Simplified-Chinese (`zh-CN`) translation pack for the auth surface. The file conforms structurally…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/react-auth
|
||||
```
|
||||
|
||||
Peer requirements: `react@^19`, `react-dom@^19`, `react-router-dom@^7`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AuthProvider,
|
||||
AuthGuard,
|
||||
createAuthClient,
|
||||
useAuth,
|
||||
} from "@suaveplan/react-auth";
|
||||
|
||||
const client = createAuthClient({
|
||||
baseURL: "https://auth.example.com",
|
||||
siteDomain: "tenant-a",
|
||||
});
|
||||
|
||||
export function App({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AuthProvider config={{ baseURL: "https://auth.example.com" }} client={client}>
|
||||
<AuthGuard requireAuth requireVerified fallback={<LoginRedirect />}>
|
||||
{children}
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
const { user, signOut } = useAuth();
|
||||
return (
|
||||
<>
|
||||
<h1>{user?.email}</h1>
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/client/auth-client.md`](./src/client/auth-client.md) — typed wrapper around `better-auth/client`, propagates `x-site-domain`.
|
||||
- [`src/context/auth-context.md`](./src/context/auth-context.md) — `AuthProvider` + `useAuth` + 11 actions.
|
||||
- [`src/components/auth-guard/auth-guard.md`](./src/components/auth-guard/auth-guard.md) — declarative gate component.
|
||||
- [`src/components/auth-provider/auth-provider.md`](./src/components/auth-provider/auth-provider.md) — back-compat re-export of the context surface.
|
||||
- [`src/hooks/use-auth-guard.md`](./src/hooks/use-auth-guard.md) — imperative `react-router-dom` redirect counterpart.
|
||||
- [`src/hooks/use-permission.md`](./src/hooks/use-permission.md) — bitwise permission check (16 flags).
|
||||
- [`src/lib/better-auth-adapter.md`](./src/lib/better-auth-adapter.md) — narrows `better-auth` response unions to internal shapes.
|
||||
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — `REACT_AUTH_CODES` registry + 10 typed error classes.
|
||||
- [`src/observability/react-auth-metrics.md`](./src/observability/react-auth-metrics.md) — counters + histogram under `suaveplan.react-auth`.
|
||||
- [`src/translations/en-gb.md`](./src/translations/en-gb.md) — British English locale pack + `AuthTranslationPack` shape.
|
||||
- [`src/translations/cy-gb.md`](./src/translations/cy-gb.md) — Welsh locale pack.
|
||||
- [`src/translations/zh-cn.md`](./src/translations/zh-cn.md) — Simplified Chinese locale pack.
|
||||
- [`src/types/auth.types.md`](./src/types/auth.types.md) — `User`, `Session`, credential / response shapes.
|
||||
- [`src/types/crud.types.md`](./src/types/crud.types.md) — multi-tenant CRUD shapes (Site, UserSettings, Friend, …).
|
||||
|
||||
## Wave scope
|
||||
|
||||
This package is the **Wave 1** core. The Wave 2 forms (`LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `EmailVerifyForm`) and Wave 3 page components (`Login`, `Register`, `ForgotPassword`, `ResetPassword`, `EmailVerify`, `Profile`, `PermissionGate`) extend this core without changing it.
|
||||
|
||||
## Observability
|
||||
|
||||
All instrumented call paths emit through `metrics.getMeter("suaveplan.react-auth")`. Counters: `react_auth.sign_in.total`, `react_auth.sign_up.total`, `react_auth.sign_out.total`, `react_auth.guard.allow.total`, `react_auth.guard.deny.total`. Histogram: `react_auth.session_fetch.duration_ms`.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# `auth-client`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`auth-client.ts` ships a typed `AuthClient` class plus the `createAuthClient(config)` factory that wraps `better-auth/client`. The wrapper centralises three concerns the underlying client leaves to the caller: (1) consistent injection of an optional `x-site-domain` header across every request, used for multi-tenant routing on the auth server; (2) a structural surface that does not depend on the dynamic plugin generic graph emitted by `better-auth` (each method we call is typed as an optional callable so plugin presence is detected at runtime and reported as a no-op when absent rather than as a compile-time error); and (3) normalisation of the `listSessions` payload from native `Date` to ISO string for serialisation safety. Configuration is validated through Zod at the boundary and a `ReactAuthConfigInvalidError` is thrown with code `REACT_AUTH.CONFIG_INVALID` when the URL or site domain is malformed.
|
||||
|
||||
## Features
|
||||
|
||||
- `AuthClient` class + `createAuthClient(config)` factory.
|
||||
- Optional `siteDomain` propagates as the `x-site-domain` header on every call.
|
||||
- Optional `fetch` injection seam for testing (no `vi.mock` required).
|
||||
- Zod-validated config; invalid input throws `ReactAuthConfigInvalidError`.
|
||||
- `listSessions()` normalises `Date` fields to ISO strings.
|
||||
- Every method is plugin-aware: when the underlying client lacks the method it returns `undefined` (or `[]` for `listSessions`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "@suaveplan/react-auth";
|
||||
|
||||
const client = createAuthClient({
|
||||
baseURL: "https://auth.example.com",
|
||||
siteDomain: "tenant-a",
|
||||
});
|
||||
|
||||
await client.signIn({ email: "a@b.com", password: "secret" });
|
||||
const session = await client.getSession();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a custom `fetch` (e.g. a recorder or a happy-path stub) without ever mocking the `better-auth` module:
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "@suaveplan/react-auth";
|
||||
|
||||
const captured: Request[] = [];
|
||||
const recordingFetch: typeof fetch = async (input, init) => {
|
||||
const req = new Request(input, init);
|
||||
captured.push(req);
|
||||
return new Response(JSON.stringify({ data: null }), { status: 200 });
|
||||
};
|
||||
|
||||
const client = createAuthClient({
|
||||
baseURL: "https://auth.example.com",
|
||||
siteDomain: "tenant-b",
|
||||
fetch: recordingFetch,
|
||||
});
|
||||
|
||||
await client.getSession();
|
||||
expect(captured[0].headers.get("x-site-domain")).toBe("tenant-b");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AuthClientConfig` — `baseURL: string` (URL), `siteDomain?: string`, `fetch?: typeof fetch`.
|
||||
- `class AuthClient`
|
||||
- `constructor(config: AuthClientConfig)` — validates `config`, throws `ReactAuthConfigInvalidError` on failure.
|
||||
- `signIn(credentials: LoginCredentials): Promise<unknown>`
|
||||
- `signUp(data: RegisterData): Promise<unknown>`
|
||||
- `signOut(): Promise<unknown>`
|
||||
- `getSession(): Promise<unknown>`
|
||||
- `forgotPassword(email: string): Promise<unknown>`
|
||||
- `resetPassword(data: { token: string; password: string }): Promise<unknown>`
|
||||
- `changePassword(data: ChangePasswordData): Promise<unknown>`
|
||||
- `verifyEmail(token: string): Promise<unknown>`
|
||||
- `resendVerificationEmail(email: string): Promise<unknown>`
|
||||
- `updateProfile(data: UpdateProfileData): Promise<unknown>`
|
||||
- `deleteAccount(password: string): Promise<unknown>`
|
||||
- `listSessions(): Promise<Session[]>` — Date fields ISO-normalised.
|
||||
- `revokeSession(sessionId: string): Promise<unknown>`
|
||||
- `revokeAllSessions(): Promise<unknown>`
|
||||
- `getRawClient(): BetterAuthClientLike` — escape hatch for plugin-specific access.
|
||||
- `function createAuthClient(config: AuthClientConfig): AuthClient` — thin factory.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The client does not throw on missing plugin methods. The upstream package emitted a single `@ts-expect-error` to work around `forgetPassword` not being statically typed; the wrapper replaces that with a structural interface (`BetterAuthClientLike`) where every method is optional. This keeps the call sites compile-safe while also covering deployments where the auth server registers a different plugin set. `listSessions` returns `[]` when no `data` array is present so callers can render an empty state without branching on `undefined`. The `x-site-domain` header is computed per-call from `this.config.siteDomain` so that future hot-swaps of the client config (out of scope for Wave 1) automatically pick up the new value. Constructor-time validation uses Zod with `z.string().url()` for `baseURL` — failing fast catches misconfiguration during app boot rather than on first request.
|
||||
@@ -0,0 +1,55 @@
|
||||
# `auth-guard`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<AuthGuard>` is the declarative route gate. It reads the current auth state from the surrounding `AuthProvider`, evaluates four optional predicates (`requireAuth`, `requireGuest`, `requireVerified`, `allowedRoles`), and renders one of three things: the configured `loadingComponent` while the auth state is loading, the `children` if every predicate passes, or the `fallback` element (defaulting to `null`) when a predicate fails. Unlike `useAuthGuard`, the guard does not navigate the user — it composes into the render tree, so callers can show a different sub-tree (a sign-in CTA, a 403 message) without hitting the router. Every deny path increments the `react_auth.guard.deny.total` counter with a `predicate` attribute; the allow path increments `react_auth.guard.allow.total`. Props are Zod-validated; misuse throws `ReactAuthGuardPropsInvalidError`.
|
||||
|
||||
## Features
|
||||
|
||||
- Four predicates: `requireAuth`, `requireGuest`, `requireVerified`, `allowedRoles`.
|
||||
- `loadingComponent` slot with a default `<Loader2>` spinner.
|
||||
- `fallback` slot for declarative deny UI.
|
||||
- `onUnauthorized` callback invoked on every deny path.
|
||||
- OTel allow/deny counters labelled by predicate.
|
||||
- Zod-validated props; misuse throws typed error.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { AuthGuard } from "@suaveplan/react-auth";
|
||||
|
||||
<AuthGuard requireAuth>
|
||||
<Dashboard />
|
||||
</AuthGuard>;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { AuthGuard, defaultAccessConfigs } from "@suaveplan/react-auth";
|
||||
|
||||
<AuthGuard
|
||||
{...defaultAccessConfigs.adminOnly}
|
||||
loadingComponent={<CustomLoader />}
|
||||
fallback={<AccessDeniedPanel />}
|
||||
onUnauthorized={() => audit.log("admin_panel_denied")}
|
||||
>
|
||||
<AdminPanel />
|
||||
</AuthGuard>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AuthGuardProps` — `{ children: ReactNode; redirectTo?: string; requireAuth?: boolean; requireGuest?: boolean; requireVerified?: boolean; allowedRoles?: readonly string[]; loadingComponent?: ReactNode; fallback?: ReactNode; onUnauthorized?: () => void }`.
|
||||
- `interface AccessConfig` — predicate-only block (no `children`), useful for sharing config across routes.
|
||||
- `interface RouteAccessConfig` — `{ public; protected; guestOnly; verifiedOnly; adminOnly }`.
|
||||
- `const defaultAccessConfigs: RouteAccessConfig` — pre-built configs.
|
||||
- `function AuthGuard(props: AuthGuardProps): ReactNode`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The guard renders `loadingComponent ?? defaultLoading()` while `isLoading === true`. The default loader uses `lucide-react`'s `<Loader2>` and a tailwind-flavoured spinner; consumers wanting a different visual override via the `loadingComponent` prop without monkey-patching styles. Each predicate failure increments a distinct deny counter so dashboards can surface which check is rejecting traffic. The `onUnauthorized` callback is invoked before the deny render, so analytics fires even when `fallback === undefined` and the component falls through to `null`. The `requireGuest` branch is useful for login pages where an authenticated user should be bounced back to `/`; the deny render returns `null` so the caller-level `useAuthGuard` can handle the actual redirect.
|
||||
@@ -0,0 +1,56 @@
|
||||
# `auth-provider`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`auth-provider.tsx` is a thin back-compatibility re-export shim that forwards `AuthContext`, `AuthProvider`, `useAuth`, and the associated types from the canonical implementation at `src/context/auth-context.tsx`. Upstream consumers of `@suaveplan/react-auth` historically imported `AuthProvider` from `@suaveplan/react-auth` (re-exported through `components/auth-provider/auth-provider.tsx`). The migration keeps that path stable so apps porting from upstream continue to compile without touching imports. Internally everything points at the single implementation; there is no duplicate state machine, no parallel context, and no diverging behaviour. Tests, stories, and Wave-2 forms will all consume the same context value regardless of which import path the caller picks.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable backward-compatible import path.
|
||||
- Single source of truth — re-exports the canonical context.
|
||||
- Identical runtime behaviour to the canonical surface.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { AuthProvider } from "@suaveplan/react-auth/components/auth-provider";
|
||||
|
||||
<AuthProvider config={{ baseURL: "https://auth.example.com" }}>
|
||||
<App />
|
||||
</AuthProvider>;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AuthContext,
|
||||
AuthProvider,
|
||||
type AuthContextValue,
|
||||
} from "@suaveplan/react-auth/components/auth-provider";
|
||||
import { useContext } from "react";
|
||||
|
||||
function DebugBadge() {
|
||||
const ctx = useContext(AuthContext) as AuthContextValue | undefined;
|
||||
return <span>{ctx?.isAuthenticated ? "in" : "out"}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
This module re-exports the following from `src/context/auth-context.tsx`:
|
||||
|
||||
- `AuthContext` — React Context handle.
|
||||
- `type AuthContextValue` — full context payload.
|
||||
- `AuthProvider` — provider component.
|
||||
- `type AuthProviderProps` — provider prop shape.
|
||||
- `type AuthState` — state-block shape.
|
||||
- `useAuth` — context-reading hook.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The re-export is a single `export { ... } from "../../context/auth-context.js"` statement so Tree-shaking sees both paths as the same module under `sideEffects: false`. No additional runtime code lives here — every symbol is forwarded by name with no namespace wrapping. The path exists purely for ergonomics: callers porting from upstream do not need to grep-replace import statements as part of the migration. Wave 2 form components and Wave 3 page components will continue to use the canonical `context/auth-context.js` path internally for their imports to keep the dependency tree flat.
|
||||
@@ -0,0 +1,118 @@
|
||||
# EmailVerificationView
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<EmailVerificationView>` is the pure-UI surface that renders the four states of an email-verification flow — `verifying`, `success`, `expired`, `error` — with the right icon, copy, and CTA per state. It is a status view rather than a form: there are no inputs, no validation, just a deterministic switch from `status` to one of four cards. The page-level host owns the token-extraction, the API call against `useAuth().verifyEmail`, and the status transitions; the view is a dumb renderer that can also be reused inside admin re-verification flows or as a notification banner.
|
||||
|
||||
## Features
|
||||
|
||||
- Four discrete render branches keyed off the `status` prop, each with its own icon (`Loader2` / `CheckCircle2` / `AlertTriangle` / `XCircle`).
|
||||
- Each branch ships a CTA wired to one of `onContinueToLogin` / `onRegisterAgain` / `onTryAgain` callbacks supplied by the host.
|
||||
- The `error` branch additionally renders a destructive `<Alert>` with the same message for screen-reader prominence.
|
||||
- The `error` retry CTA shows a spinner and is disabled while `isLoading` is true.
|
||||
- Optional `message` prop overrides the default per-status description copy.
|
||||
- `translations?: Partial<AuthEmailVerificationStrings>` for full Welsh / zh-CN coverage via Wave 1's translation packs.
|
||||
- Always renders a "back to login" link except in the `verifying` state (where no action is meaningful yet).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { EmailVerificationView } from "@suaveplan/react-auth";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
export function VerifyEmailPage() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<EmailVerificationView status="verifying" />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Drive every transition from URL state + `useAuth().verifyEmail`:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
EmailVerificationView,
|
||||
useAuth,
|
||||
} from "@suaveplan/react-auth";
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
export function VerifyEmail() {
|
||||
const { verifyEmail } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
const token = params.get("token");
|
||||
const [status, setStatus] = useState<
|
||||
"verifying" | "success" | "error" | "expired"
|
||||
>("verifying");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (token === null) {
|
||||
setStatus("expired");
|
||||
return;
|
||||
}
|
||||
verifyEmail({ token })
|
||||
.then(() => setStatus("success"))
|
||||
.catch((err: unknown) => {
|
||||
setStatus("error");
|
||||
setMessage(
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
});
|
||||
}, [token, verifyEmail]);
|
||||
|
||||
return (
|
||||
<EmailVerificationView
|
||||
status={status}
|
||||
message={message}
|
||||
onContinueToLogin={() => navigate("/auth/login")}
|
||||
onRegisterAgain={() => navigate("/auth/register")}
|
||||
onTryAgain={() => setStatus("verifying")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EmailVerificationView(props: EmailVerificationViewProps): ReactElement`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `status` | `"verifying" \| "success" \| "error" \| "expired"` | — required | Which card to render. |
|
||||
| `message` | `string` | `""` | Override for the default description copy. |
|
||||
| `onContinueToLogin` | `() => void` | — | Fired by the success-status CTA. |
|
||||
| `onRegisterAgain` | `() => void` | — | Fired by the expired-status CTA. |
|
||||
| `onTryAgain` | `() => void` | — | Fired by the error-status retry CTA. |
|
||||
| `isLoading` | `boolean` | `false` | Disables the error-status retry CTA. |
|
||||
| `translations` | `Partial<AuthEmailVerificationStrings>` | `enGb.auth.emailVerification` | Override any subset of the 8 copy keys. |
|
||||
|
||||
### `EmailVerificationStatus`
|
||||
|
||||
```ts
|
||||
type EmailVerificationStatus =
|
||||
| "verifying"
|
||||
| "success"
|
||||
| "error"
|
||||
| "expired";
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Each card is implemented as a small internal component (`VerifyingCard`, `SuccessCard`, `ExpiredCard`, `ErrorCard`) and dispatched through a `switch`-style helper to keep the public file under the 300-line cap.
|
||||
- The component intentionally does NOT consume `useTranslation` — Wave 1 dropped the `@suaveplan/react-translate` dependency, and Wave 2 follows that precedent. Translations are injected via the `translations` prop instead.
|
||||
- The `verifying` card carries `role="status"` on the spinner wrapper for assistive-tech announcements.
|
||||
- The error card duplicates the message in a destructive `<Alert>` so the failure is announced even when the CTA is off-screen on narrow viewports.
|
||||
- No OTel instruments emitted from the view; metrics live on the Wave 1 `verifyEmail` action.
|
||||
@@ -0,0 +1,110 @@
|
||||
# ForgotPasswordForm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<ForgotPasswordForm>` is the pure-UI surface for requesting a password-reset email. It owns the email input, the Zod validation, the destructive error alert, the "send another email" success card, and the link back to sign-in — but it never imports the Wave 1 `AuthClient` or `useNavigate`. The page-level host wires `onSubmit` to `useAuth().forgotPassword` and flips `isSuccess` once the call resolves. This split keeps the form trivially testable and reusable inside modals, embedded widgets, or admin tools where routing is owned by the host.
|
||||
|
||||
## Features
|
||||
|
||||
- React Hook Form + `zodResolver` against a one-field `forgotPasswordSchema` (`email` required and well-formed).
|
||||
- `mode: "onBlur"` — validation runs as the field is left, before the user smashes submit.
|
||||
- Destructive `<Alert>` slot rendering the caller-supplied `Error.message`.
|
||||
- Two render branches: the request form (default) and a success card (`isSuccess`) showing `emailSent` and an `onReset` "send another" button.
|
||||
- Submit button disables and spins while either `isSubmitting` or `isLoading` is true.
|
||||
- `translations?: Partial<AuthForgotPasswordStrings>` for full Welsh / zh-CN coverage via Wave 1's translation packs.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { ForgotPasswordForm } from "@suaveplan/react-auth";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
export function ForgotPasswordPage() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ForgotPasswordForm
|
||||
onSubmit={async ({ email }) => {
|
||||
console.log("requesting reset for", email);
|
||||
}}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire to the Wave 1 `useAuth().forgotPassword` action with success-card
|
||||
state and Welsh copy:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ForgotPasswordForm,
|
||||
cyGb,
|
||||
useAuth,
|
||||
} from "@suaveplan/react-auth";
|
||||
import { useState } from "react";
|
||||
|
||||
export function CymraegForgot() {
|
||||
const { forgotPassword } = useAuth();
|
||||
const [sentTo, setSentTo] = useState<string | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
return (
|
||||
<ForgotPasswordForm
|
||||
translations={cyGb.auth.forgotPassword}
|
||||
error={error}
|
||||
isSuccess={sentTo !== null}
|
||||
emailSent={sentTo ?? ""}
|
||||
onReset={() => {
|
||||
setSentTo(null);
|
||||
}}
|
||||
onSubmit={async ({ email }) => {
|
||||
setError(null);
|
||||
try {
|
||||
await forgotPassword({ email });
|
||||
setSentTo(email);
|
||||
} catch (caught) {
|
||||
setError(
|
||||
caught instanceof Error
|
||||
? caught
|
||||
: new Error(String(caught))
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ForgotPasswordForm(props: ForgotPasswordFormProps): ReactElement`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `onSubmit` | `(data: ForgotPasswordFormData) => Promise<void> \| void` | — required | Invoked once Zod accepts the payload. |
|
||||
| `error` | `Error \| null \| undefined` | `null` | Rendered in a destructive `<Alert>`. |
|
||||
| `isLoading` | `boolean` | `false` | Disables the submit button. |
|
||||
| `isSuccess` | `boolean` | `false` | Switches to the success card. |
|
||||
| `emailSent` | `string` | `""` | Email shown in the success card. |
|
||||
| `onReset` | `() => void` | — | Fired by the success-card "send another" button. |
|
||||
| `translations` | `Partial<AuthForgotPasswordStrings>` | `enGb.auth.forgotPassword` | Override any subset of the 6 copy keys. |
|
||||
|
||||
### `ForgotPasswordFormData`
|
||||
|
||||
```ts
|
||||
interface ForgotPasswordFormData {
|
||||
email: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Both render branches reuse the same `<Card>` chassis so width / spacing remain stable across the request → success transition.
|
||||
- The success card uses an `<Alert>` (non-destructive) to surface the "check your spam folder" hint rendered from the `translations.description` slot.
|
||||
- `onReset` is optional — when the host wants the success card to be a dead-end (e.g. after rate-limit lockout), simply omit the prop and the button becomes a no-op.
|
||||
- No OTel instruments are emitted from the form itself; metrics live on the Wave 1 `forgotPassword` action.
|
||||
@@ -0,0 +1,110 @@
|
||||
# LoginForm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<LoginForm>` is a pure-UI sign-in form used by the Wave 3 `<Login>` page (and any custom consumer). It owns the rendering, validation, and accessibility of the email + password + remember-me input cluster, but never imports the Wave 1 `AuthClient` or `useNavigate`. Side effects — authentication, navigation, error toasts — belong to the page-level host that supplies `onSubmit`. This separation makes the form trivial to test in isolation and reusable inside non-router contexts such as modals or embedded widgets.
|
||||
|
||||
## Features
|
||||
|
||||
- React Hook Form with the `zodResolver` adapter against an internal `loginSchema` (email required + valid, password ≥ 8 chars, optional remember-me boolean).
|
||||
- `mode: "onBlur"` validation — errors appear as the user leaves each field rather than after the first submit.
|
||||
- Password show/hide toggle with an accessible `aria-label` flipping between "Show password" and "Hide password".
|
||||
- Destructive `<Alert>` slot that renders any caller-supplied `Error`.
|
||||
- Submit button shows a `Loader2` spinner when either internal `isSubmitting` or external `isLoading` is true.
|
||||
- `translations?: Partial<AuthLoginStrings>` prop overrides the default English copy. Pass `cyGb.auth.login` or `zhCn.auth.login` from Wave 1's translation packs for full Welsh / Simplified-Chinese coverage.
|
||||
- "Forgot password?" and "Sign up" links use `<Link>` from `react-router-dom`, so the form must mount inside a `<BrowserRouter>` (or `<MemoryRouter>` in tests).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { LoginForm } from "@suaveplan/react-auth";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<LoginForm
|
||||
onSubmit={async ({ email, password }) => {
|
||||
console.log("authenticating", email, password);
|
||||
}}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire the form to the Wave 1 `useAuth().signIn` action with optimistic error
|
||||
surfacing and a translated copy pack:
|
||||
|
||||
```tsx
|
||||
import { LoginForm, useAuth, cyGb } from "@suaveplan/react-auth";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
export function CymraegLogin() {
|
||||
const { signIn } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<LoginForm
|
||||
translations={cyGb.auth.login}
|
||||
error={error}
|
||||
isLoading={loading}
|
||||
onSubmit={async ({ email, password, rememberMe }) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
await signIn({ email, password, rememberMe });
|
||||
navigate("/");
|
||||
} catch (caught) {
|
||||
setError(
|
||||
caught instanceof Error
|
||||
? caught
|
||||
: new Error(String(caught))
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `LoginForm(props: LoginFormProps): ReactElement`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `onSubmit` | `(data: LoginCredentials) => Promise<void> \| void` | — required | Invoked with the typed payload only after Zod validation passes. |
|
||||
| `error` | `Error \| null \| undefined` | `null` | When non-null, rendered inside a destructive `<Alert>`. |
|
||||
| `isLoading` | `boolean` | `false` | When `true` the submit button is disabled and spins. |
|
||||
| `translations` | `Partial<AuthLoginStrings>` | `enGb.auth.login` | Override any subset of the 8 copy keys; missing keys fall back to the default English pack. |
|
||||
|
||||
### `LoginCredentials`
|
||||
|
||||
```ts
|
||||
interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Imported from `@suaveplan/react-auth` (Wave 1 surface).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The Zod schema is defined at module scope so the resolver instance is stable across renders — RHF does not re-build the validator on every mount.
|
||||
- `useForm`'s generic is set to `z.infer<typeof loginSchema>` (the locally inferred shape) and the validated payload is `as LoginCredentials`-cast at the submit boundary. The two shapes share the same fields; the cast is documentation-grade, not a behavioural change.
|
||||
- The password toggle uses `useState` rather than RHF state because it is purely presentational.
|
||||
- The form renders inside a full-bleed `<Card>` (max-width `md`) sized to fit the Wave 3 `<Login>` page; embedded callers can wrap the form in their own container by setting `className` overrides on the parent.
|
||||
- No OTel instruments are emitted from the form — metrics live on the Wave 1 `signIn` action.
|
||||
@@ -0,0 +1,71 @@
|
||||
# `<PermissionGate>` — bitmask permission gate
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<PermissionGate>` is a declarative conditional renderer that consumes the Wave 1 `usePermission()` hook and decides whether to render its children based on the current user's bitwise permission set. It is the primary tool for hiding UI surfaces (admin links, destructive buttons, advanced settings panels) from users who lack the required permission bits.
|
||||
|
||||
The component is intentionally tiny — every interesting policy decision is delegated to `usePermission()`, which in turn reads `useAuth().user.roles` and performs the bitwise AND against the configured permission constants from `Permissions`. Two semantics are supported: any-of (the default, useful for "show this if any one of these roles applies") and all-of (`requireAll`, useful for compound predicates such as "must have both moderator and admin").
|
||||
|
||||
## Features
|
||||
|
||||
- Any-of semantics by default; all-of via `requireAll`.
|
||||
- Accepts named `Permission` keys (`"USER_READ"`, `"ADMIN_WRITE"`, ...) or raw numeric bitmasks.
|
||||
- Fallback subtree (default `null`) rendered when the check fails.
|
||||
- Unauthenticated users always fail the check — `usePermission()` returns `false` when `user === null`.
|
||||
- Zero runtime cost beyond a single context read + bitwise AND.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { PermissionGate } from "@suaveplan/react-auth";
|
||||
|
||||
export function AdminLink() {
|
||||
return (
|
||||
<PermissionGate permissions={["ADMIN_READ"]}>
|
||||
<a href="/admin">Admin Console</a>
|
||||
</PermissionGate>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
import { PermissionGate, Permissions } from "@suaveplan/react-auth";
|
||||
|
||||
export function ModeratorPanel() {
|
||||
return (
|
||||
<PermissionGate
|
||||
permissions={[
|
||||
Permissions.MODERATE_USERS,
|
||||
Permissions.MODERATE_CONTENT,
|
||||
]}
|
||||
requireAll
|
||||
fallback={<p>You do not have moderator access.</p>}
|
||||
>
|
||||
<ModeratorTools />
|
||||
</PermissionGate>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PermissionGateProps`
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `children` | `ReactNode` | — | Subtree rendered when the permission check passes. |
|
||||
| `fallback` | `ReactNode` | `null` | Subtree rendered when the permission check fails. |
|
||||
| `permissions` | `ReadonlyArray<Permission \| number>` | `[]` | Permission keys or raw numeric bitmasks. |
|
||||
| `requireAll` | `boolean` | `false` | When true, every entry must match (all-of). |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Empty `permissions` array combined with default `requireAll={false}` denies (because `[].some(...) === false`). This matches the upstream contract; pass `requireAll={true}` to allow on empty input.
|
||||
- The gate does NOT throw when no `<AuthProvider>` is mounted — `usePermission()` does, surfacing the misuse at the call site rather than silently rendering the fallback.
|
||||
- For route-level guarding, prefer `<AuthGuard>` (which redirects); `<PermissionGate>` is for in-page subtree hiding.
|
||||
@@ -0,0 +1,75 @@
|
||||
# RegisterFormFields
|
||||
|
||||
## Purpose
|
||||
|
||||
`<RegisterFormFields>` is an internal helper that owns the responsive identity-grid layout used by `<RegisterForm>`. It is extracted from the parent file so neither file exceeds the 300-line hard cap from `AGENTS.md §3`. The helper accepts every `react-hook-form` and translation prop it needs explicitly — it never touches `useForm` itself — so it remains a thin, dumb, framework-free renderer that is easy to inspect during code review.
|
||||
|
||||
## Features
|
||||
|
||||
- Renders the 10 form cells (email, username, password, confirm password, first name, surname, date of birth, sex select, gender, company) in a `grid-cols-1 sm:grid-cols-2` two-column responsive grid.
|
||||
- Encapsulates the shared `<PasswordCell>` helper for the password + confirm password input + show/hide toggle pair.
|
||||
- Accepts an explicit `t: AuthRegisterStrings` so all copy comes from the parent's merged translation table; the helper owns no defaults.
|
||||
- Accepts `setValue` for the `sex` `<Select>` because Radix's controlled API does not surface through `register`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
This helper is not exported from `@suaveplan/react-auth` — consumers should reach for `<RegisterForm>` directly. The internal call site looks like:
|
||||
|
||||
```tsx
|
||||
<RegisterFormFields
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
errors={errors}
|
||||
disabled={isSubmitting}
|
||||
showPassword={showPassword}
|
||||
showConfirmPassword={showConfirmPassword}
|
||||
onTogglePassword={() => setShowPassword((v) => !v)}
|
||||
onToggleConfirmPassword={() => setShowConfirmPassword((v) => !v)}
|
||||
t={t}
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Although the helper is internal, the same pattern can be reused inside a custom registration form — for example a tenant-specific one that adds an `inviteCode` field. Wrap the helper, append your own field cells, and feed it your own `useForm` instance:
|
||||
|
||||
```tsx
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { RegisterFormFields } from "@suaveplan/react-auth/components/register-form/register-form-fields";
|
||||
|
||||
// ...inside a custom form:
|
||||
const form = useForm<MyExtendedRegisterValues>({
|
||||
resolver: zodResolver(myExtendedSchema),
|
||||
});
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(submit)}>
|
||||
<RegisterFormFields {...buildProps(form)} t={t} />
|
||||
<Input id="inviteCode" {...form.register("inviteCode")} />
|
||||
</form>
|
||||
);
|
||||
```
|
||||
|
||||
(Note: this subpath is not part of the public API. The helper is documented here for code-reviewers; consumers should compose `<RegisterForm>` rather than the internal helper.)
|
||||
|
||||
## API Reference
|
||||
|
||||
### `RegisterFormFields(props: RegisterFormFieldsProps): ReactElement`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|---|---|---|
|
||||
| `register` | `UseFormRegister<RegisterFormData>` | Forwarded from the parent `useForm`. |
|
||||
| `setValue` | `UseFormSetValue<RegisterFormData>` | Used by the `sex` `<Select>` to write back into the form state. |
|
||||
| `errors` | `FieldErrors<RegisterFormData>` | Per-field error messages. |
|
||||
| `disabled` | `boolean` | Disables every input when `isSubmitting` is true. |
|
||||
| `showPassword` | `boolean` | Toggle state for the password field. |
|
||||
| `showConfirmPassword` | `boolean` | Toggle state for the confirm-password field. |
|
||||
| `onTogglePassword` | `() => void` | Flip the password toggle. |
|
||||
| `onToggleConfirmPassword` | `() => void` | Flip the confirm-password toggle. |
|
||||
| `t` | `AuthRegisterStrings` | Fully-merged translation table (parent owns the merge with defaults). |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The internal `<PasswordCell>` is private to this file because it carries a layout assumption (the toggle button is absolutely positioned over the input) that does not generalise.
|
||||
- The `sex` `<Select>` defaults to `"prefer-not-to-say"` and writes back through `setValue("sex", value)`. The Zod schema treats `sex` as optional so missing the field never blocks submission.
|
||||
- No OTel instruments emitted; metrics live on the Wave 1 `signUp` action.
|
||||
@@ -0,0 +1,127 @@
|
||||
# RegisterForm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<RegisterForm>` is the pure-UI surface for account creation. It renders an identity grid (email + username + password + confirm + optional first-name / surname / dob / sex / gender / company), runs the entire input set through a single `registerSchema` (Zod), and emits one validated payload from `onSubmit`. The form never imports the Wave 1 `AuthClient` or `useNavigate` — the page-level host wires `onSubmit` to `useAuth().signUp` and handles post-registration redirects. This separation keeps the form testable in isolation and reusable inside onboarding tours, admin invite flows, and tenant-specific registration pages.
|
||||
|
||||
## Features
|
||||
|
||||
- React Hook Form + `zodResolver` with email validity, username regex (`/^[a-zA-Z0-9_]+$/`, length 3–30), password ≥ 8 chars (max 100), and a cross-field `confirmPassword` refinement.
|
||||
- Default `sex` value `"prefer-not-to-say"` so the field is always submittable.
|
||||
- Two-column responsive grid (`grid-cols-1 sm:grid-cols-2`) collapsing to a single column on narrow viewports.
|
||||
- Password show/hide toggles on both password and confirm fields, each with discriminating `aria-label`s.
|
||||
- Destructive `<Alert>` for caller-supplied `Error.message`.
|
||||
- `translations?: Partial<AuthRegisterStrings>` for full Welsh / zh-CN coverage via Wave 1's translation packs.
|
||||
- Implementation split across `register-form.tsx` and `register-form-fields.tsx` to honour the 300-line cap (§3); the split is invisible to consumers — only `RegisterForm` is exported.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { RegisterForm } from "@suaveplan/react-auth";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
export function RegisterPage() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<RegisterForm
|
||||
onSubmit={async (data) => {
|
||||
console.log("creating account", data);
|
||||
}}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire to `useAuth().signUp` with error capture, navigation on success, and
|
||||
Welsh copy:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
RegisterForm,
|
||||
cyGb,
|
||||
useAuth,
|
||||
} from "@suaveplan/react-auth";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
export function CymraegRegister() {
|
||||
const { signUp } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
return (
|
||||
<RegisterForm
|
||||
translations={cyGb.auth.register}
|
||||
error={error}
|
||||
isLoading={loading}
|
||||
onSubmit={async (data) => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
await signUp({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
username: data.username,
|
||||
firstName: data.firstName,
|
||||
surname: data.surname,
|
||||
});
|
||||
navigate("/auth/verify-email");
|
||||
} catch (caught) {
|
||||
setError(
|
||||
caught instanceof Error
|
||||
? caught
|
||||
: new Error(String(caught))
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `RegisterForm(props: RegisterFormProps): ReactElement`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `onSubmit` | `(data: RegisterFormData) => Promise<void> \| void` | — required | Invoked with the typed payload once Zod accepts. |
|
||||
| `error` | `Error \| null \| undefined` | `null` | Rendered in a destructive `<Alert>`. |
|
||||
| `isLoading` | `boolean` | `false` | Disables the submit button. |
|
||||
| `translations` | `Partial<AuthRegisterStrings>` | `enGb.auth.register` | Override any subset of the 16 copy keys. |
|
||||
|
||||
### `RegisterFormData`
|
||||
|
||||
```ts
|
||||
interface RegisterFormData {
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
username: string;
|
||||
firstName?: string;
|
||||
surname?: string;
|
||||
dob?: string;
|
||||
sex?: "prefer-not-to-say"
|
||||
| "traditional-male"
|
||||
| "traditional-female"
|
||||
| "other";
|
||||
gender?: string;
|
||||
company?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Field grid lives in `register-form-fields.tsx` and accepts an explicit `t: AuthRegisterStrings` so the child file owns no translation defaults of its own.
|
||||
- The `sex` select uses Wave 1's `@suaveplan/react-ui` `<Select>` (a Radix portal). In headless test environments the dropdown is not exercised — the default value flows through the Zod schema and the submit payload regardless.
|
||||
- The cross-field `password === confirmPassword` check uses Zod's `.refine` with `path: ["confirmPassword"]` so the error attaches under the right field.
|
||||
- No OTel instruments emitted from the form; metrics live on the Wave 1 `signUp` action.
|
||||
@@ -0,0 +1,118 @@
|
||||
# ResetPasswordForm
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<ResetPasswordForm>` is the pure-UI surface for setting a new password after the user clicks a reset link. It owns three render modes — the form itself, a success card, and an invalid-token card — but never imports the Wave 1 `AuthClient` or `useNavigate`. The page-level host extracts the token from the URL, flips `hasToken` accordingly, calls `useAuth().resetPassword` from `onSubmit`, and flips `isSuccess` once the call resolves. The form is therefore trivially testable in isolation and reusable inside embedded reset flows where routing is owned by the host.
|
||||
|
||||
## Features
|
||||
|
||||
- React Hook Form + `zodResolver` against `resetPasswordSchema` (both fields ≥ 8 chars + cross-field match refinement).
|
||||
- `mode: "onBlur"` validation; password mismatch surfaces under the confirm field via Zod `path: ["confirmPassword"]`.
|
||||
- Independent password show/hide toggle per field with discriminating `aria-label`s ("Show new password" vs. "Show confirm new password").
|
||||
- Three render branches: form (default), success (`isSuccess`), invalid-token (`hasToken: false`). Each renders its own CTA wired to a host-supplied navigation callback.
|
||||
- Destructive `<Alert>` slot rendering caller-supplied `Error.message`.
|
||||
- `translations?: Partial<AuthResetPasswordStrings>` for Welsh / zh-CN coverage via Wave 1's translation packs.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { ResetPasswordForm } from "@suaveplan/react-auth";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
export function ResetPasswordPage() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ResetPasswordForm
|
||||
onSubmit={async ({ password }) => {
|
||||
console.log("resetting", password);
|
||||
}}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire URL-token extraction to `hasToken`, call `useAuth().resetPassword`,
|
||||
and route on success:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
ResetPasswordForm,
|
||||
useAuth,
|
||||
} from "@suaveplan/react-auth";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ResetPassword() {
|
||||
const { resetPassword } = useAuth();
|
||||
const [params] = useSearchParams();
|
||||
const token = params.get("token");
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
return (
|
||||
<ResetPasswordForm
|
||||
hasToken={token !== null}
|
||||
error={error}
|
||||
isSuccess={success}
|
||||
onNavigateToLogin={() => {
|
||||
navigate("/auth/login");
|
||||
}}
|
||||
onNavigateToForgotPassword={() => {
|
||||
navigate("/auth/forgot-password");
|
||||
}}
|
||||
onSubmit={async ({ password }) => {
|
||||
setError(null);
|
||||
try {
|
||||
await resetPassword({ password }, token ?? "");
|
||||
setSuccess(true);
|
||||
} catch (caught) {
|
||||
setError(
|
||||
caught instanceof Error
|
||||
? caught
|
||||
: new Error(String(caught))
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ResetPasswordForm(props: ResetPasswordFormProps): ReactElement`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `onSubmit` | `(data: ResetPasswordFormData) => Promise<void> \| void` | — required | Invoked once Zod accepts the payload. |
|
||||
| `onNavigateToLogin` | `() => void` | — | Fired by the success-view CTA. |
|
||||
| `onNavigateToForgotPassword` | `() => void` | — | Fired by the invalid-token-view CTA. |
|
||||
| `error` | `Error \| null \| undefined` | `null` | Rendered in a destructive `<Alert>`. |
|
||||
| `isLoading` | `boolean` | `false` | Disables the submit button. |
|
||||
| `isSuccess` | `boolean` | `false` | Switches to the success card. |
|
||||
| `hasToken` | `boolean` | `true` | When `false`, switches to the invalid-token card. |
|
||||
| `translations` | `Partial<AuthResetPasswordStrings>` | `enGb.auth.resetPassword` | Override any subset of the 7 copy keys. |
|
||||
|
||||
### `ResetPasswordFormData`
|
||||
|
||||
```ts
|
||||
interface ResetPasswordFormData {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The shared internal `<PasswordField>` helper encapsulates the input + toggle button pair. This keeps the public file under the 300-line cap (§3) and ensures both fields render identically.
|
||||
- Render-branch order is intentional: `isSuccess` short-circuits before `hasToken` so a successful reset on a now-expired-but-already-used token still shows the success card.
|
||||
- Toggle `aria-label` strings derive from the (translated) field label so screen-reader output is localised end-to-end.
|
||||
- No OTel instruments emitted from the form; metrics live on the Wave 1 `resetPassword` action.
|
||||
@@ -0,0 +1,58 @@
|
||||
# test-wrap
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Internal test helper consumed only by the Wave 2 form `.test.tsx` files in `src/components/<form>/`. The helper wraps a node in every provider the forms need to mount inside a happy-dom environment: `<MemoryRouter>`, `<TranslationProvider>`, and `<ThemeProvider>`. Without these providers, `@suaveplan/react-ui` primitives (which the forms compose) throw `react-ui.use_themed_variants.no_provider` and `react-i18n` `useTranslation()` panics on first render. The helper centralises the wrap so every form test mounts under the same conditions.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `wrapForm(node)` function returning a fully-wrapped `ReactElement`.
|
||||
- A module-scope `createI18n` call that yields an empty-message i18n instance — the tests assert against the form's own translation defaults rather than catalog lookups, so an empty catalog is correct.
|
||||
- Uses Wave 1's `@suaveplan/react-ui`'s `lightTheme` via the dedicated subpath export so the theme bundle is the same one production callers ship.
|
||||
- Renders inside a `<MemoryRouter>` because every form ships at least one `<Link>` to `/auth/login` / `/auth/register`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { render } from "@testing-library/react";
|
||||
import { wrapForm } from "../test-wrap.js";
|
||||
import { LoginForm } from "./login-form.js";
|
||||
|
||||
const { getByLabelText } = render(
|
||||
wrapForm(<LoginForm onSubmit={async () => {}} />)
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Pass any node — including nested fragments and arrays — and the wrap is the same:
|
||||
|
||||
```tsx
|
||||
import { wrapForm } from "../test-wrap.js";
|
||||
import { LoginForm, RegisterForm } from "@suaveplan/react-auth";
|
||||
|
||||
render(
|
||||
wrapForm(
|
||||
<>
|
||||
<LoginForm onSubmit={async () => {}} />
|
||||
<RegisterForm onSubmit={async () => {}} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `wrapForm(node: ReactNode): ReactElement`
|
||||
|
||||
Returns the input node wrapped in `<MemoryRouter><TranslationProvider><ThemeProvider>{node}</ThemeProvider></TranslationProvider></MemoryRouter>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Not exported from the package root barrel — `wrapForm` is internal test plumbing only.
|
||||
- Excluded from coverage via `vitest.config.ts` (`exclude: ["src/components/test-wrap.tsx"]`) so it does not skew the 100/100/100/100 threshold.
|
||||
- The empty `messages` map on the i18n instance keeps tests deterministic — every translation lookup returns its key, and the forms supply their own English defaults via the `translations` prop.
|
||||
@@ -0,0 +1,74 @@
|
||||
# `auth-context`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`auth-context.tsx` exposes the React Context surface for the auth flow. It defines `AuthContext`, the `AuthProvider` component, and the `useAuth()` hook. The provider owns an `AuthClient` instance (either injected or constructed from `config`) and a state block of `{ user, session, isLoading, isAuthenticated, error }`. On mount (unless `skipInitialCheck` is set) it fetches the current session, normalises it through `transformSessionResponse`, and settles the state. It exposes eleven action callbacks (`signIn`, `signUp`, `signOut`, `updateProfile`, `changePassword`, `forgotPassword`, `resetPassword`, `verifyEmail`, `resendVerificationEmail`, `deleteAccount`, `refreshSession`) — every action wraps `client` calls with loading/error state mutations, re-fetches the session where appropriate, records the corresponding OTel counter, and re-throws on failure so callers can `catch` for form-level UI handling. `useAuth()` outside an `AuthProvider` throws `ReactAuthContextMissingError` so the failure is immediate and typed rather than a downstream `undefined` access.
|
||||
|
||||
## Features
|
||||
|
||||
- `AuthContext` + `AuthProvider` + `useAuth()` triple.
|
||||
- 11 typed action callbacks; each settles `isLoading` and surfaces `error` deterministically.
|
||||
- `skipInitialCheck` flag short-circuits the mount-time session fetch (test surface).
|
||||
- `onAuthStateChange` callback fires on every state transition (useful for analytics).
|
||||
- Re-throws action errors so form-level UI can render messages; never silently swallows.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { AuthProvider, useAuth } from "@suaveplan/react-auth";
|
||||
|
||||
function Inside() {
|
||||
const { user, signOut } = useAuth();
|
||||
return user ? <button onClick={() => signOut()}>Sign out</button> : null;
|
||||
}
|
||||
|
||||
<AuthProvider config={{ baseURL: "https://auth.example.com" }}>
|
||||
<Inside />
|
||||
</AuthProvider>;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a pre-built client and observe state changes:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AuthProvider,
|
||||
createAuthClient,
|
||||
type AuthState,
|
||||
} from "@suaveplan/react-auth";
|
||||
|
||||
const client = createAuthClient({
|
||||
baseURL: "https://auth.example.com",
|
||||
siteDomain: "tenant-a",
|
||||
});
|
||||
|
||||
function onAuthStateChange(state: AuthState) {
|
||||
analytics.track("auth_state", { authenticated: state.isAuthenticated });
|
||||
}
|
||||
|
||||
<AuthProvider
|
||||
config={{ baseURL: "https://auth.example.com" }}
|
||||
client={client}
|
||||
onAuthStateChange={onAuthStateChange}
|
||||
>
|
||||
{/* ... */}
|
||||
</AuthProvider>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AuthState` — `{ user: User | null; session: Session | null; isLoading: boolean; isAuthenticated: boolean; error: Error | null }`.
|
||||
- `interface AuthContextValue extends AuthState` — adds `client: AuthClient` plus the 11 action callbacks.
|
||||
- `interface AuthProviderProps` — `{ children: ReactNode; config: AuthClientConfig; client?: AuthClient; onAuthStateChange?: (state: AuthState) => void; skipInitialCheck?: boolean }`.
|
||||
- `const AuthContext: React.Context<AuthContextValue | undefined>` — context handle.
|
||||
- `function AuthProvider(props: AuthProviderProps): ReactNode` — wraps children with the context.
|
||||
- `function useAuth(): AuthContextValue` — reads the context; throws `ReactAuthContextMissingError` when outside an `AuthProvider`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
State is consolidated in a single `useState` block so transitions are atomic — callers never see a half-updated value. Every action wraps its critical section in `try`/`catch`/`finally`-style logic but with explicit `setState` calls rather than a `finally` clause, because the success and failure branches set different sets of fields. Caught values pass through `toError(value)` which wraps non-`Error` throwables so the `error` field is always a real `Error` (this avoids violating biome's `useThrowOnlyError` rule when a downstream surface re-throws from the context). Session fetches go through `timedSessionFetch` which records the wall-clock duration into the `react_auth.session_fetch.duration_ms` histogram with a `success`/`failure` label. The `mounted` ref guards the initial-load effect from setting state after unmount; React 19's `StrictMode` double-mount runs the cleanup before the second mount, so the first effect's `mounted = false` is honoured.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes.ts` defines the stable error-code registry for `@suaveplan/react-auth` plus the ten typed `ReactAuthError` subclasses that wrap each code. Every code is registered through `@suaveplan/error`'s `defineCodes("REACT_AUTH", [...])`, which produces a frozen `{ KEY: "REACT_AUTH.KEY" }` map and forms the basis for programmatic error handling, OTel attribute tagging, and i18n key resolution. Each subclass extends `ReactAuthError` (which extends `SuaveplanError<ReactAuthErrorCode>`), carries its own constructor with structured `context` fields when appropriate, and is thrown from at least one production path. Consumers can `catch` either `ReactAuthError` for the family or a specific subclass for narrow handling.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `REACT_AUTH_CODES` map with 10 stable codes.
|
||||
- Typed `ReactAuthError` base + 10 typed subclasses.
|
||||
- Structured `cause` chain support via `SuaveplanError`.
|
||||
- Optional `context` payload propagation (e.g. `permission` on `ReactAuthPermissionDeniedError`).
|
||||
- `ReactAuthErrorCode` union type for switch-narrowed handling.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ReactAuthError, ReactAuthConfigInvalidError } from "@suaveplan/react-auth";
|
||||
|
||||
try {
|
||||
createAuthClient({ baseURL: "not-a-url" });
|
||||
} catch (err) {
|
||||
if (err instanceof ReactAuthConfigInvalidError) {
|
||||
console.error("config invalid", err.code, err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Switch on `error.code` for routing across the family:
|
||||
|
||||
```ts
|
||||
import {
|
||||
REACT_AUTH_CODES,
|
||||
type ReactAuthErrorCode,
|
||||
ReactAuthError,
|
||||
} from "@suaveplan/react-auth";
|
||||
|
||||
function describe(err: unknown): string {
|
||||
if (!(err instanceof ReactAuthError)) return "unknown";
|
||||
const code: ReactAuthErrorCode = err.code;
|
||||
switch (code) {
|
||||
case REACT_AUTH_CODES.CONFIG_INVALID:
|
||||
return "configuration failure";
|
||||
case REACT_AUTH_CODES.CONTEXT_MISSING:
|
||||
return "provider not mounted";
|
||||
case REACT_AUTH_CODES.GUARD_PROPS_INVALID:
|
||||
case REACT_AUTH_CODES.GUARD_OPTIONS_INVALID:
|
||||
return "guard misuse";
|
||||
default:
|
||||
return err.code;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const REACT_AUTH_CODES` — frozen map of 10 codes (`CONFIG_INVALID`, `CONTEXT_MISSING`, `GUARD_PROPS_INVALID`, `GUARD_OPTIONS_INVALID`, `CLIENT_NOT_INITIALISED`, `BETTER_AUTH_RESPONSE_INVALID`, `SIGN_IN_FAILED`, `SIGN_UP_FAILED`, `SESSION_FETCH_FAILED`, `PERMISSION_DENIED`).
|
||||
- `type ReactAuthErrorCode` — union of every code value.
|
||||
- `class ReactAuthError extends SuaveplanError<ReactAuthErrorCode>` — base.
|
||||
- `class ReactAuthConfigInvalidError` — invalid `AuthClientConfig`.
|
||||
- `class ReactAuthContextMissingError` — `useAuth` outside `<AuthProvider>`.
|
||||
- `class ReactAuthGuardPropsInvalidError` — invalid `<AuthGuard>` props.
|
||||
- `class ReactAuthGuardOptionsInvalidError` — invalid `useAuthGuard` options.
|
||||
- `class ReactAuthClientNotInitialisedError` — client used before construction completes.
|
||||
- `class ReactAuthBetterAuthResponseInvalidError` — payload outside the union shape.
|
||||
- `class ReactAuthSignInFailedError` — wrapper for sign-in failure (carries `cause`).
|
||||
- `class ReactAuthSignUpFailedError` — wrapper for sign-up failure (carries `cause`).
|
||||
- `class ReactAuthSessionFetchFailedError` — wrapper for session-fetch failure.
|
||||
- `class ReactAuthPermissionDeniedError` — permission check denied (carries `permission` context).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every subclass takes an options object with `message` and optional `cause`/`context` fields. `cause` is normalised to `Error | undefined` (non-`Error` throwables are dropped so the cause chain never carries opaque values). The `ReactAuthPermissionDeniedError` constructor builds its message from the provided permission name when present, falling back to a generic string. The `context` object is propagated through `SuaveplanError` and surfaces both in the wire-format payload and on the OTel span event so error attribution flows through to dashboards.
|
||||
@@ -0,0 +1,56 @@
|
||||
# `use-auth-guard`
|
||||
|
||||
## Purpose
|
||||
|
||||
`useAuthGuard(options)` is the imperative counterpart to the declarative `<AuthGuard>` component. It reads the current auth state from the surrounding `AuthProvider`, watches it via `useEffect`, and calls `react-router-dom`'s `navigate()` to redirect when any of the configured predicates fails. It also returns a `{ isLoading, isAuthenticated, user, canAccess }` snapshot so callers can render different content inline without waiting for the redirect. Configuration is Zod-validated; invalid options throw `ReactAuthGuardOptionsInvalidError`. The hook is intended for whole-page guards (where the side-effect redirect is the desired UX) — use `<AuthGuard>` when you want to compose a fallback element into a sub-tree instead.
|
||||
|
||||
## Features
|
||||
|
||||
- Four predicate paths: `requireAuth`, `requireGuest`, `requireVerified`, `allowedRoles`.
|
||||
- Predicate-specific redirect targets: `/auth/login`, `/`, `/auth/verify-email`, `/unauthorized`.
|
||||
- `onUnauthorized` callback fires before auth / role redirects (audit hook).
|
||||
- Zod-validated options; invalid input throws `ReactAuthGuardOptionsInvalidError`.
|
||||
- Returns `canAccess` for inline checks while the redirect is in flight.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { useAuthGuard } from "@suaveplan/react-auth";
|
||||
|
||||
export function ProtectedPage() {
|
||||
useAuthGuard({ requireAuth: true });
|
||||
return <DashboardContent />;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combine multiple predicates with an audit callback:
|
||||
|
||||
```tsx
|
||||
import { useAuthGuard } from "@suaveplan/react-auth";
|
||||
|
||||
export function AdminConsole() {
|
||||
const { canAccess, isLoading } = useAuthGuard({
|
||||
requireAuth: true,
|
||||
requireVerified: true,
|
||||
allowedRoles: ["admin", "super-admin"],
|
||||
redirectTo: "/auth/login",
|
||||
onUnauthorized: () => audit.log("admin_console_denied"),
|
||||
});
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
if (!canAccess) return null;
|
||||
return <AdminPanel />;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface UseAuthGuardOptions` — `{ redirectTo?: string; requireAuth?: boolean; requireGuest?: boolean; requireVerified?: boolean; allowedRoles?: readonly string[]; onUnauthorized?: () => void }`.
|
||||
- `interface UseAuthGuardResult` — `{ isLoading: boolean; isAuthenticated: boolean; user: User | null; canAccess: boolean }`.
|
||||
- `function useAuthGuard(options?: UseAuthGuardOptions): UseAuthGuardResult`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The effect short-circuits when `isLoading` is true so transient mount states do not trigger spurious redirects. Each predicate is evaluated in priority order: `requireAuth` → `requireGuest` → `requireVerified` → `allowedRoles`. The `canAccess` boolean is computed off the latest render's state, not from the effect, so consumers reading it in the same render see the same logical state the effect will act on. The `allowedRoles` shape is `readonly string[]` to discourage callers from mutating the array between renders. Zod validation runs on every render but is cheap; option misuse fails immediately rather than later when an undefined property is read.
|
||||
@@ -0,0 +1,62 @@
|
||||
# `use-permission`
|
||||
|
||||
## Purpose
|
||||
|
||||
`usePermission()` exposes a bitwise permission-check API over the current `user.roles` bitmask. The package ships a closed registry of 16 named permission flags (`Permissions` enum) covering user / post / admin / system / moderation / settings axes. The hook accepts either a `Permission` string key (for ergonomics in JSX) or a raw numeric bit (for composition), tests `(user.roles & bit) === bit` for each query, and returns helpers for the three common patterns: single flag, any-of, all-of. With no authenticated user every predicate returns `false`, mirroring the strictest safe default. `getPermissionNames()` projects the bitmask onto the human-readable flag names so consumers can render checklists without re-implementing the mapping.
|
||||
|
||||
## Features
|
||||
|
||||
- 16-flag closed `Permissions` enum keyed by name with bit values.
|
||||
- Accepts string keys or numeric bits at every call site.
|
||||
- `hasPermission`, `hasAnyPermission`, `hasAllPermissions`, `getPermissionNames`, `userRoles` fields.
|
||||
- Unauthenticated users get safe-default `false` / `[]` returns.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { usePermission } from "@suaveplan/react-auth";
|
||||
|
||||
function DeleteButton() {
|
||||
const { hasPermission } = usePermission();
|
||||
if (!hasPermission("POST_DELETE")) return null;
|
||||
return <button type="button">Delete</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose checks against numeric bits and named flags together; render the user's effective permission list:
|
||||
|
||||
```tsx
|
||||
import { Permissions, usePermission } from "@suaveplan/react-auth";
|
||||
|
||||
function AuditBadge() {
|
||||
const { hasAllPermissions, getPermissionNames } = usePermission();
|
||||
const moderator = hasAllPermissions([
|
||||
Permissions.MODERATE_USERS,
|
||||
Permissions.MODERATE_CONTENT,
|
||||
1 << 6, // ADMIN_READ — same bit, asserted by number
|
||||
]);
|
||||
return (
|
||||
<details>
|
||||
<summary>{moderator ? "Moderator" : "Member"}</summary>
|
||||
<ul>
|
||||
{getPermissionNames().map((name) => (
|
||||
<li key={name}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const Permissions` — closed map of `{ NAME: bit }` for 16 flags.
|
||||
- `type Permission = keyof typeof Permissions`.
|
||||
- `interface UsePermissionResult` — `{ hasPermission(p): boolean; hasAnyPermission(ps): boolean; hasAllPermissions(ps): boolean; getPermissionNames(): string[]; userRoles: number }`.
|
||||
- `function usePermission(): UsePermissionResult`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `Permissions` map is `as const` so each key narrows to its literal numeric bit. The hook reads `user` from `useAuth()`, which means it shares the same context-rebind semantics as the rest of the auth surface — a `signIn` that changes `user.roles` propagates to every consumer on the next render without manual re-subscription. `getPermissionNames` iterates `Object.entries(Permissions)` and is intentionally O(n) in flag count (16) — short and constant. `userRoles` is exposed as a fallback `0` when unauthenticated, so callers performing custom bit checks do not need a separate `user === null` guard.
|
||||
@@ -0,0 +1,60 @@
|
||||
# `better-auth-adapter`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`better-auth-adapter.ts` provides the response-narrowing bridge between `better-auth/client` (which returns a `BetterAuthError | BetterAuthData<T>` union with native `Date` timestamps) and the package's internal `User` / `Session` / `AuthResponse` shapes (which carry ISO-string timestamps for serialisation safety). The module exposes two structural type guards (`isBetterAuthError`, `hasBetterAuthData`) that narrow `unknown` to the appropriate branch without `any`, plus four `transform*` functions that map a successful payload onto an internal shape and surface the error branch's message when the call failed. No `any` types appear anywhere — every guard accepts `unknown` and narrows through explicit field checks.
|
||||
|
||||
## Features
|
||||
|
||||
- Two type guards: `isBetterAuthError(unknown)`, `hasBetterAuthData<T>(unknown)`.
|
||||
- Four transforms: `transformSessionResponse`, `transformAuthResponse`, `transformGenericResponse`, `transformUpdateProfileResponse`.
|
||||
- ISO-string normalisation for every `Date` field on the way out.
|
||||
- Robust to `null` / `undefined` / malformed payloads — returns a default with a `message` string when the shape doesn't match.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { transformSessionResponse } from "@suaveplan/react-auth";
|
||||
|
||||
const raw = await client.getSession();
|
||||
const { user, session, error } = transformSessionResponse(raw);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Chain the guards directly when you need to act differently on each branch:
|
||||
|
||||
```ts
|
||||
import {
|
||||
hasBetterAuthData,
|
||||
isBetterAuthError,
|
||||
transformAuthResponse,
|
||||
} from "@suaveplan/react-auth";
|
||||
|
||||
const raw = await client.signIn({ email, password });
|
||||
if (isBetterAuthError(raw)) {
|
||||
logger.warn({ code: raw.error, message: raw.message }, "sign-in rejected");
|
||||
return;
|
||||
}
|
||||
if (hasBetterAuthData<{ user: unknown }>(raw)) {
|
||||
const out = transformAuthResponse(raw);
|
||||
if (out.user) await persistSession(out.user);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function isBetterAuthError(response: unknown): response is { error: string; message: string }`
|
||||
- `function hasBetterAuthData<T = unknown>(response: unknown): response is { data: T }`
|
||||
- `function transformSessionResponse(response: unknown): { session: Session | null; user: User | null; error?: string }`
|
||||
- `function transformAuthResponse(response: unknown): AuthResponse`
|
||||
- `function transformGenericResponse(response: unknown): AuthResponse`
|
||||
- `function transformUpdateProfileResponse(response: unknown): AuthResponse`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The two top-level guards use `typeof value === "object" && value !== null` plus structural field checks. They do not use `instanceof` because the underlying client may return plain objects parsed from JSON. The user-shape guard checks `id`, `email`, `emailVerified`, `name`, `createdAt`, and `updatedAt` are present and of the expected primitive class (`Date | string`); the session-shape guard checks `id`, `userId`, and `expiresAt`. When the payload includes a `Date` instance for any timestamp, the transform calls `.toISOString()`; when it's already a string, it is passed through. The `transformUser` helper hard-codes `roles: 0` and `status: 0` because those fields belong to the internal `User` type but are not modelled by better-auth's session payload — call sites that need richer role data must read them from the `Session` row directly (Wave 2 wires this through the form components).
|
||||
@@ -0,0 +1,68 @@
|
||||
# `react-auth-metrics`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`react-auth-metrics.ts` builds the OTel instrument set for `@suaveplan/react-auth`. Instruments are constructed lazily through `metrics.getMeter("suaveplan.react-auth")` so consumers without a registered `MeterProvider` pay zero overhead — the OTel API ships a no-op meter by default. The module exposes five counters (`react_auth.sign_in.total`, `react_auth.sign_up.total`, `react_auth.sign_out.total`, `react_auth.guard.allow.total`, `react_auth.guard.deny.total`) and one histogram (`react_auth.session_fetch.duration_ms`), plus helper functions that record the corresponding event with a typed attribute payload. The helpers also push every event into an in-memory `state` ring so spec-conformance tests can assert exact attribute payloads without standing up a full SDK pipeline. `resetReactAuthMetricsState()` clears the ring between tests.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy OTel instrument construction; no-op until a `MeterProvider` is registered.
|
||||
- 5 counters + 1 histogram covering the wave-1 production paths.
|
||||
- Typed `outcome`/`predicate` attributes per record helper.
|
||||
- In-memory `state` ring (`getReactAuthMetricsState`) for assertion-friendly tests.
|
||||
- `resetReactAuthMetricsState()` test-only helper.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { recordSignIn } from "@suaveplan/react-auth";
|
||||
|
||||
await client.signIn(credentials);
|
||||
recordSignIn("success");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire a real `MeterProvider` (e.g. for shipping metrics to Prometheus) and assert via the in-memory ring in tests:
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import {
|
||||
getReactAuthMetricsState,
|
||||
recordSessionFetchDuration,
|
||||
resetReactAuthMetricsState,
|
||||
} from "@suaveplan/react-auth";
|
||||
|
||||
const provider = new MeterProvider();
|
||||
metrics.setGlobalMeterProvider(provider);
|
||||
|
||||
resetReactAuthMetricsState();
|
||||
recordSessionFetchDuration(42, "success");
|
||||
const state = getReactAuthMetricsState();
|
||||
expect(state.sessionFetchDurationMs).toEqual([
|
||||
{ attributes: { outcome: "success" }, value: 42 },
|
||||
]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface ReactAuthMetricSample` — `{ attributes: Record<string, string>; value?: number }`.
|
||||
- `interface ReactAuthMetricsState` — `{ signInTotal; signUpTotal; signOutTotal; guardAllowTotal; guardDenyTotal; sessionFetchDurationMs }` arrays.
|
||||
- `function getReactAuthMetrics()` — returns the cached instrument bundle.
|
||||
- `function recordSignIn(outcome: "success" | "failure"): void`.
|
||||
- `function recordSignUp(outcome: "success" | "failure"): void`.
|
||||
- `function recordSignOut(outcome: "success" | "failure"): void`.
|
||||
- `function recordGuardAllow(predicate: string): void`.
|
||||
- `function recordGuardDeny(predicate: string): void`.
|
||||
- `function recordSessionFetchDuration(durationMs: number, outcome: "success" | "failure"): void`.
|
||||
- `function getReactAuthMetricsState(): ReactAuthMetricsState`.
|
||||
- `function resetReactAuthMetricsState(): void`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The OTel API exposes a global no-op `MeterProvider` until the application opts in by calling `setGlobalMeterProvider(...)`; this means the helpers are safe to call from production paths without any registration ceremony. The in-memory state ring is unbounded by design — it is intended only for tests and the package's own conformance checks. A long-running process would never call `getReactAuthMetricsState()` directly, so leak risk is bounded by test scope. The cached instrument bundle is module-level (`let cached: CachedInstruments | undefined`) so the meter is resolved at most once per process; the lazy build means importing the module is free.
|
||||
@@ -0,0 +1,65 @@
|
||||
# `<EmailVerificationPage>` — routing-aware verification page
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<EmailVerificationPage>` is the routing-aware adapter that wraps the Wave 2 `<EmailVerificationView>` for the `?token=…`-bearing email-verification link sent by the server. It extracts the token from the URL search params, calls `useAuth().verifyEmail(token)` exactly once on mount, and renders one of four states via the underlying view component: `verifying` (default while the call is in flight), `success` (after a resolved call), `expired` (after a rejection whose message contains "expired" or "invalid"), or `error` (everything else).
|
||||
|
||||
Use this as the `Route element` for `/auth/verify-email`. The page never re-implements the four state cards — those live in the Wave 2 view.
|
||||
|
||||
## Features
|
||||
|
||||
- `useAuthGuard({ requireGuest: true })` so authenticated visitors are redirected away.
|
||||
- Single in-flight `verifyEmail(token)` call on mount; React's `useEffect` dep array is `[token, verifyEmail]` so re-renders don't fire spurious calls.
|
||||
- Error classifier: rejections whose `message` contains `expired` or `invalid` (case-insensitive) map to the `expired` state; all other failures map to `error`.
|
||||
- Three navigation callbacks wired into the view: `onContinueToLogin` → `/auth/login`, `onRegisterAgain` → `/auth/register`, `onTryAgain` reloads the page.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { EmailVerificationPage } from "@suaveplan/react-auth";
|
||||
|
||||
export function AuthRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/auth/verify-email"
|
||||
element={<EmailVerificationPage />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
When sending the verification email, embed the token in the URL:
|
||||
|
||||
```ts
|
||||
const link = new URL("/auth/verify-email", site.origin);
|
||||
link.searchParams.set("token", verificationToken);
|
||||
await mailer.send({ to: user.email, subject, body: link.toString() });
|
||||
```
|
||||
|
||||
If you want a different post-verification destination, wrap the page in a custom layout that hooks into `useAuth()` to redirect on `user.emailVerified === true`.
|
||||
|
||||
## API Reference
|
||||
|
||||
`<EmailVerificationPage>` accepts no props.
|
||||
|
||||
### `EmailVerificationPageState`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `status` | `EmailVerificationStatus` | One of `"verifying" \| "success" \| "error" \| "expired"`. |
|
||||
| `message` | `string` | Human-readable message displayed by the view. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The reload-style `onTryAgain` handler intentionally short-circuits on SSR (`typeof window === "undefined"`). The page is browser-only in practice, but the guard keeps the code defensive.
|
||||
- The `verifyEmail` mount-time call is fired even if the token is malformed; the server's response carries the failure mode (404, expired, malformed) which the classifier maps to `expired` / `error`.
|
||||
- The page intentionally does NOT renavigate on success — the user clicks the visible CTA to continue to login, mirroring the upstream UX where some users may want to copy state from the verification screen first.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `<ForgotPasswordPage>` — routing-aware reset-link page
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<ForgotPasswordPage>` is the routing-aware adapter that wraps the Wave 2 `<ForgotPasswordForm>` with the navigation and auth-context wiring needed for a "forgot your password?" route. It drives the form's `isSuccess` / `emailSent` props from local state on a successful `useAuth().forgotPassword(email)` call, and clears that state when the form's `onReset` callback fires.
|
||||
|
||||
Use this as the `Route element` for `/auth/forgot-password`. The page never navigates on success — it transitions the same form node to its built-in success view, mirroring the upstream UX.
|
||||
|
||||
## Features
|
||||
|
||||
- Calls `useAuth().forgotPassword(email)` on submit.
|
||||
- Tracks local `isSuccess` / `emailSent` state so the form renders the success card after a resolved call.
|
||||
- `onReset` clears local state and returns the form to its entry view, so the user can send another link if needed.
|
||||
- `forgotPassword` rejections are swallowed; errors surface via `useAuth().error`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { ForgotPasswordPage } from "@suaveplan/react-auth";
|
||||
|
||||
export function AuthRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/auth/forgot-password"
|
||||
element={<ForgotPasswordPage />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
To override the success-view copy or wire analytics into the submission, mount `<ForgotPasswordForm>` directly with custom handlers — for example to capture an analytics event:
|
||||
|
||||
```tsx
|
||||
import { useAuth, ForgotPasswordForm } from "@suaveplan/react-auth";
|
||||
|
||||
function CustomForgotPassword() {
|
||||
const { forgotPassword, error } = useAuth();
|
||||
const [success, setSuccess] = useState(false);
|
||||
return (
|
||||
<ForgotPasswordForm
|
||||
onSubmit={async ({ email }) => {
|
||||
track("password.forgot.submit", { email });
|
||||
await forgotPassword(email);
|
||||
setSuccess(true);
|
||||
}}
|
||||
isSuccess={success}
|
||||
emailSent="(redacted)"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
`<ForgotPasswordPage>` accepts no props.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The page does NOT guard against authenticated visitors — a logged-in user reaching the forgot-password route is a legitimate flow when they want to rotate their password from a known-good session.
|
||||
- The `emailSent` state is the literal value the user typed, not a redacted echo — the form decides how to render it.
|
||||
@@ -0,0 +1,67 @@
|
||||
# `<LoginPage>` — routing-aware login page
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<LoginPage>` is the routing-aware adapter that wraps the Wave 2 `<LoginForm>` with the navigation and auth-context wiring needed to drop into a `react-router-dom` v7 application. It applies a guest-only auth guard, calls `useAuth().signIn` on submit, redirects to `?returnTo` (or `/dashboard` as fallback) on success, and surfaces errors through the form's destructive alert via the `useAuth().error` field.
|
||||
|
||||
Use this component for the sign-in route of a shipping application. For a customisable lower-level UI consider mounting `<LoginForm>` directly and wiring submission and navigation manually.
|
||||
|
||||
## Features
|
||||
|
||||
- `useAuthGuard({ requireGuest: true })` — already-authenticated visitors are redirected to `/`.
|
||||
- `?returnTo` query param honoured for post-login navigation; defaults to `/dashboard`.
|
||||
- `signIn` rejections are swallowed so the page never throws into a router error boundary.
|
||||
- Reads `useAuth().isLoading` to disable the submit button during the in-flight `signIn`.
|
||||
- Reads `useAuth().error` to render the destructive alert in the form.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { AuthProvider, LoginPage } from "@suaveplan/react-auth";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<AuthProvider config={{ baseURL: "https://auth.example.com" }}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/auth/login" element={<LoginPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
When a protected route renders `<AuthGuard requireAuth fallback={<Navigate to={`/auth/login?returnTo=${path}`} />}>`, the user returns to the original path after sign-in:
|
||||
|
||||
```tsx
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
function Protected({ children }) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const loc = useLocation();
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to={`/auth/login?returnTo=${loc.pathname}`} replace />;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
```
|
||||
|
||||
The `LoginPage` reads the `returnTo` search param via `useSearchParams` and routes the user back after a successful sign-in.
|
||||
|
||||
## API Reference
|
||||
|
||||
`<LoginPage>` accepts no props. It reads context via `useAuth()` and `useSearchParams()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The page mounts the Wave 2 `<LoginForm>` and never re-implements form-level concerns (Zod validation, react-hook-form wiring, password show/hide). Those remain in the form layer.
|
||||
- The submit handler is `async` and catches synchronously; nothing propagates to a React error boundary on a failed `signIn`.
|
||||
- The guard is a side-effect of `useAuthGuard` — the page still renders the form for one frame while the guard's `useEffect` schedules the redirect.
|
||||
@@ -0,0 +1,64 @@
|
||||
# profile-account-section — `<AccountSection>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`profile-account-section.tsx` houses the delete-account tab renderer for `<ProfilePage>`. It is split out of `profile-sections.tsx` so each implementation file stays under the 300-line hard cap. The component renders an `<Alert variant="destructive">` framing a danger-zone block; clicking the initial delete button reveals a confirmation pane with a password input and two CTAs (`confirm-delete`, `cancel-delete`). The parent page owns the visibility state via `showDeleteConfirm` and the three callbacks (`onRequestDelete`, `onCancelDelete`, `onConfirmDelete`).
|
||||
|
||||
## Features
|
||||
|
||||
- Single-card destructive surface using the `react-ui` `<Alert variant="destructive">` primitive.
|
||||
- Two-stage confirmation: initial "Delete Account" button → confirm pane with password re-entry + "Yes, Delete My Account" + "Cancel".
|
||||
- The confirm button is disabled while `isLoading` is true OR the password field is empty.
|
||||
- Spinning loader (`<Loader2>`) appears alongside the deleting copy while the call is in flight.
|
||||
- Errors at the `currentPassword` field surface inline.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { AccountSection } from "./profile-account-section.js";
|
||||
|
||||
<AccountSection
|
||||
t={t}
|
||||
formData={formData}
|
||||
errors={errors}
|
||||
isLoading={isLoading}
|
||||
onChange={handleInputChange}
|
||||
showDeleteConfirm={showDeleteConfirm}
|
||||
onRequestDelete={() => setShowDeleteConfirm(true)}
|
||||
onCancelDelete={handleCancelDelete}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
/>;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Hooking an analytics callback into the delete flow is handled at the parent page level via `onDeleteAccount`. From this component's perspective, the `onConfirmDelete` callback is the only one that triggers a network call:
|
||||
|
||||
```tsx
|
||||
async function handleConfirmDelete() {
|
||||
if (!formData.currentPassword) {
|
||||
setErrors({ currentPassword: t.errorPasswordRequiredForDelete });
|
||||
return;
|
||||
}
|
||||
await deleteAccount(formData.currentPassword);
|
||||
track("account.deleted");
|
||||
navigate("/auth/login");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `AccountSectionProps extends SectionProps`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `showDeleteConfirm` | `boolean` | When `true`, the confirm pane is rendered. |
|
||||
| `onRequestDelete` | `() => void` | Invoked when the initial delete button is clicked. |
|
||||
| `onCancelDelete` | `() => void` | Invoked when the cancel button is clicked. |
|
||||
| `onConfirmDelete` | `() => Promise<void>` | Invoked when the confirm button is clicked. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The component reuses `formData.currentPassword` instead of introducing a `deletePassword` field — the parent page resets that field on cancel.
|
||||
- The placeholder copy on the delete-password input is intentionally the `errorPasswordRequiredForDelete` string (also used as the inline error). This matches upstream UX: the placeholder explains the requirement, the error reinforces it after a failed submit.
|
||||
- The component does not render its own `<form>` — the confirm button is a regular `<Button>` whose `onClick` triggers the delete. That keeps the password input out of the `<form>` element used by `<PasswordSection>` and prevents accidental form-submit collisions.
|
||||
@@ -0,0 +1,76 @@
|
||||
# profile-sections — `<ProfileSection>` + `<PasswordSection>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`profile-sections.tsx` houses the two read/write tab renderers for `<ProfilePage>`: `<ProfileSection>` (name + email update form) and `<PasswordSection>` (current / new / confirm password form with independent visibility toggles). It is a pure-UI render layer — all state and side effects live on the parent page. The split keeps each implementation file under the 300-line hard cap mandated by `CLAUDE.md` while keeping the section markup co-located so a designer changing the profile form does not have to navigate through `<ProfilePage>` itself.
|
||||
|
||||
`<AccountSection>` lives in a sibling file (`profile-account-section.tsx`) because the delete-confirmation flow adds ~120 lines of conditional markup that would push this file over the cap.
|
||||
|
||||
## Features
|
||||
|
||||
- `<ProfileSection>` — name + email inputs, submit button, optional email-level error display.
|
||||
- `<PasswordSection>` — three password rows (current / new / confirm) using a private `<PasswordRow>` factor; per-row visibility toggle button; submit button disabled until both `currentPassword` and `newPassword` are non-empty.
|
||||
- `<PasswordRow>` — private helper that renders `<Label>` + `<Input>` + show/hide `<Button>` with `aria-label="Show password" | "Hide password"`.
|
||||
- Strict typing: `SectionProps` is shared across all section renderers and is exported so `profile-account-section.tsx` can extend it.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { ProfileSection } from "./profile-sections.js";
|
||||
|
||||
<ProfileSection
|
||||
t={t}
|
||||
formData={formData}
|
||||
errors={errors}
|
||||
isLoading={isLoading}
|
||||
onChange={handleInputChange}
|
||||
onSubmit={handleProfileUpdate}
|
||||
/>;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
`<PasswordSection>` takes both a `show` record and a `toggle(key)` setter so the three visibility toggles share a single store on the parent:
|
||||
|
||||
```tsx
|
||||
const [show, setShow] = useState({ current: false, next: false, confirm: false });
|
||||
const toggle = (k) => setShow((prev) => ({ ...prev, [k]: !prev[k] }));
|
||||
|
||||
<PasswordSection
|
||||
{...sectionProps}
|
||||
onSubmit={handlePasswordChange}
|
||||
show={show}
|
||||
toggle={toggle}
|
||||
/>;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SectionProps`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `t` | `ProfilePageStrings` | Resolved translations table. |
|
||||
| `formData` | `ProfilePageFormData` | Current form values. |
|
||||
| `errors` | `Partial<ProfilePageFormData>` | Per-field error messages. |
|
||||
| `isLoading` | `boolean` | Disables every input. |
|
||||
| `onChange` | `(field) => ChangeHandler` | Per-field change-handler factory. |
|
||||
|
||||
### `ProfileSectionProps extends SectionProps`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `onSubmit` | `(e: FormEvent) => Promise<void>` | Form submit handler. |
|
||||
|
||||
### `PasswordSectionProps extends SectionProps`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `onSubmit` | `(e: FormEvent) => Promise<void>` | Form submit handler. |
|
||||
| `show` | `{ current; next; confirm }` | Per-row visibility flags. |
|
||||
| `toggle` | `(k) => void` | Flip a row's visibility. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `PasswordRow` is intentionally NOT exported — it is an internal factor to keep the password section's markup under control. The `aria-label` is hardcoded in English; if a downstream locale wants Welsh / Chinese show/hide labels it should compose a custom password section.
|
||||
- The submit-button disable condition on `<PasswordSection>` requires both `currentPassword` AND `newPassword` to be truthy — this matches upstream behaviour and keeps users from accidentally posting a change request with one field empty.
|
||||
@@ -0,0 +1,85 @@
|
||||
# `<ProfilePage>` — three-tab profile-settings page
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<ProfilePage>` is the routing-aware profile-settings surface for an authenticated user. It renders three tabs — Profile (name + email), Password (current / new / confirm), and Account (danger zone / delete account) — over the Wave 1 `useAuth()` action surface (`updateProfile`, `changePassword`, `deleteAccount`). It applies `useAuthGuard({ requireAuth: true })` so unauthenticated visitors are redirected to `/auth/login`, hydrates the profile form from `useAuth().user`, and exposes optional `onSuccess` and `onDeleteAccount` callbacks for downstream analytics. All copy flows through a `translations?: Partial<AuthProfilePageStrings>` prop defaulting to `enGb.auth.profilePage`.
|
||||
|
||||
The implementation is split into three files to honour the 300-line hard cap: `profile.tsx` (shell + state + actions), `profile-sections.tsx` (Profile and Password tab renderers), and `profile-account-section.tsx` (Account tab with delete-account confirmation flow). All three sit behind the same package entry point.
|
||||
|
||||
## Features
|
||||
|
||||
- Three tabs via `@suaveplan/react-ui` `<Tabs>`: profile / password / account.
|
||||
- Auth-guard: unauthenticated visitors redirected to `/auth/login`.
|
||||
- Hydrates name + email from `useAuth().user` on mount.
|
||||
- Calls `updateProfile`, `changePassword`, `deleteAccount` from the matching tab.
|
||||
- Local validation on password change: required, ≥8 chars, must match.
|
||||
- Delete-account confirmation flow with a password re-entry input.
|
||||
- Three independent password-visibility toggles (current / new / confirm).
|
||||
- `successMessage` and `error.message` alerts surfaced inline.
|
||||
- Locale overrides via `translations`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { ProfilePage } from "@suaveplan/react-auth";
|
||||
|
||||
export function ProfileRoute() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Hook into analytics:
|
||||
|
||||
```tsx
|
||||
import { ProfilePage } from "@suaveplan/react-auth";
|
||||
|
||||
export function MyProfile() {
|
||||
return (
|
||||
<ProfilePage
|
||||
onSuccess={() => track("profile.update.success")}
|
||||
onDeleteAccount={() => track("account.deleted")}
|
||||
translations={{
|
||||
title: "My Account",
|
||||
subtitle: "Keep your details up to date.",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ProfilePageProps`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `onSuccess` | `() => void` | Invoked after profile update or password change succeeds. |
|
||||
| `onDeleteAccount` | `() => void` | Invoked after `deleteAccount` resolves, before navigation. |
|
||||
| `translations` | `Partial<AuthProfilePageStrings>` | Locale overrides. |
|
||||
|
||||
### `ProfilePageFormData`
|
||||
|
||||
`{ name, email, currentPassword, newPassword, confirmPassword }` — five strings.
|
||||
|
||||
### `ProfilePageSection`
|
||||
|
||||
`{ id, title, active, icon }` — descriptor consumed by the `<TabsList>` render loop. `id` is one of `"profile" | "password" | "account"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The export is `<ProfilePage>`; the upstream `Profile` alias is preserved as a named re-export for back-compat.
|
||||
- The page does NOT navigate after `updateProfile` or `changePassword` succeed — it stays on the same route and surfaces a success alert above the tab bar.
|
||||
- The delete-account flow re-uses the `currentPassword` form field rather than a dedicated input, mirroring the upstream UX.
|
||||
- `clearMessages()` is invoked on every input change so success / error banners disappear when the user starts editing again.
|
||||
- `onValueChange` on `<Tabs>` clears messages so banners from one tab do not bleed across tab changes.
|
||||
@@ -0,0 +1,68 @@
|
||||
# `<RegisterPage>` — routing-aware sign-up page
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<RegisterPage>` is the routing-aware adapter that wraps the Wave 2 `<RegisterForm>` with the navigation and auth-context wiring needed for a sign-up route. It applies a guest-only auth guard, strips `confirmPassword` from the form payload before invoking `useAuth().signUp`, and navigates to `/dashboard` on success.
|
||||
|
||||
Use this component as the `Route element` for the `/auth/register` path. If a downstream application needs a richer flow (multi-step wizard, marketing copy, A/B testing) it can compose `<RegisterForm>` directly instead.
|
||||
|
||||
## Features
|
||||
|
||||
- `useAuthGuard({ requireGuest: true })` — already-authenticated visitors are redirected to `/`.
|
||||
- Strips the client-side-only `confirmPassword` field before calling `signUp`. The `RegisterData` payload that reaches the auth client matches the server contract.
|
||||
- `signUp` rejections are swallowed; errors surface via `useAuth().error` into the form's alert.
|
||||
- Navigates to `/dashboard` on success.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { RegisterPage } from "@suaveplan/react-auth";
|
||||
|
||||
export function AuthRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/auth/register" element={<RegisterPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combine with a custom landing redirect after sign-up. Because `<RegisterPage>` always navigates to `/dashboard`, an app that wants a different post-sign-up route can intercept via a `useAuth().onAuthStateChange` callback registered on the `<AuthProvider>`:
|
||||
|
||||
```tsx
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { AuthProvider } from "@suaveplan/react-auth";
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<AuthProvider
|
||||
config={{ baseURL: "/auth" }}
|
||||
onAuthStateChange={(state) => {
|
||||
if (state.isAuthenticated && state.user?.emailVerified === false) {
|
||||
navigate("/auth/verify-email");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RegisterPage />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
`<RegisterPage>` accepts no props. It reads context via `useAuth()` and navigates via `useNavigate()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The page never inspects the underlying form's Zod schema; it relies on the form to deliver a validated `RegisterFormData` payload.
|
||||
- The destructured `confirmPassword` discard uses a `void` statement to keep Biome's `noUnusedVariables` rule satisfied without resorting to `// biome-ignore`.
|
||||
- The `signUp` action wraps better-auth; failure modes (`USER_EXISTS`, `WEAK_PASSWORD`, network) propagate via the context `error` field.
|
||||
@@ -0,0 +1,56 @@
|
||||
# `<ResetPasswordPage>` — routing-aware password-reset page
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<ResetPasswordPage>` is the routing-aware adapter that wraps the Wave 2 `<ResetPasswordForm>` for the `?token=…`-bearing reset link emailed by `forgotPassword`. It extracts the token from the URL search params, hands it to `useAuth().resetPassword(token, password)` on submit, and drives the form between three states: entry (token present), success (after resolution), and invalid-token (token absent).
|
||||
|
||||
Use this as the `Route element` for `/auth/reset-password`. The component never re-implements the form-level password rules — both `password.min(8)` and the cross-field `confirmPassword` refinement live in the form.
|
||||
|
||||
## Features
|
||||
|
||||
- `useAuthGuard({ requireGuest: true })` so authenticated visitors are redirected away.
|
||||
- `?token` extracted via `useSearchParams`; `hasToken={!!token}` drives the form's branch.
|
||||
- `useAuth().resetPassword(token, password)` invoked on submit when a token is present.
|
||||
- Local `isSuccess` flips after a resolved call so the form renders the "password reset!" card.
|
||||
- Two navigation callbacks: `onNavigateToLogin` (`/auth/login`) and `onNavigateToForgotPassword` (`/auth/forgot-password`).
|
||||
- `resetPassword` rejections are swallowed; errors surface via `useAuth().error`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { ResetPasswordPage } from "@suaveplan/react-auth";
|
||||
|
||||
export function AuthRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/auth/reset-password" element={<ResetPasswordPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
When emailing the reset link, embed the token directly:
|
||||
|
||||
```ts
|
||||
const link = new URL("/auth/reset-password", site.origin);
|
||||
link.searchParams.set("token", resetToken);
|
||||
await mailer.send({ to, subject, body: link.toString() });
|
||||
```
|
||||
|
||||
The page extracts the token via `useSearchParams().get("token")` and renders the invalid-token branch if the user lands on the route without a token (e.g. they typed the URL by hand). Both branches share the same component instance so React preserves form state across the entry/success transitions, but throws state away on the entry/invalid transition (which only happens on page reload anyway).
|
||||
|
||||
## API Reference
|
||||
|
||||
`<ResetPasswordPage>` accepts no props. It reads context via `useAuth()` and routing via `useSearchParams` / `useNavigate`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `if (!token) return;` guard inside `handleSubmit` is defence in depth — the form already renders the invalid-token view in that case and never invokes `onSubmit`.
|
||||
- The page does not memoise the navigation callbacks; `<ResetPasswordForm>` re-renders cheaply and React's `useCallback` is not warranted here.
|
||||
@@ -0,0 +1,37 @@
|
||||
# `cy-gb`
|
||||
|
||||
## Purpose
|
||||
|
||||
`cy-gb.ts` ships the Welsh (Cymraeg, `cy-GB`) translation pack for the auth surface. The file conforms structurally to `AuthTranslationPack` — every key present in `en-gb.ts` MUST be present here, otherwise `tsc` fails the typecheck gate. This guarantees Wave 2 form components can resolve any key in any locale without locale-specific guards. Pure data; no runtime code.
|
||||
|
||||
## Features
|
||||
|
||||
- Welsh copy across all 10 functional groups.
|
||||
- Compile-time conformance to `AuthTranslationPack`.
|
||||
- Default-exported for direct consumption (`import cyGb from "@suaveplan/react-auth/translations"`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { cyGb } from "@suaveplan/react-auth";
|
||||
console.log(cyGb.auth.login.title); // "Mewngofnodi"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { authTranslations } from "@suaveplan/react-auth";
|
||||
|
||||
function detectLocaleAndResolve(prefersWelsh: boolean) {
|
||||
const locale = prefersWelsh ? "cy-gb" : "en-gb";
|
||||
return authTranslations[locale].auth.login.forgotPassword;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `default` — the Welsh translation pack of type `AuthTranslationPack`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Translations are reviewed by a fluent Welsh speaker before each release; the strings here are the verbatim upstream copy. When a string would mention a date or a number, the Wave 2 form components will inject ICU-formatted values through `@suaveplan/i18n-icu-parser` — the strings stored here intentionally avoid pre-formatted numerics. Punctuation conventions (e.g. apostrophe usage, full-stop placement in error messages) follow standard editorial conventions for Welsh, matching the upstream pack.
|
||||
@@ -0,0 +1,44 @@
|
||||
# `en-gb`
|
||||
|
||||
## Purpose
|
||||
|
||||
`en-gb.ts` ships the British-English string table consumed by the Wave 2 forms and Wave 3 page components. The file also defines the canonical `AuthTranslationPack` shape (broken into 10 named sub-interfaces — login, register, forgotPassword, resetPassword, emailVerification, profile, validation, errors, success, loading — so every locale conforms to the same structural contract). The default-exported object literal serves as both the production string source and the structural baseline against which `cy-gb` and `zh-cn` are checked at compile time. No runtime code; pure data.
|
||||
|
||||
## Features
|
||||
|
||||
- `AuthTranslationPack` interface (and 10 sub-interfaces) — structural shape every locale conforms to.
|
||||
- Default-exported object literal with British-English copy across 10 functional groups.
|
||||
- Compile-time pin: a missing or extra key in any locale fails `tsc`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import enGb from "@suaveplan/react-auth";
|
||||
|
||||
console.log(enGb.auth.login.title); // "Sign In"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Resolve a translation key from the active locale by walking the typed map:
|
||||
|
||||
```ts
|
||||
import { authTranslations, type AuthTranslationPack } from "@suaveplan/react-auth";
|
||||
|
||||
type LocaleKey = "en-gb" | "cy-gb" | "zh-cn";
|
||||
|
||||
function tLogin(locale: LocaleKey, key: keyof AuthTranslationPack["auth"]["login"]) {
|
||||
return authTranslations[locale].auth.login[key];
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AuthLoginStrings` / `AuthRegisterStrings` / `AuthForgotPasswordStrings` / `AuthResetPasswordStrings` / `AuthEmailVerificationStrings` / `AuthProfileStrings` / `AuthValidationStrings` / `AuthErrorStrings` / `AuthSuccessStrings` / `AuthLoadingStrings` — section interfaces.
|
||||
- `interface AuthStrings` — aggregate of the section interfaces.
|
||||
- `interface AuthTranslationPack` — `{ auth: AuthStrings }`.
|
||||
- `default` — the British-English translation pack.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The interface is defined here (not in `types/`) because it co-locates with the canonical source-of-truth string set. The compile-time conformance check on `cy-gb` and `zh-cn` works because each locale annotates its module-level constant with `: AuthTranslationPack`; if a section is missing or a typo introduces an extra field, `tsc` fails the typecheck gate. The `as const` pattern is deliberately avoided here — using a structural interface (not literal narrowing) lets translators ship variant copy without retyping the schema, and prevents the cross-locale assignment errors seen in upstream's first port.
|
||||
@@ -0,0 +1,36 @@
|
||||
# `zh-cn`
|
||||
|
||||
## Purpose
|
||||
|
||||
`zh-cn.ts` ships the Simplified-Chinese (`zh-CN`) translation pack for the auth surface. The file conforms structurally to `AuthTranslationPack`; every key from `en-gb.ts` is present, otherwise the typecheck gate fails. Wave 2 form components resolve keys against this pack identically to any other locale. Pure data; no runtime code.
|
||||
|
||||
## Features
|
||||
|
||||
- Simplified-Chinese copy across all 10 functional groups.
|
||||
- Compile-time conformance to `AuthTranslationPack`.
|
||||
- Default-exported for direct consumption.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { zhCn } from "@suaveplan/react-auth";
|
||||
console.log(zhCn.auth.login.title); // "登录"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { authTranslations } from "@suaveplan/react-auth";
|
||||
|
||||
function localisedRegister() {
|
||||
return authTranslations["zh-cn"].auth.register;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `default` — the Simplified-Chinese translation pack of type `AuthTranslationPack`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Translations are reviewed by a native speaker before each release. Punctuation follows Simplified-Chinese conventions: full-width commas (`,`) and full stops (`。`) where the English source uses ASCII punctuation, and quotation marks rendered as ASCII double-quotes only when wrapping a literal token like `"DELETE"`. Numerals stay ASCII for downstream ICU formatting. The pack is sized to match `en-gb` byte-for-byte structurally — translators add or remove copy by editing the upstream `en-gb.ts` first and propagating the schema change here.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `auth.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
`auth.types.ts` defines the public-surface auth data shapes consumed by `AuthClient`, `AuthProvider`, and the Wave 2 form / Wave 3 page components. The module is types-only and ships zero runtime code; it is the contract between the better-auth bridge layer and the React surface. Every shape is exported individually rather than packed behind a namespace so consumers can compose them into their own state machines. Field semantics match the upstream package so apps porting from `../suaveplan/packages/react/react-auth/` see byte-for-byte compatible types.
|
||||
|
||||
## Features
|
||||
|
||||
- `User` — extended user record with role bitmask, status, banning, two-factor, biological-sex, profile fields.
|
||||
- `Session` — active session token + IP / user-agent / impersonation fields.
|
||||
- `Account` — linked OAuth-or-credential record.
|
||||
- `LoginCredentials` / `RegisterData` / `ResetPasswordData` / `ResetPasswordCredentials` — input payloads for the four credential flows.
|
||||
- `UpdateProfileData` / `ChangePasswordData` — profile-mutation payloads.
|
||||
- `AuthResponse` — generic envelope returned by the better-auth bridge.
|
||||
- `AuthError` — structured error payload with `code` / `message` / `field`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { LoginCredentials, User } from "@suaveplan/react-auth";
|
||||
|
||||
const credentials: LoginCredentials = {
|
||||
email: "a@b.com",
|
||||
password: "secret",
|
||||
rememberMe: true,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type {
|
||||
AuthResponse,
|
||||
RegisterData,
|
||||
User,
|
||||
} from "@suaveplan/react-auth";
|
||||
|
||||
async function register(data: RegisterData): Promise<User | null> {
|
||||
const response: AuthResponse = await client.signUp(data) as AuthResponse;
|
||||
return response.user ?? null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type BiologicalSex` — `"prefer-not-to-say" | "traditional-male" | "traditional-female" | "other"`.
|
||||
- `interface User` — full user shape (id, email, username, name, roles, status, optional ban/2FA/phone fields, timestamps).
|
||||
- `interface Session` — active session row.
|
||||
- `interface Account` — linked auth account row.
|
||||
- `interface LoginCredentials` — `{ email; password; rememberMe? }`.
|
||||
- `interface RegisterData` — `{ email; password; username; firstName?; surname?; dob?; sex?; gender?; company? }`.
|
||||
- `interface ResetPasswordData` — `{ token; password }`.
|
||||
- `interface ResetPasswordCredentials` — `{ password; confirmPassword }`.
|
||||
- `interface UpdateProfileData` — partial profile mutation payload.
|
||||
- `interface ChangePasswordData` — `{ currentPassword; newPassword }`.
|
||||
- `interface AuthResponse` — `{ user?; session?; token?; message? }`.
|
||||
- `interface AuthError` — `{ code; message; field? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`User.roles` is a bitmask consumed by `usePermission`. `User.status` is an opaque numeric field that downstream sites map to their own state machines (active / suspended / pending). Date-like fields are ISO strings throughout; the wrapper normalises `Date` to string at the bridge layer (see `better-auth-adapter`). `Session.expiresAt` is the one exception — it carries a `Date` because consumer code needs to compute `session.expiresAt.getTime() - Date.now()` for refresh logic. The optional `BiologicalSex` union is closed at the type level so registration form components can derive their `<select>` options from the type via `BiologicalSex` itself.
|
||||
@@ -0,0 +1,53 @@
|
||||
# `crud.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
`crud.types.ts` defines the multi-tenant CRUD data shapes shared between `@suaveplan/react-auth` and downstream consumer apps. These are the wire-format shapes returned by the backend's site / profile / social / notification endpoints — the auth surface owns them because every authenticated consumer ends up rendering a `<Profile>` page or a notification list as part of the auth flow. The module is types-only with zero runtime code. Every shape carries explicit primitive / union-typed fields so JSON deserialisation can be checked at the boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- `Site` — tenant record with theme / push / feature-flag settings.
|
||||
- `UserSettings` — per-user social / privacy / theme / notification block.
|
||||
- `BankAccount` / `Friend` / `Group` / `Post` / `Message` / `Notification` / `Event` — social-graph + audit records.
|
||||
- `Role` / `DeviceToken` / `PushNotification` — admin + push-delivery records.
|
||||
- Every nested config block is its own named interface (no inline object literals — rule 11 compliance).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { Site, UserSettings } from "@suaveplan/react-auth";
|
||||
|
||||
async function loadTenant(siteId: string): Promise<Site> {
|
||||
const resp = await fetch(`/api/sites/${siteId}`);
|
||||
return (await resp.json()) as Site;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type { Friend, Group, Post } from "@suaveplan/react-auth";
|
||||
|
||||
interface SocialFeedItem {
|
||||
readonly group: Group;
|
||||
readonly posts: readonly Post[];
|
||||
readonly friends: readonly Friend[];
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface SiteApplePushConfig` — `{ key_id; team_id; bundle_id; key_path; environment }`.
|
||||
- `interface SiteFirebasePushConfig` — `{ server_key; project_id }`.
|
||||
- `interface SitePushConfig` — `{ apple?; firebase? }`.
|
||||
- `interface SiteThemeConfig` — `{ primary_color; secondary_color; font_family }`.
|
||||
- `interface SiteSettings` — `{ maintenance_mode; theme; features; push? }`.
|
||||
- `interface Site` — full tenant row.
|
||||
- `interface UserSettingsCounters` / `UserSettingsPrivacy` / `UserSettingsLocalization` / `UserSettingsTheme` / `UserSettingsNotifications` / `UserSettingsSocialLimits` — per-section settings blocks.
|
||||
- `interface UserSettings` — aggregate user-settings row.
|
||||
- `interface BankAccount` — linked bank-account row.
|
||||
- `interface Friend` / `Group` / `PostLocation` / `Post` / `Message` / `Notification` / `Event` / `Role` / `DeviceToken` / `PushNotification` — see source for field detail.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Field naming follows the backend's snake_case convention (`created_at`, `member_count`) intentionally — the React surface deserialises JSON without renaming so callers know exactly what wire shape they're holding. Where downstream code needs camelCase, the `betterAuthAdapter` or a dedicated mapper at the call site handles the rename. Every nested block (push config, theme, counters) is broken into its own named interface to satisfy CLAUDE.md rule 11 (no inline object literals in interfaces) and to allow individual blocks to be referenced from validation schemas in Wave 2.
|
||||
Reference in New Issue
Block a user