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:
@@ -0,0 +1,106 @@
|
||||
# @suaveplan/react-auth-whitelabel - White-label authentication React components with tenant branding wired through @suaveplan/react-ui runtime theme mutator
|
||||
|
||||
White-label authentication React components with tenant branding wired
|
||||
through the `@suaveplan/react-ui` runtime theme-token mutator.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [login-form](./src/components/login-form/login-form.md) — `<LoginForm>` is the white-label login surface consumed by every tenant in the stack. It renders a controlled e…
|
||||
- [login-form](./src/components/login-form/login-form.md) — `<LoginForm>` is the white-label login surface consumed by every tenant in the stack. It renders a controlled e…
|
||||
- [password-reset-form](./src/components/password-reset-form/password-reset-form.md) — `<PasswordResetForm>` is the white-label password-reset surface. It owns a single state machine across two modes — `req…
|
||||
- [password-reset-confirm](./src/components/password-reset-form/password-reset-confirm.md) — `PasswordResetConfirm` is the confirm-mode body of `<PasswordResetForm>` — the screen the user lands on from the email…
|
||||
- [password-reset-form](./src/components/password-reset-form/password-reset-form.md) — `<PasswordResetForm>` is the white-label password-reset surface. It owns a single state machine across two modes — `req…
|
||||
- [password-reset-request](./src/components/password-reset-form/password-reset-request.md) — `PasswordResetRequest` is the request-mode body of `<PasswordResetForm>` — the screen the user lands on when they click…
|
||||
- [password-reset-success](./src/components/password-reset-form/password-reset-success.md) — `PasswordResetSuccess` is the success state of `<PasswordResetForm>`, shared between the request and confirm modes. It…
|
||||
- [register-form](./src/components/register-form/register-form.md) — `<RegisterForm>` is the white-label self-registration surface. It renders the header (logo + title + subtitle), the inl…
|
||||
- [register-form-fields](./src/components/register-form/register-form-fields.md) — `RegisterFormFields` is the inner field block of `<RegisterForm>` — name (optional, optionally required), email, passwo…
|
||||
- [register-form-terms](./src/components/register-form/register-form-terms.md) — `RegisterFormTerms` is the terms-acceptance checkbox block of `<RegisterForm>`. It renders the acceptance checkbox, the…
|
||||
- [register-form](./src/components/register-form/register-form.md) — `<RegisterForm>` is the white-label self-registration surface. It renders the header (logo + title + subtitle), the inl…
|
||||
- [auth-context](./src/context/auth-context.md) — `auth-context.tsx` ships the `<AuthProvider>` React context provider together with the three hooks `useAuth`, `useUser`…
|
||||
- [auth-http](./src/context/auth-http.md) — `auth-http.ts` houses the three helpers every `AuthProvider` verb shares: `resolveFetch`, `resolveNavigate`, and `postJ…
|
||||
- [branding-context](./src/context/branding-context.md) — `branding-context.tsx` ships the `<BrandingProvider>` React context provider together with the hooks `useBranding`, `us…
|
||||
- [error-codes](./src/errors/error-codes.md) — `error-codes.ts` registers the canonical, stable error-code registry used by every error thrown from `@suaveplan/react-…
|
||||
- [react-auth-whitelabel-error](./src/errors/react-auth-whitelabel-error.md) — `ReactAuthWhitelabelError` is the canonical error class for every error thrown by `@suaveplan/react-auth-whitelabel`. I…
|
||||
- [types](./src/types/branding.schema.md) — `TenantBrandingSchema` is the Zod schema that validates every `TenantBranding` payload entering the package from an unt…
|
||||
- [branding.schema](./src/types/branding.schema.md) — `TenantBrandingSchema` is the Zod schema that validates every `TenantBranding` payload entering the package from an unt…
|
||||
- [utils](./src/utils/navigate-or-call.md) — `navigateOrCall(target)` is the canonical helper used by every form component in this package to act on a "URL or click…
|
||||
- [navigate-or-call](./src/utils/navigate-or-call.md) — `navigateOrCall(target)` is the canonical helper used by every form component in this package to act on a "URL or click…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## What ships
|
||||
|
||||
- **Forms** — `LoginForm`, `RegisterForm`, `PasswordResetForm`
|
||||
- **Contexts** — `AuthProvider`, `BrandingProvider`
|
||||
- **Hooks** — `useAuth`, `useBranding`, `useIsAuthenticated`, `useLogo`,
|
||||
`useTenantBranding`, `useUser`
|
||||
- **Types** — `AuthConfig`, `AuthContextValue`, `AuthUser`,
|
||||
`BrandingContextValue`, `BrandingProviderProps`, `RegisterData`,
|
||||
`TenantBranding`, `TenantBrandingSchema`
|
||||
- **Errors** — `REACT_AUTH_WL_CODES` + `ReactAuthWhitelabelError`
|
||||
|
||||
## Quick start
|
||||
|
||||
`BrandingProvider` calls `useThemeMutator()` from
|
||||
`@suaveplan/react-ui/theme`. That hook MUST be invoked inside a
|
||||
`<ThemeProvider>`. Wrap your subtree like this:
|
||||
|
||||
```tsx
|
||||
import { ThemeProvider, lightTheme } from "@suaveplan/react-ui/theme";
|
||||
import {
|
||||
AuthProvider,
|
||||
BrandingProvider,
|
||||
LoginForm,
|
||||
} from "@suaveplan/react-auth-whitelabel";
|
||||
|
||||
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
|
||||
<BrandingProvider
|
||||
brandingEndpoint="/api/branding"
|
||||
tenantId={tenantSlug}
|
||||
>
|
||||
<AuthProvider config={{ authUrl: "/api/auth" }}>
|
||||
<LoginForm
|
||||
onForgotPassword={() => router.push("/forgot")}
|
||||
onRegister={() => router.push("/register")}
|
||||
/>
|
||||
</AuthProvider>
|
||||
</BrandingProvider>
|
||||
</ThemeProvider>;
|
||||
```
|
||||
|
||||
On every branding change, `BrandingProvider` writes `--brand-primary`,
|
||||
`--brand-secondary`, `--brand-accent`, and `--brand-font` to
|
||||
`document.documentElement.style` via
|
||||
`useThemeMutator().setExtensionTokens(...)`. Cleanup is automatic on
|
||||
unmount.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/components/login-form/login-form.md`](./src/components/login-form/login-form.md)
|
||||
- [`src/components/register-form/register-form.md`](./src/components/register-form/register-form.md)
|
||||
- [`src/components/register-form/register-form-fields.md`](./src/components/register-form/register-form-fields.md)
|
||||
- [`src/components/register-form/register-form-terms.md`](./src/components/register-form/register-form-terms.md)
|
||||
- [`src/components/password-reset-form/password-reset-form.md`](./src/components/password-reset-form/password-reset-form.md)
|
||||
- [`src/components/password-reset-form/password-reset-request.md`](./src/components/password-reset-form/password-reset-request.md)
|
||||
- [`src/components/password-reset-form/password-reset-confirm.md`](./src/components/password-reset-form/password-reset-confirm.md)
|
||||
- [`src/components/password-reset-form/password-reset-success.md`](./src/components/password-reset-form/password-reset-success.md)
|
||||
- [`src/context/auth-context.md`](./src/context/auth-context.md)
|
||||
- [`src/context/auth-http.md`](./src/context/auth-http.md)
|
||||
- [`src/context/branding-context.md`](./src/context/branding-context.md)
|
||||
- [`src/types/branding.schema.md`](./src/types/branding.schema.md)
|
||||
- [`src/errors/error-codes.md`](./src/errors/error-codes.md)
|
||||
- [`src/errors/react-auth-whitelabel-error.md`](./src/errors/react-auth-whitelabel-error.md)
|
||||
|
||||
## Out of scope (Excluded)
|
||||
|
||||
- MFA / passkey / WebAuthn flows — deferred until `@suaveplan/webauthn`
|
||||
- `@suaveplan/otp` ship .
|
||||
- Magic-link / SMS OTP — depends on `@suaveplan/security-codes` /
|
||||
`@suaveplan/emailer` / `@suaveplan/sms`.
|
||||
- Better-Auth first-class adapter — overlaps with the concurrent
|
||||
`@suaveplan/react-auth` Wave 1.
|
||||
- Audit-log integration, password-strength meter, rate limiter, axe-core
|
||||
story gate — depend on packages not yet .
|
||||
- Bundle-size snapshot — repo-wide tooling not yet wired.
|
||||
- Playwright E2E specs — see `e2e/README.md`.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# E2E specs — deferred
|
||||
|
||||
### Excluded
|
||||
|
||||
The three upstream Playwright specs at
|
||||
`/mnt/s/web/suaveplan/packages/react/react-auth-whitelabel/e2e/{login-form,register-form,password-reset-form}.spec.ts`
|
||||
were not ported in the initial migration. They rely on a running Storybook
|
||||
instance at `/iframe.html?id=components-loginform--default&viewMode=story`
|
||||
and on Playwright wiring that has not yet landed in Suaveplan.
|
||||
Once `@suaveplan/storybook-test-utils` (referenced in the enterprise plan
|
||||
§4 as a soft dep) ships and `playwright.config.ts` is wired at the repo
|
||||
root for the React stack, port the three specs verbatim — every selector
|
||||
already matches the JSX output (labels, `role="alert"`, button
|
||||
names are identical to upstream because the `useTranslation()` default
|
||||
values are byte-identical).
|
||||
|
||||
Manual smoke coverage in the interim:
|
||||
|
||||
```
|
||||
bun run --filter=@suaveplan/react-auth-whitelabel build-storybook
|
||||
# then open the served bundle and walk every story
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
# `<LoginForm>`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<LoginForm>` is the white-label login surface consumed by every tenant in
|
||||
the stack. It renders a controlled email + password form with
|
||||
optional remember-me checkbox, optional OAuth provider list, and optional
|
||||
forgot-password / register-link affordances. The component pulls its visible
|
||||
strings from `useTranslation()` so every label is localised, sources its
|
||||
logo + tenant name from `useBranding()` + `useLogo()`, and delegates the
|
||||
actual auth call to `useAuth().login(...)` unless the caller supplies an
|
||||
`onSubmit` override. Submission errors are rendered inline in a `role="alert"`
|
||||
region; provider-button clicks invoke `useAuth().loginWithProvider(...)`.
|
||||
|
||||
## Features
|
||||
|
||||
- Fully controlled inputs — no `react-hook-form` dependency.
|
||||
- `useTranslation()` with `{ defaultValue }` for every visible string.
|
||||
- Optional OAuth provider button list.
|
||||
- Optional remember-me, forgot-password, register-link affordances.
|
||||
- `onSubmit` override for plug-in flows (Better Auth, custom REST).
|
||||
- Custom logo, title, subtitle overrides.
|
||||
- Disabled state propagates to every input + button.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<LoginForm
|
||||
onForgotPassword={() => router.push("/forgot")}
|
||||
onRegister={() => router.push("/register")}
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<LoginForm
|
||||
providers={[
|
||||
{ id: "google", name: "Google", icon: GoogleIcon },
|
||||
{ id: "github", name: "GitHub", icon: "https://cdn/github.svg" },
|
||||
]}
|
||||
showRememberMe
|
||||
onSubmit={async ({ email, password, rememberMe }) => {
|
||||
await betterAuthClient.signIn.email({ email, password, rememberMe });
|
||||
}}
|
||||
onSuccess={() => router.push("/dashboard")}
|
||||
onError={(err) => analytics.track("login_error", { code: err.code })}
|
||||
logo={<TenantLogo />}
|
||||
title={t("custom.title")}
|
||||
subtitle={t("custom.subtitle")}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LoginForm(props: LoginFormProps): ReactNode` — props on
|
||||
`login-form.types.ts`.
|
||||
- Returns a `role="alert"` region on error; a `role="form"` is implied via
|
||||
the native `<form>` element.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The OAuth list is hidden when `providers.length === 0`, so a tenant with no
|
||||
configured providers gets a clean form. The remember-me checkbox is a UI
|
||||
hint only — the actual semantics live with the backend the consumer wires
|
||||
through `AuthConfig.fetch`. Provider-icon rendering supports both a React
|
||||
component (`provider.icon` as `ComponentType<{ className?: string }>`) and
|
||||
a URL string (`<img src={...} />`). The `navigateOrCall` helper inside the
|
||||
file handles the string-or-handler shape on every link prop so consumers can
|
||||
pass a URL or an in-app router callback.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `<PasswordResetConfirm>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`PasswordResetConfirm` is the confirm-mode body of `<PasswordResetForm>` —
|
||||
the screen the user lands on from the email link with a `token` query
|
||||
parameter. It renders the new-password input, the confirm-password input,
|
||||
the submit button (with translated in-flight copy), and an optional
|
||||
back-to-login affordance. It is a separate named sub-component so the
|
||||
parent stays under the 300-LOC hard budget and so the request/confirm
|
||||
markup can be tested independently. The parent owns the `token` and the
|
||||
network call; this component is purely the controlled-input shell.
|
||||
|
||||
## Features
|
||||
|
||||
- Two controlled inputs driven by the parent.
|
||||
- Browser-native `minLength={minPasswordLength}` on the password field
|
||||
supplements the parent's JS validation.
|
||||
- Submit copy toggles via the parent's `isSubmitting` flag.
|
||||
- Optional back-to-login affordance.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<PasswordResetConfirm
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
onSubmit={handleSubmit}
|
||||
minPasswordLength={8}
|
||||
isDisabled={false}
|
||||
isSubmitting={false}
|
||||
showLoginLink
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<PasswordResetConfirm
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await resetPassword(token, password);
|
||||
setIsSuccess(true);
|
||||
}}
|
||||
minPasswordLength={12}
|
||||
isDisabled={isLoading}
|
||||
isSubmitting={isSubmitting}
|
||||
showLoginLink
|
||||
onLogin={() => router.push("/login")}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PasswordResetConfirm(props: PasswordResetConfirmProps): ReactNode` —
|
||||
see `password-reset-form.types.ts` for the prop shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The component intentionally does NOT receive the reset token — the parent
|
||||
holds it and either threads it into the `resetPassword(token, password)`
|
||||
call or surfaces a `MISSING_RESET_TOKEN` error in the parent's `role="alert"`
|
||||
region BEFORE handing off to this sub-component. Splitting that policy
|
||||
out keeps the confirm-body purely concerned with the controlled inputs.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `<PasswordResetForm>`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<PasswordResetForm>` is the white-label password-reset surface. It owns a
|
||||
single state machine across two modes — `request` (email entry) and
|
||||
`confirm` (new password entry with a reset token) — and a shared success
|
||||
state. The component delegates the request-mode body to
|
||||
`<PasswordResetRequest>`, the confirm-mode body to
|
||||
`<PasswordResetConfirm>`, and the success view to
|
||||
`<PasswordResetSuccess>`, keeping each rendered slice independently
|
||||
testable and the parent file under the 300-LOC hard budget. Confirm mode
|
||||
short-circuits with an inline `MISSING_RESET_TOKEN` error when the
|
||||
`token` prop is missing — the network is never touched.
|
||||
|
||||
## Features
|
||||
|
||||
- Dual-mode form sharing a single state machine.
|
||||
- Local validation (min length, confirm-password match, missing token)
|
||||
blocks submission before any network call.
|
||||
- Optional `onSubmit` override.
|
||||
- Mode-aware default titles, subtitles, and success messages — every
|
||||
string sourced from `useTranslation()`.
|
||||
- Optional back-to-login affordance shared by every mode.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<PasswordResetForm mode="request" onLogin="/login" />
|
||||
```
|
||||
|
||||
```tsx
|
||||
<PasswordResetForm
|
||||
mode="confirm"
|
||||
token={new URLSearchParams(location.search).get("token") ?? undefined}
|
||||
onLogin="/login"
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<PasswordResetForm
|
||||
mode={mode}
|
||||
token={token}
|
||||
minPasswordLength={12}
|
||||
onSubmit={async (values) => {
|
||||
if ("email" in values) {
|
||||
await betterAuthClient.forgetPassword({ email: values.email });
|
||||
} else {
|
||||
await betterAuthClient.resetPassword({ token, password: values.password });
|
||||
}
|
||||
}}
|
||||
successMessage={customSuccess}
|
||||
onSuccess={() => analytics.track("password_reset_success")}
|
||||
onError={(err) => analytics.track("password_reset_error", { code: err.code })}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PasswordResetForm(props: PasswordResetFormProps): ReactNode` — see
|
||||
`password-reset-form.types.ts`. `minPasswordLength` defaults to 8;
|
||||
`mode` defaults to `"request"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The success state is a single boolean — both modes share the same
|
||||
`isSuccess` so the success view renders identically regardless of which
|
||||
mode produced it. The mode-specific copy lives in lookup tables
|
||||
(`defaultTitles`, `defaultSubtitles`, `defaultSuccessMessages`) keyed by
|
||||
the `mode` discriminator. Submission errors are surfaced inline AND
|
||||
forwarded to the `onError` callback; consumers that want analytics
|
||||
without UI side-effects can ignore the inline error region by overriding
|
||||
its styling.
|
||||
@@ -0,0 +1,64 @@
|
||||
# `<PasswordResetRequest>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`PasswordResetRequest` is the request-mode body of `<PasswordResetForm>` —
|
||||
the screen the user lands on when they click "Forgot password?". It
|
||||
renders the email input, the submit button (with a translated
|
||||
"Sending..." copy while the network call is in flight), and an optional
|
||||
back-to-login affordance. It exists as a named sub-component so the parent
|
||||
`password-reset-form.tsx` stays under the 300-LOC hard budget while
|
||||
keeping the request-vs-confirm modes structurally separate.
|
||||
|
||||
## Features
|
||||
|
||||
- Single controlled email input driven by the parent.
|
||||
- Submit button copy toggles between "Send Reset Link" / "Sending..."
|
||||
via the parent's `isSubmitting` flag.
|
||||
- Optional back-to-login affordance (URL or handler).
|
||||
- Every visible string sourced from `useTranslation()`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<PasswordResetRequest
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
onSubmit={handleSubmit}
|
||||
isDisabled={false}
|
||||
isSubmitting={false}
|
||||
showLoginLink
|
||||
onLogin={() => router.push("/login")}
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<PasswordResetRequest
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await requestPasswordReset(email);
|
||||
setIsSuccess(true);
|
||||
}}
|
||||
isDisabled={isLoading}
|
||||
isSubmitting={isSubmitting}
|
||||
showLoginLink
|
||||
onLogin="https://app.example.com/login"
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PasswordResetRequest(props: PasswordResetRequestProps): ReactNode` —
|
||||
see `password-reset-form.types.ts`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The component takes the submit handler as a prop instead of owning it,
|
||||
because the parent form needs to coordinate request-vs-confirm submissions
|
||||
through a single `isSubmitting` / `isSuccess` state machine. The
|
||||
`navigateOrCall` helper handles both string-URL and callback shapes for
|
||||
`onLogin` so consumers can pass either.
|
||||
@@ -0,0 +1,52 @@
|
||||
# `<PasswordResetSuccess>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`PasswordResetSuccess` is the success state of `<PasswordResetForm>`,
|
||||
shared between the request and confirm modes. It renders a mode-specific
|
||||
heading ("Check Your Email" for request mode, "Password Reset Complete"
|
||||
for confirm mode), the success copy in a `role="status"` live region,
|
||||
and an optional back-to-login button. The component is intentionally
|
||||
stateless — it is purely a presentational view of the parent's success
|
||||
state machine, which lets the parent file stay under the 300-LOC budget.
|
||||
|
||||
## Features
|
||||
|
||||
- Mode-specific success heading.
|
||||
- `role="status"` live region for assistive technology.
|
||||
- Optional back-to-login affordance (URL or handler).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<PasswordResetSuccess
|
||||
mode="request"
|
||||
message={t("auth.forgotPassword.successMessage", { defaultValue: "..." })}
|
||||
showLoginLink
|
||||
onLogin="/login"
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<PasswordResetSuccess
|
||||
mode={mode}
|
||||
message={successMessage ?? defaultSuccessMessages[mode]}
|
||||
showLoginLink={showLoginLink}
|
||||
onLogin={onLogin}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `PasswordResetSuccess(props: PasswordResetSuccessProps): ReactNode` —
|
||||
see `password-reset-form.types.ts`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`role="status"` is the WAI-ARIA pattern for non-interruptive live
|
||||
announcements — assistive technologies will announce the success copy
|
||||
without stealing focus. The parent owns the message string so a tenant
|
||||
override (passed via `successMessage`) flows through this component
|
||||
without any translation indirection.
|
||||
@@ -0,0 +1,64 @@
|
||||
# `<RegisterFormFields>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`RegisterFormFields` is the inner field block of `<RegisterForm>` — name
|
||||
(optional, optionally required), email, password, and confirm-password
|
||||
inputs together with their labels and required indicators. It exists as a
|
||||
named sub-component so the parent `register-form.tsx` stays under the
|
||||
300-LOC hard budget while keeping every field's translation key, autocomplete
|
||||
attribute, and placeholder co-located with the input element. All four
|
||||
inputs are fully controlled by the parent — every value + setter pair is
|
||||
passed in through props.
|
||||
|
||||
## Features
|
||||
|
||||
- Fully controlled inputs driven by parent setters.
|
||||
- `autocomplete` set per field (`name`, `email`, `new-password`).
|
||||
- Required indicators driven by `requireName` and the always-required
|
||||
email + password.
|
||||
- `minLength={minPasswordLength}` on the password field — supplements the
|
||||
parent's JS validation with browser-native min-length check.
|
||||
- Every label/placeholder routed through `useTranslation()` with
|
||||
`{ defaultValue }`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<form>
|
||||
<RegisterFormFields {...fieldProps} />
|
||||
</form>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<RegisterFormFields
|
||||
name={name}
|
||||
setName={setName}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
showNameField
|
||||
requireName
|
||||
minPasswordLength={12}
|
||||
isDisabled={isLoading || isSubmitting}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RegisterFormFields(props: RegisterFormFieldsProps): ReactNode` — see
|
||||
`register-form.types.ts` for the prop shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The component is pure — it renders state but does not own it. The parent
|
||||
form handles submission, error rendering, and submit-button copy. Splitting
|
||||
the field block from the form shell isolates the markup churn (which is
|
||||
mostly localised string keys) from the submission logic, making both files
|
||||
easier to audit. Coverage is exercised end-to-end via the parent form's
|
||||
test file.
|
||||
@@ -0,0 +1,58 @@
|
||||
# `<RegisterFormTerms>`
|
||||
|
||||
## Purpose
|
||||
|
||||
`RegisterFormTerms` is the terms-acceptance checkbox block of
|
||||
`<RegisterForm>`. It renders the acceptance checkbox, the configurable
|
||||
terms-of-service affordance (rendered as a link when `termsUrl` is supplied,
|
||||
plain text otherwise), and an optional privacy-policy link. Splitting it
|
||||
into its own sub-component keeps the parent form under the 300-LOC budget
|
||||
while making the (slightly intricate) "link OR text" markup easy to test in
|
||||
isolation. When `showTermsCheckbox` is `false`, the component renders
|
||||
`null` so the parent does not need to wrap it in a conditional.
|
||||
|
||||
## Features
|
||||
|
||||
- Checkbox is fully controlled by the parent.
|
||||
- Terms text renders as `<a target="_blank" rel="noopener noreferrer">`
|
||||
when `termsUrl` is supplied, plain inline text otherwise.
|
||||
- Optional privacy-policy `<a>` appended after a translated "and" connector.
|
||||
- Renders `null` when `showTermsCheckbox` is `false`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<RegisterFormTerms
|
||||
acceptTerms={acceptTerms}
|
||||
setAcceptTerms={setAcceptTerms}
|
||||
showTermsCheckbox
|
||||
isDisabled={false}
|
||||
/>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<RegisterFormTerms
|
||||
acceptTerms={acceptTerms}
|
||||
setAcceptTerms={setAcceptTerms}
|
||||
showTermsCheckbox
|
||||
termsUrl="https://example.com/terms"
|
||||
privacyUrl="https://example.com/privacy"
|
||||
isDisabled={isSubmitting}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RegisterFormTerms(props: RegisterFormTermsProps): ReactNode` — props on
|
||||
`register-form.types.ts`. Returns `null` when the checkbox is hidden.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The "and" connector is a separate translation key so the surrounding
|
||||
fragment composes cleanly in locales where the connector is more than a
|
||||
word (e.g. Welsh "ac" vs Spanish "y"). The component intentionally keeps no
|
||||
state of its own — the parent form is the single source of truth for the
|
||||
`acceptTerms` flag, and the parent's submit validator gates submission on
|
||||
that flag.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `<RegisterForm>`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`<RegisterForm>` is the white-label self-registration surface. It renders
|
||||
the header (logo + title + subtitle), the inline error region, the field
|
||||
block (delegated to `RegisterFormFields`), the terms checkbox (delegated
|
||||
to `RegisterFormTerms`), the submit button, an optional OAuth provider
|
||||
list, and an optional back-to-login affordance. The component validates
|
||||
password length, password equality, name presence (when `requireName`),
|
||||
and terms acceptance BEFORE calling `useAuth().register(...)` (or the
|
||||
`onSubmit` override). Every validation error renders inline in the
|
||||
`role="alert"` region and short-circuits the auth call so the network is
|
||||
never touched until the form is locally clean.
|
||||
|
||||
## Features
|
||||
|
||||
- Local validation: min password length, confirm-password match,
|
||||
required name, terms acceptance.
|
||||
- Field block + terms block extracted into sub-components so this file
|
||||
stays under the 300-LOC hard budget.
|
||||
- `onSubmit` override for plug-in flows.
|
||||
- Optional OAuth providers, optional back-to-login link.
|
||||
- Custom logo / title / subtitle.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<RegisterForm onLogin={() => router.push("/login")} />
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<RegisterForm
|
||||
showNameField
|
||||
requireName
|
||||
minPasswordLength={12}
|
||||
termsUrl="https://example.com/tos"
|
||||
privacyUrl="https://example.com/privacy"
|
||||
providers={[{ id: "google", name: "Google" }]}
|
||||
onSubmit={async (values) => {
|
||||
await betterAuthClient.signUp.email(values);
|
||||
}}
|
||||
onSuccess={() => router.push("/welcome")}
|
||||
onError={(err) => analytics.track("signup_error", { code: err.code })}
|
||||
/>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RegisterForm(props: RegisterFormProps): ReactNode` — see
|
||||
`register-form.types.ts`. `minPasswordLength` defaults to 8.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The validator is intentionally pre-`fetch` so a network round-trip never
|
||||
runs against an invalid local form. The translator routes every error
|
||||
message through `t(..., { defaultValue })` so a tenant overriding the
|
||||
default English copy will see their override even on local validation.
|
||||
The sub-component split is a budget-only concern; the parent owns all
|
||||
state and submission logic.
|
||||
@@ -0,0 +1,75 @@
|
||||
# `<AuthProvider>` and `useAuth` / `useUser` / `useIsAuthenticated`
|
||||
|
||||
## Purpose
|
||||
|
||||
`auth-context.tsx` ships the `<AuthProvider>` React context provider together
|
||||
with the three hooks `useAuth`, `useUser`, and `useIsAuthenticated`. The
|
||||
provider wires a tenant-aware auth backend through an injectable `fetch`
|
||||
(`FetchLike`) and an injectable `navigate(url)` seam so consumers can plug
|
||||
real backends in production and stub them in tests without `vi.mock`. Every
|
||||
verb — `login`, `loginWithProvider`, `register`, `logout`,
|
||||
`requestPasswordReset`, `resetPassword` — is surfaced on the context value
|
||||
returned by `useAuth()` and translates HTTP failures into stable, code-tagged
|
||||
errors via `ReactAuthWhitelabelError`.
|
||||
|
||||
## Features
|
||||
|
||||
- Injectable `fetch: FetchLike` for testability — defaults to
|
||||
`globalThis.fetch`.
|
||||
- Injectable `navigate(url)` seam — defaults to `window.location.href = url`
|
||||
in browsers, no-op in non-browser environments.
|
||||
- Session probe on mount unless `initialUser` is supplied (SSR/hydration).
|
||||
- Network failures → `REACT_AUTH_WL.AUTH_NETWORK_FAILED`.
|
||||
- Non-ok responses → `REACT_AUTH_WL.AUTH_REJECTED` with the backend's
|
||||
`message` field threaded through when present.
|
||||
- Hooks called outside the provider throw
|
||||
`REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
<AuthProvider config={{ authUrl: "/api/auth" }}>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
```
|
||||
|
||||
```tsx
|
||||
const { login, user, isAuthenticated } = useAuth();
|
||||
await login("a@b.c", "pw");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
const config: AuthConfig = {
|
||||
authUrl: "https://auth.acme.com",
|
||||
redirectUrl: "/dashboard",
|
||||
fetch: customFetch, // testable seam
|
||||
navigate: (url) => router.navigate(url),
|
||||
providers: [{ id: "google", name: "Google" }],
|
||||
};
|
||||
|
||||
<AuthProvider config={config} initialUser={ssrUser}>
|
||||
<Routes>...</Routes>
|
||||
</AuthProvider>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AuthProvider({ config, initialUser?, children })` — context provider.
|
||||
- `useAuth()` — returns `AuthContextValue` (`user`, `isLoading`,
|
||||
`isAuthenticated`, `error`, `login`, `loginWithProvider`, `register`,
|
||||
`logout`, `requestPasswordReset`, `resetPassword`).
|
||||
- `useUser()` — returns `AuthUser | null`.
|
||||
- `useIsAuthenticated()` — returns `boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The session probe in `useEffect` swallows transport errors on purpose — a
|
||||
session check that fails should not surface as a visible error to the user
|
||||
(they simply remain anonymous). Verbs called from the UI translate failures
|
||||
to typed errors AND surface them on the context's `error` state so a single
|
||||
error region can render the current failure without each call site
|
||||
re-implementing the same try/catch. Every verb sets `isLoading` true/false
|
||||
around the network call so consumers can disable submit buttons via
|
||||
`isLoading || isSubmitting`.
|
||||
@@ -0,0 +1,63 @@
|
||||
# `auth-http.ts` — fetch + navigate seam helpers
|
||||
|
||||
## Purpose
|
||||
|
||||
`auth-http.ts` houses the three helpers every `AuthProvider` verb shares:
|
||||
`resolveFetch`, `resolveNavigate`, and `postJson`. Each helper exists so the
|
||||
`AuthProvider` body stays under the 300-LOC budget while keeping the
|
||||
network-translation policy in a single, testable file. Failures from the
|
||||
configured `fetch` are translated to `REACT_AUTH_WL.AUTH_NETWORK_FAILED`;
|
||||
non-ok responses are translated to `REACT_AUTH_WL.AUTH_REJECTED` with the
|
||||
backend-supplied `message` field threaded through the i18n message key when
|
||||
present.
|
||||
|
||||
## Features
|
||||
|
||||
- `resolveFetch(config)` — pick the injected `fetch` over `globalThis.fetch`.
|
||||
- `resolveNavigate(config)` — pick the injected `navigate(url)` over the
|
||||
default `window.location.href` writer.
|
||||
- `postJson(fetchImpl, url, body, rejectMessage, options?)` — single helper
|
||||
for every JSON POST, with error translation, optional credentials, and
|
||||
best-effort `.json()` recovery on failure responses.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
const fetchImpl = resolveFetch(config);
|
||||
const navigate = resolveNavigate(config);
|
||||
const data = await postJson(fetchImpl, url, body, "ui.label.fallback_msg");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
try {
|
||||
await postJson(fetchImpl, `${config.authUrl}/login`, { email, password },
|
||||
"ui.login.rejected", { credentials: "include" });
|
||||
} catch (err) {
|
||||
if (err.code === REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED) {
|
||||
showOfflineBanner();
|
||||
} else if (err.code === REACT_AUTH_WL_CODES.AUTH_REJECTED) {
|
||||
focusErrorRegion();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `resolveFetch(config: AuthConfig): FetchLike` — never throws; falls back to
|
||||
`globalThis.fetch` when the config omits one.
|
||||
- `defaultNavigate(url: string): void` — no-op when `window` is undefined.
|
||||
- `resolveNavigate(config: AuthConfig): (url: string) => void` — picks the
|
||||
injected seam or the default.
|
||||
- `postJson(fetchImpl, url, body, rejectMessage, options?)` — returns the
|
||||
parsed JSON body. Errors are subclasses of `ReactAuthWhitelabelError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`postJson` swallows `.json()` parse errors on failure responses on purpose:
|
||||
some backends return an empty 401/403 body, in which case the `rejectMessage`
|
||||
fallback is the user-visible label. The `.json()` parse on success is also
|
||||
swallowed and returns `{}` so a 204-style "no content" response keeps a
|
||||
clean caller signature.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `<BrandingProvider>` and tenant branding hooks
|
||||
|
||||
## Purpose
|
||||
|
||||
`branding-context.tsx` ships the `<BrandingProvider>` React context provider
|
||||
together with the hooks `useBranding`, `useTenantBranding`, and `useLogo`.
|
||||
The provider's job is to resolve a `TenantBranding` payload (either static
|
||||
via prop or fetched from a configurable endpoint) and to project that
|
||||
payload onto two surfaces: (1) the `@suaveplan/react-ui` runtime
|
||||
theme-token layer for every `--brand-*` CSS custom property, and (2) the
|
||||
non-CSS DOM chrome — `document.title`, `<link rel="icon">`, and the tenant
|
||||
custom-CSS `<style>` injection. The CSS path goes through
|
||||
`useThemeMutator().setExtensionTokens(...)` so per-tenant tokens stack on
|
||||
top of the active chassis theme exactly like any other mutator layer, with
|
||||
automatic cleanup on unmount.
|
||||
|
||||
## Features
|
||||
|
||||
- Static branding via prop OR endpoint fetch via `brandingEndpoint` (+
|
||||
optional `/tenantId` segment).
|
||||
- Injectable `fetch: FetchLike` for testability.
|
||||
- Zod validation (`TenantBrandingSchema.safeParse`) on every fetched
|
||||
payload — Zod failures surface as `BRANDING_INVALID_PAYLOAD`.
|
||||
- Network failures (transport throw or non-ok response) surface as
|
||||
`BRANDING_FETCH_FAILED`.
|
||||
- `--brand-primary`, `--brand-secondary`, `--brand-accent`, `--brand-font`
|
||||
written via `setExtensionTokens` — cleanup is automatic on unmount.
|
||||
- `document.title`, favicon `<link>`, and tenant `<style>` injection are
|
||||
direct DOM writes (not CSS variables; no conflict with the mutator).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```tsx
|
||||
import { ThemeProvider, lightTheme } from "@suaveplan/react-ui/theme";
|
||||
import { BrandingProvider } from "@suaveplan/react-auth-whitelabel";
|
||||
|
||||
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
|
||||
<BrandingProvider staticBranding={tenantBranding}>
|
||||
<App />
|
||||
</BrandingProvider>
|
||||
</ThemeProvider>
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<BrandingProvider
|
||||
brandingEndpoint="/api/branding"
|
||||
tenantId={tenantSlug}
|
||||
fetch={customFetch}
|
||||
>
|
||||
<App />
|
||||
</BrandingProvider>
|
||||
```
|
||||
|
||||
```tsx
|
||||
const branding = useTenantBranding();
|
||||
const lightLogo = useLogo(false);
|
||||
const darkLogo = useLogo(true);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `BrandingProvider(props)` — props include `brandingEndpoint?`,
|
||||
`tenantId?`, `staticBranding?`, `fetch?`, `children`.
|
||||
- `useBranding()` — returns `{ branding, isLoading, error, refresh }`.
|
||||
- `useTenantBranding()` — returns the current `TenantBranding`.
|
||||
- `useLogo(preferDark?)` — returns the most-appropriate logo URL or
|
||||
`undefined`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The mutator effect runs on every `branding` change AND on every
|
||||
`mutator` identity change. The mutator handle is stable across re-renders
|
||||
(it's `useMemo`-wrapped inside `@suaveplan/react-ui`), so the effect only
|
||||
re-fires when the branding payload actually changes. The cleanup
|
||||
contract for `--brand-*` is delegated to `useThemeMutator` — when the
|
||||
provider unmounts, the runtime invokes the mutator's `clear()` automatically,
|
||||
removing every `--brand-*` declaration from `document.documentElement.style`.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Error codes — `REACT_AUTH_WL`
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes.ts` registers the canonical, stable error-code registry used by
|
||||
every error thrown from `@suaveplan/react-auth-whitelabel`. The codes are
|
||||
namespaced under `REACT_AUTH_WL` and registered at module load via
|
||||
`defineCodes`, which means the `@suaveplan/error` registry is wired before any
|
||||
consumer imports a hook, context, or component. The codes survive minification,
|
||||
remain stable across releases, and let consumers branch on `.code` instead of
|
||||
inspecting the constructor name or message text — both of which are unstable
|
||||
across bundles.
|
||||
|
||||
## Features
|
||||
|
||||
- Single source of truth for error-code identity in this package.
|
||||
- `as const` literal-string codes for type-narrowing.
|
||||
- Module-load-time registration so the wire-format adapter, redaction layer,
|
||||
OTel span-event bridge, and i18n resolver all see the codes without an
|
||||
explicit init call.
|
||||
- Each code is thrown by ≥1 production code path — the
|
||||
`error-codes.test.ts` grep-test enforces the contract.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { REACT_AUTH_WL_CODES } from "@suaveplan/react-auth-whitelabel/errors";
|
||||
|
||||
if (err.code === REACT_AUTH_WL_CODES.BRANDING_FETCH_FAILED) {
|
||||
showFallbackBrand();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
REACT_AUTH_WL_CODES,
|
||||
type ReactAuthWhitelabelCode,
|
||||
} from "@suaveplan/react-auth-whitelabel/errors";
|
||||
|
||||
function isRecoverable(code: ReactAuthWhitelabelCode): boolean {
|
||||
switch (code) {
|
||||
case REACT_AUTH_WL_CODES.BRANDING_FETCH_FAILED:
|
||||
case REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `REACT_AUTH_WL_CODES` — frozen record of literal string codes; each value is
|
||||
prefixed with `"REACT_AUTH_WL."`.
|
||||
- `ReactAuthWhitelabelCode` — union of every `REACT_AUTH_WL_CODES[K]` value.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes("REACT_AUTH_WL", [...])` enforces uniqueness inside the namespace
|
||||
and stamps the registry with the call site for diagnostics. We intentionally
|
||||
do NOT eagerly import the registry from package code other than the error
|
||||
class — the registry side-effect runs once at module evaluation and that is
|
||||
the only required wiring.
|
||||
@@ -0,0 +1,68 @@
|
||||
# `ReactAuthWhitelabelError`
|
||||
|
||||
## Purpose
|
||||
|
||||
`ReactAuthWhitelabelError` is the canonical error class for every error thrown
|
||||
by `@suaveplan/react-auth-whitelabel`. It extends `SuaveplanError` so the
|
||||
error participates in the project-wide error substrate (cause-chain traversal,
|
||||
redaction-aware wire format, OTel span-event adapter, i18n message resolver).
|
||||
Every instance is stamped with `context.tier === "react-auth-whitelabel"` so
|
||||
log sinks and dashboards can group white-label auth failures without
|
||||
inspecting `error.constructor.name`, which is unstable across minified
|
||||
bundles. Construction always carries a `messageKey` so the i18n resolver can
|
||||
localise the user-visible error string at the boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Type-narrowed `.code` of `ReactAuthWhitelabelCode`.
|
||||
- Auto-injected `context.tier` for log/dashboard grouping.
|
||||
- Carries a translation key, not a baked English string.
|
||||
- Compatible with `SuaveplanError`'s cause-chain walker, wire serialiser,
|
||||
and OTel span-event adapter.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ReactAuthWhitelabelError } from "@suaveplan/react-auth-whitelabel/errors";
|
||||
import { REACT_AUTH_WL_CODES } from "@suaveplan/react-auth-whitelabel/errors";
|
||||
|
||||
throw new ReactAuthWhitelabelError(
|
||||
REACT_AUTH_WL_CODES.PROVIDER_OUTSIDE_TREE,
|
||||
"react_auth_wl.error.provider_outside_tree",
|
||||
{ context: { hook: "useAuth" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
try {
|
||||
await login(email, password);
|
||||
} catch (cause) {
|
||||
throw new ReactAuthWhitelabelError(
|
||||
REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED,
|
||||
"react_auth_wl.error.network",
|
||||
{ cause, context: { authUrl: config.authUrl } }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The thrown error retains the original `cause` chain so monitoring sinks can
|
||||
correlate transport errors with the user-facing failure.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new ReactAuthWhitelabelError(code, messageKey, options?)` — constructs an
|
||||
error. `options.context.tier` is always set to `"react-auth-whitelabel"`,
|
||||
even if the caller passes a different value (the helper merges, then
|
||||
overrides `tier`).
|
||||
- `.code` — `ReactAuthWhitelabelCode` literal.
|
||||
- `.message` — short English description sourced from `messageKey` (the i18n
|
||||
resolver substitutes a localised translation when available).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor merges caller-supplied `context` with the canonical `tier`
|
||||
stamp. Callers must NOT pass a different `tier` — it will silently win at
|
||||
merge time, but the contract is that this package's errors are always
|
||||
tagged `"react-auth-whitelabel"`.
|
||||
@@ -0,0 +1,59 @@
|
||||
# `TenantBrandingSchema`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`TenantBrandingSchema` is the Zod schema that validates every `TenantBranding`
|
||||
payload entering the package from an untrusted boundary — specifically the
|
||||
JSON returned by the branding endpoint configured on `<BrandingProvider>`.
|
||||
It mirrors `TenantBranding` exactly: every field's optionality and type
|
||||
match the TypeScript definition. The schema is the boundary contract the
|
||||
`BrandingProvider` enforces before storing a fetched payload, so an invalid
|
||||
or partial JSON response cannot poison the running app's theme tokens or
|
||||
document chrome.
|
||||
|
||||
## Features
|
||||
|
||||
- Field-by-field parity with `TenantBranding`.
|
||||
- `satisfies z.ZodType<TenantBranding>` keeps schema and type in lockstep —
|
||||
a missing field on either side fails the compile.
|
||||
- Imported with `import z from "zod"` per the Zod import rule.
|
||||
- Required string fields use `.min(1)` so a JSON `""` value is rejected.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { TenantBrandingSchema } from "@suaveplan/react-auth-whitelabel";
|
||||
|
||||
const parsed = TenantBrandingSchema.parse(await response.json());
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const result = TenantBrandingSchema.safeParse(payload);
|
||||
if (!result.success) {
|
||||
logger.warn("invalid branding payload", { issues: result.error.issues });
|
||||
return;
|
||||
}
|
||||
applyBranding(result.data);
|
||||
```
|
||||
|
||||
The `BrandingProvider` uses `safeParse` so a malformed payload is converted
|
||||
into a `ReactAuthWhitelabelError(REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD)` and
|
||||
surfaced on the context's `error` field instead of crashing the React tree.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `TenantBrandingSchema` — a `z.ZodObject` whose inferred type is exactly
|
||||
`TenantBranding`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema lives in its own file so the type-only consumer of `TenantBranding`
|
||||
can keep the runtime Zod dependency tree-shaken when validation is not used.
|
||||
Static branding passed via `staticBranding` is trusted and never parsed — the
|
||||
consumer authored it. Network payloads are always parsed.
|
||||
@@ -0,0 +1,53 @@
|
||||
# `navigateOrCall`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`navigateOrCall(target)` is the canonical helper used by every form
|
||||
component in this package to act on a "URL or click handler" prop. The
|
||||
forms accept `string | (() => void) | undefined` for affordances like
|
||||
`onForgotPassword`, `onRegister`, `onLogin`, etc. This helper centralises
|
||||
the three-branch routing so each form does not carry an inline copy of
|
||||
the same logic. Treating string values as URLs and function values as
|
||||
imperative handlers lets consumers wire either a static href or an
|
||||
in-app router callback without changing the component prop signature.
|
||||
|
||||
## Features
|
||||
|
||||
- String target → `window.location.href = target` (no-op outside a
|
||||
browser).
|
||||
- Function target → invoke directly.
|
||||
- `undefined` → no-op.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
navigateOrCall("/login");
|
||||
navigateOrCall(() => router.push("/login"));
|
||||
navigateOrCall(undefined);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```tsx
|
||||
<button onClick={() => navigateOrCall(props.onLogin)}>
|
||||
Sign in
|
||||
</button>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `navigateOrCall(target: string | (() => void) | undefined): void` —
|
||||
never throws; safely handles non-browser environments by detecting
|
||||
`typeof window`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The string branch checks `typeof window !== "undefined"` so a server-side
|
||||
or test environment that lacks `window` does not crash. The helper does
|
||||
not return the target — callers cannot observe whether the navigation
|
||||
ran; this matches the imperative redirect semantics expected at every
|
||||
call site.
|
||||
Reference in New Issue
Block a user