Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-errors-reverify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/mosaic": patch
---

Preserve all errors during UserProfile reverification. Show errors for the active password or code beside that field, and unrelated errors in a banner without marking the field invalid.
62 changes: 31 additions & 31 deletions packages/mosaic/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,37 @@ A flow lives in `src/features/<feature>/`. The **model** (`*.model.ts(x)`) is th

Most problems a feature hits already have a shared answer. Find the row for your problem and use it. Do not write a local version, even a small one.

| Problem | Use |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| An async action from a button or row: pending, an error, one lock | `usePendingAction` (`src/hooks/`) |
| A form: values, validation, submit, field errors, success UI | `useForm` (`src/components/form/`), with `validateAsync` for server checks and `onSuccess` for mounted success UI |
| A `<form>` element, without the browser's validation popup | `Form` (`src/primitives/form/`) |
| A model save whose Clerk errors should land on form fields | `save` (`src/utils/errors.ts`) in the model, or reject with `FormSubmitError` |
| A confirm step before an action | `useConfirmController` (`src/blocks/confirm/`) with `Confirmation` (`src/blocks/confirmation/`) |
| A delete that needs the user to type a phrase | `useConfirmController` (`src/blocks/confirm/`) with `Destructive` (`src/blocks/destructive/`) |
| An action that may need step-up verification | `useReverificationFlow` (`src/features/reverification/`) in the model, passed to `useConfirmController` |
| A direct action that needs step-up verification | `useReverificationFlow` in the model, with `ReverificationDialog` (`src/blocks/reverification/`) in the view |
| Showing any error to the user | One of the owners above. They call `toLocalizableError` and resolve copy with `useErrorText` |
| Moving focus after a list row is removed | `useListRemovalFocus` (`src/hooks/`) |
| A debounced, abortable async check as the user types | `useDebouncedAsync` (`src/hooks/`) |
| A spinner that should not flash or flicker | `useSpinDelay` (`src/hooks/`) |
| A placeholder for content that is still loading | `Skeleton` (`src/components/skeleton/`), see [Skeletons](docs/skeletons.md) |
| A date shown to the user | `formatDate` (`src/utils/format-date.ts`) |
| A value turned into kebab case, e.g. for a `data-*` attribute | `kebabCase` (`src/utils/kebab-case.ts`) |
| Current time for display labels | `useNow` (`src/hooks/`) |
| Whether enterprise SSO permits adding another identification | `allowsIdentificationCreation` (`src/utils/allows-identification-creation.ts`) |
| An inline message row; height is undefined until measured | `useHeldMessage`, `useMessageHeight`, `FeedbackBody` (`src/utils/feedback.tsx`) + `src/styles/feedback.styles.ts` |
| Data read from a Clerk resource that should be cached across mounts | A query hook in `src/queries/`, called from the model (`src/queries/use-user-sessions-query.ts`) |
| An item changed in place in an infinite list, kept until the next reload | `replaceInPages` (`src/utils/infinite-pages.ts`), passed to the list's `setData` |
| An organization permission checked with `checkAuthorization` | `ORGANIZATION_PERMISSIONS` (`src/utils/permissions.ts`) |
| A verification code's length or resend cooldown | `CODE_LENGTH`, `CODE_RESEND_SECONDS` (`src/utils/verification.ts`) |
| An avatar or logo file picker and its rejection errors | `AVATAR_UPLOAD`, `AVATAR_REJECTION_ERRORS` (`src/utils/avatar-upload.ts`) |
| Siblings that slide when an item is added or removed | `useLayoutAnimation` (`src/primitives/hooks/`) |
| An input that reformats as the user types without moving the caret | `useFormattedInput` (`src/primitives/hooks/`) |
| A panel that animates open and closed to its measured size | `usePanelSize` (`src/primitives/hooks/`) |
| One screen replacing another on the same card or dialog | `Flow` (`src/components/flow/`), see [Views](docs/views.md) |
| An interaction with an async lifecycle or two values that move together | A machine in the controller file, via `setup()` (`src/machine/`) |
| Styles several components share | `src/styles/*.styles.ts` |
| Problem | Use |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| An async action from a button or row: pending, an error, one lock | `usePendingAction` (`src/hooks/`) |
| A form: values, validation, submit, field errors, success UI | `useForm` (`src/components/form/`), with `validateAsync` for server checks and `onSuccess` for mounted success UI |
| A `<form>` element, without the browser's validation popup | `Form` (`src/primitives/form/`) |
| A model save whose Clerk errors should land on form fields | `save` (`src/utils/errors.ts`) in the model, or reject with `FormSubmitError` |
| A confirm step before an action | `useConfirmController` (`src/blocks/confirm/`) with `Confirmation` (`src/blocks/confirmation/`) |
| A delete that needs the user to type a phrase | `useConfirmController` (`src/blocks/confirm/`) with `Destructive` (`src/blocks/destructive/`) |
| An action that may need step-up verification | `useReverificationFlow` (`src/features/reverification/`) in the model, passed to `useConfirmController` |
| A direct action that needs step-up verification | `useReverificationFlow` in the model, with `ReverificationDialog` (`src/blocks/reverification/`) in the view |
| Showing any error to the user | One of the owners above. They call `toLocalizableError` (or `toLocalizableErrors` for all errors) and resolve copy with `useErrorText` |
| Moving focus after a list row is removed | `useListRemovalFocus` (`src/hooks/`) |
| A debounced, abortable async check as the user types | `useDebouncedAsync` (`src/hooks/`) |
| A spinner that should not flash or flicker | `useSpinDelay` (`src/hooks/`) |
| A placeholder for content that is still loading | `Skeleton` (`src/components/skeleton/`), see [Skeletons](docs/skeletons.md) |
| A date shown to the user | `formatDate` (`src/utils/format-date.ts`) |
| A value turned into kebab case, e.g. for a `data-*` attribute | `kebabCase` (`src/utils/kebab-case.ts`) |
| Current time for display labels | `useNow` (`src/hooks/`) |
| Whether enterprise SSO permits adding another identification | `allowsIdentificationCreation` (`src/utils/allows-identification-creation.ts`) |
| An inline message row; height is undefined until measured | `useHeldMessage`, `useMessageHeight`, `FeedbackBody` (`src/utils/feedback.tsx`) + `src/styles/feedback.styles.ts` |
| Data read from a Clerk resource that should be cached across mounts | A query hook in `src/queries/`, called from the model (`src/queries/use-user-sessions-query.ts`) |
| An item changed in place in an infinite list, kept until the next reload | `replaceInPages` (`src/utils/infinite-pages.ts`), passed to the list's `setData` |
| An organization permission checked with `checkAuthorization` | `ORGANIZATION_PERMISSIONS` (`src/utils/permissions.ts`) |
| A verification code's length or resend cooldown | `CODE_LENGTH`, `CODE_RESEND_SECONDS` (`src/utils/verification.ts`) |
| An avatar or logo file picker and its rejection errors | `AVATAR_UPLOAD`, `AVATAR_REJECTION_ERRORS` (`src/utils/avatar-upload.ts`) |
| Siblings that slide when an item is added or removed | `useLayoutAnimation` (`src/primitives/hooks/`) |
| An input that reformats as the user types without moving the caret | `useFormattedInput` (`src/primitives/hooks/`) |
| A panel that animates open and closed to its measured size | `usePanelSize` (`src/primitives/hooks/`) |
| One screen replacing another on the same card or dialog | `Flow` (`src/components/flow/`), see [Views](docs/views.md) |
| An interaction with an async lifecycle or two values that move together | A machine in the controller file, via `setup()` (`src/machine/`) |
| Styles several components share | `src/styles/*.styles.ts` |

## Signs you are about to invent a pattern

Expand Down
5 changes: 5 additions & 0 deletions packages/mosaic/docs/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,11 @@ One path, so every error a user sees goes through the `errors.*` catalog:
`toLocalizableError(cause)` from `utils/errors.ts`. Use one of them instead of
catching in a feature. A controller with its own machine maps in the machine's
`onError` (`user-profile-rename-passkey.controller.ts`).
- **Machines that display all errors use `toLocalizableErrors(cause)`.** It
preserves each error and its parameter name. Reverification stores this array,
routes matching parameters to the active field and everything else to the
global banner, then localizes each message at render. Single-error owners keep
using `toLocalizableError`, including its global-first `SaveError` behavior.
- **Forms map through `save`.** A model wraps a form's Clerk call in
`save(run, fields, params)` from `utils/errors.ts`, which rethrows a Clerk
error as a `SaveError` with each error routed to the field it names. The form
Expand Down
Loading
Loading