cauthy-auth 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -187,13 +187,21 @@ const Form: AuthComponents['Form'] = ({ as, children, ...rest }) =>
187
187
  as === 'form' ? <Stack as="form" gap={3} {...rest}>{children}</Stack> : <Stack gap={3} {...rest}>{children}</Stack>
188
188
  ```
189
189
 
190
- `Button` is told what kind of button it is: `primary` sends the form; `secondary` is any other way (a provider, a passkey, a sign-in link, SSO, cancel, send again); `danger` removes, leaves, deletes, disconnects, signs out everywhere or turns the second step off. `loading` is true on the one button whose action is under way; every button is `disabled` meanwhile.
190
+ `Button` is told what kind of button it is: `primary` sends the form; `secondary` is any other way (a provider, a passkey, a sign-in link, SSO, cancel, send again); `danger` removes, leaves, deletes, disconnects, signs out everywhere or turns the second step off. `loading` is true on the one button whose action is under way; every button and field is `disabled` meanwhile, and the form carries `aria-busy`. After a sign-in, `loading` stays true until the page is left, so draw your design system's spinner whenever it is true and keep the label:
191
+
192
+ ```tsx
193
+ const Button = ({ variant, loading, children, ...rest }: AuthButtonProps) => (
194
+ <MyButton tone={variant} isLoading={loading} aria-busy={loading || undefined} {...rest}>{children}</MyButton>
195
+ )
196
+ ```
197
+
198
+ The forms stay busy from the press until the work is really over. A sign-in that leaves the form (the password, the second step, a passkey, a provider, SSO, a sign-up done, an invitation accepted) stays busy until the browser leaves the page; with `onSuccess`, return a Promise when the form stays on screen while your application moves on (`onSuccess={() => router.navigate({ to: '/' })}`), and the form waits for it, then is free again if still shown. A plain `onSuccess` keeps it busy until it unmounts. A refusal, a sign-in link sent or a sign-up that asks to confirm the address frees it; so does coming back from the browser's back-forward cache. Until the workspace says what it offers, `<SignIn />` shows its form busy with nothing to press, and `<SignUp />`, `<Account />` and `<OrganizationMembers />` say "Loading…" (`texts.loading`). The plain button draws a spinner before its label (`.cauthy-spinner` in `styles.css`, still under `prefers-reduced-motion`); with `unstyled`, it adds "…" after the label.
191
199
 
192
200
  | Part | Is given |
193
201
  | --- | --- |
194
202
  | `Form` | `as` (`form` or `div`), `onSubmit`, `noValidate`, `aria-busy`, `className`, `section`, `children`. Render a `<form>` for `as="form"`, for Enter to send it. `section` names each part of `<Account />` (`profile`, `passkeys`… as in `AccountSection`) and of `<OrganizationMembers />` (`members`, `add-member`, `organization`, `rename`), to draw each as a card |
195
203
  | `Button` | `type`, `variant` (`primary`, `secondary`, `danger`), `loading`, `disabled`, `className`, `onClick`, `children` |
196
- | `Input` | `id`, `name`, `type`, `value`, `autoComplete`, `required`, `className`, `aria-invalid`, `aria-describedby`, `onChange(e)` |
204
+ | `Input` | `id`, `name`, `type`, `value`, `autoComplete`, `required`, `disabled`, `className`, `aria-invalid`, `aria-describedby`, `onChange(e)` |
197
205
  | `Checkbox` | `id`, `name`, `checked`, `required`, `disabled`, `className`, `onChange(checked)`, `children` (its label) |
198
206
  | `Select` | `id`, `name`, `value`, `options: { value, label }[]`, `disabled`, `className`, `aria-label`, `onChange(value)` |
199
207
  | `Field` | `id`, `label`, `aside`, `help`, `error`, `required`, `className`, `children` (the control). Give the help the id `${id}-hint` and the error `${id}-error` |
@@ -212,7 +220,7 @@ What Cauthy mails from these forms (a sign-in link, a new password, a confirmati
212
220
  <CauthyProvider locale={() => i18n.language}>
213
221
  ```
214
222
 
215
- `texts` changes any sentence. Its keys are stable: `email`, `password`, `name`, `code`, `signIn`, `signUp`, `continueWith`, `orWithEmail`, `forgotPassword`, `noAccount`, `createAccount`, `company`, `iAccept`, `and`, `termsOfService`, `privacyPolicy`, `haveAccount`, `sendLink`, `linkSent`, `codeHelp`, `codeHelpPhone`, `codeHelpEmail`, `sendEmailAgain`, `sendCodeSms`, `sendCodeWhatsapp`, `codeSent`, `pushSend`, `pushPick`, `pushWaiting`, `phone`, `phoneHelp`, `phoneOn`, `phoneOff`, `addPhone`, `removePhone`, `bySms`, `byWhatsapp`, `channel`, `verify`, `checkEmail`, `backToSignIn`, `emailMeALink`, `usePasskey`, `codeFromMail`, `signInLinkSent`, `newPassword`, `savePassword`, `passwordChanged`, `confirming`, `emailConfirmed`, `invitedTo`, `invitedToOrganization`, `invitedAs`, `choosePassword`, `acceptInvite`, `openingInvitation`, `invitationInvalid`, `profile`, `save`, `saved`, `currentPassword`, `changePassword`, `passwordChangedHere`, `secondStep`, `secondStepOn`, `secondStepOff`, `turnOn`, `turnOff`, `addToApp`, `openInApp`, `recoveryCodes`, `newRecoveryCodes`, `keepCodes`, `passkeys`, `managePasskeys`, `passkeysHelp`, `noPasskeys`, `passkeyName`, `addPasskey` (unused), `addPasskeyDevice`, `addSecurityKey`, `passkeyKinds`, `removePasskey`, `passkeyAdded`, `passkeyUnnamed`, `sessions`, `signOutEverywhere`, `agents`, `agentsHelp`, `disconnect`, `anAgent`, `unknownDevice`, `emailAddress`, `newEmail`, `changeEmail`, `emailSent`, `linkedAccounts`, `noLinkedAccounts`, `linkWith`, `unlink`, `linked`, `ssoOnly`, `members`, `membersHidden`, `organizationMissing`, `memberYou`, `memberInvited`, `memberManager`, `memberRole`, `memberAdd`, `memberAdded`, `memberRemove`, `memberRemoveConfirm`, `memberLeave`, `memberLeaveConfirm`, `memberInviteAgain`, `memberInvitationSent`, `organization`, `organizationRename`, `organizationRenamed`, `organizationArchive`, `organizationArchiveConfirm`, `organizationArchived`, `organizationDelete`, `organizationDeleteConfirm`, `organizationDeleted`, `yes`, `cancel`, `termsChanged`, `termsChangedHelp`, `acceptTerms`, and `errors` by error code. Built in: English and Spanish (`locale`). The dashboard can start the look from your stylesheet (shadcn/ui and Tailwind, Stackmyth, or variables of your own); `npx cauthy-auth theme --css src/app/globals.css` reads it the same way from a terminal.
223
+ `texts` changes any sentence. Its keys are stable: `email`, `password`, `name`, `code`, `signIn`, `signUp`, `continueWith`, `orWithEmail`, `forgotPassword`, `noAccount`, `createAccount`, `company`, `iAccept`, `and`, `termsOfService`, `privacyPolicy`, `haveAccount`, `sendLink`, `linkSent`, `codeHelp`, `codeHelpPhone`, `codeHelpEmail`, `sendEmailAgain`, `sendCodeSms`, `sendCodeWhatsapp`, `codeSent`, `pushSend`, `pushPick`, `pushWaiting`, `phone`, `phoneHelp`, `phoneOn`, `phoneOff`, `addPhone`, `removePhone`, `bySms`, `byWhatsapp`, `channel`, `verify`, `checkEmail`, `backToSignIn`, `emailMeALink`, `usePasskey`, `codeFromMail`, `signInLinkSent`, `newPassword`, `savePassword`, `passwordChanged`, `confirming`, `loading`, `emailConfirmed`, `invitedTo`, `invitedToOrganization`, `invitedAs`, `choosePassword`, `acceptInvite`, `openingInvitation`, `invitationInvalid`, `profile`, `save`, `saved`, `currentPassword`, `changePassword`, `passwordChangedHere`, `secondStep`, `secondStepOn`, `secondStepOff`, `turnOn`, `turnOff`, `addToApp`, `openInApp`, `recoveryCodes`, `newRecoveryCodes`, `keepCodes`, `passkeys`, `managePasskeys`, `passkeysHelp`, `noPasskeys`, `passkeyName`, `addPasskey` (unused), `addPasskeyDevice`, `addSecurityKey`, `passkeyKinds`, `removePasskey`, `passkeyAdded`, `passkeyUnnamed`, `sessions`, `signOutEverywhere`, `agents`, `agentsHelp`, `disconnect`, `anAgent`, `unknownDevice`, `emailAddress`, `newEmail`, `changeEmail`, `emailSent`, `linkedAccounts`, `noLinkedAccounts`, `linkWith`, `unlink`, `linked`, `ssoOnly`, `members`, `membersHidden`, `organizationMissing`, `memberYou`, `memberInvited`, `memberManager`, `memberRole`, `memberAdd`, `memberAdded`, `memberRemove`, `memberRemoveConfirm`, `memberLeave`, `memberLeaveConfirm`, `memberInviteAgain`, `memberInvitationSent`, `organization`, `organizationRename`, `organizationRenamed`, `organizationArchive`, `organizationArchiveConfirm`, `organizationArchived`, `organizationDelete`, `organizationDeleteConfirm`, `organizationDeleted`, `yes`, `cancel`, `termsChanged`, `termsChangedHelp`, `acceptTerms`, and `errors` by error code. Built in: English and Spanish (`locale`). The dashboard can start the look from your stylesheet (shadcn/ui and Tailwind, Stackmyth, or variables of your own); `npx cauthy-auth theme --css src/app/globals.css` reads it the same way from a terminal.
216
224
 
217
225
  Connect your application to Cauthy in one step, from Cauthy's dashboard ("Connect a System") or from a terminal. In the application's folder, `init` reads its framework (Next.js, TanStack Start, React Router or Remix, SvelteKit, Vite + React, Express, Hono, Fastify), makes the client, writes every variable to its env file, `CAUTHY_SECRET` included, and lays down the route and a sign-in page, never over a file that is there:
218
226
 
package/dist/react.d.mts CHANGED
@@ -1197,6 +1197,8 @@ interface AuthTexts {
1197
1197
  savePassword: string;
1198
1198
  passwordChanged: string;
1199
1199
  confirming: string;
1200
+ /** While a form waits on what the workspace offers, or on the account it shows. */
1201
+ loading: string;
1200
1202
  emailConfirmed: string;
1201
1203
  invitedTo: string;
1202
1204
  invitedToOrganization: string;
@@ -1283,7 +1285,7 @@ interface AuthTexts {
1283
1285
  /** By error code; one missing shows what the server said. */
1284
1286
  errors: Record<string, string>;
1285
1287
  }
1286
- type Slot = 'form' | 'field' | 'label' | 'input' | 'button' | 'providerButton' | 'providerIcon' | 'link' | 'error' | 'fieldError' | 'hint' | 'notice' | 'separator' | 'footer' | 'check' | 'account' | 'section' | 'heading' | 'secret' | 'codes' | 'sessions' | 'session';
1288
+ type Slot = 'form' | 'field' | 'label' | 'input' | 'button' | 'providerButton' | 'providerIcon' | 'link' | 'error' | 'fieldError' | 'hint' | 'notice' | 'pending' | 'separator' | 'footer' | 'check' | 'account' | 'section' | 'heading' | 'secret' | 'codes' | 'sessions' | 'session';
1287
1289
  interface AuthFormProps {
1288
1290
  /** The language of the built-in sentences. Default: `en`. */
1289
1291
  locale?: 'en' | 'es';
@@ -1291,8 +1293,14 @@ interface AuthFormProps {
1291
1293
  texts?: Partial<Omit<AuthTexts, 'errors'>> & {
1292
1294
  errors?: Record<string, string>;
1293
1295
  };
1294
- /** Called once the person is signed in. Default: go to `returnTo`. */
1295
- onSuccess?: () => void;
1296
+ /**
1297
+ * Called once the person is signed in, instead of going to `returnTo`. The
1298
+ * form stays busy (its button loading, every control disabled) until it
1299
+ * leaves the page. When the application keeps the form on screen while it
1300
+ * moves on (a router's navigation, loading the session), return a Promise:
1301
+ * the form waits for it, then is free again if it is still shown.
1302
+ */
1303
+ onSuccess?: () => void | Promise<unknown>;
1296
1304
  /** Where to go after signing in, and where a provider returns to. Default: `/`. */
1297
1305
  returnTo?: string;
1298
1306
  /**
@@ -1335,7 +1343,9 @@ interface AuthFormProps {
1335
1343
  * other action (a provider, a passkey, a sign-in link, cancel, send again);
1336
1344
  * `danger` removes, leaves, deletes, disconnects or turns something off.
1337
1345
  * `loading` is true on the one button whose action is under way; every
1338
- * button is `disabled` meanwhile, as before.
1346
+ * button is `disabled` meanwhile. Draw a spinner (or the like) on the button
1347
+ * while `loading` is true, and keep its label: after a sign-in it stays true
1348
+ * until the page is left, so the person sees the work go on.
1339
1349
  */
1340
1350
  interface AuthButtonProps {
1341
1351
  type: 'submit' | 'button';
@@ -1376,6 +1386,7 @@ interface AuthInputProps {
1376
1386
  value: string;
1377
1387
  autoComplete?: string;
1378
1388
  required?: boolean;
1389
+ disabled?: boolean;
1379
1390
  className?: string;
1380
1391
  'aria-invalid'?: boolean;
1381
1392
  'aria-describedby'?: string;
@@ -1533,8 +1544,11 @@ interface AcceptInviteProps extends AuthFormProps {
1533
1544
  }
1534
1545
  declare function AcceptInvite(props: AcceptInviteProps): react.JSX.Element;
1535
1546
  interface PendingTermsProps extends Omit<AuthFormProps, 'links' | 'returnTo' | 'createAccounts'> {
1536
- /** Called once the person accepted. Nothing else happens: the application decides what follows. */
1537
- onSuccess?: () => void;
1547
+ /**
1548
+ * Called once the person accepted. Nothing else happens: the application
1549
+ * decides what follows. A Promise keeps the button loading until it settles.
1550
+ */
1551
+ onSuccess?: () => void | Promise<unknown>;
1538
1552
  }
1539
1553
  /**
1540
1554
  * The legal documents the signed-in person still owes (a new version of the
@@ -1587,8 +1601,8 @@ type AccountEvent = {
1587
1601
  kind: 'sessions.ended';
1588
1602
  };
1589
1603
  interface AccountProps extends Omit<AuthFormProps, 'links' | 'returnTo' | 'onSuccess'> {
1590
- /** Called after "sign out everywhere". Default: go to `/`. */
1591
- onSignedOut?: () => void;
1604
+ /** Called after "sign out everywhere". Default: go to `/`. A Promise keeps the button loading until it settles. */
1605
+ onSignedOut?: () => void | Promise<unknown>;
1592
1606
  /** The moment of a session, as the application writes dates. Default: the browser's own way, in the reader's time zone. */
1593
1607
  formatDate?: (iso: string) => string;
1594
1608
  /**
package/dist/react.d.ts CHANGED
@@ -1197,6 +1197,8 @@ interface AuthTexts {
1197
1197
  savePassword: string;
1198
1198
  passwordChanged: string;
1199
1199
  confirming: string;
1200
+ /** While a form waits on what the workspace offers, or on the account it shows. */
1201
+ loading: string;
1200
1202
  emailConfirmed: string;
1201
1203
  invitedTo: string;
1202
1204
  invitedToOrganization: string;
@@ -1283,7 +1285,7 @@ interface AuthTexts {
1283
1285
  /** By error code; one missing shows what the server said. */
1284
1286
  errors: Record<string, string>;
1285
1287
  }
1286
- type Slot = 'form' | 'field' | 'label' | 'input' | 'button' | 'providerButton' | 'providerIcon' | 'link' | 'error' | 'fieldError' | 'hint' | 'notice' | 'separator' | 'footer' | 'check' | 'account' | 'section' | 'heading' | 'secret' | 'codes' | 'sessions' | 'session';
1288
+ type Slot = 'form' | 'field' | 'label' | 'input' | 'button' | 'providerButton' | 'providerIcon' | 'link' | 'error' | 'fieldError' | 'hint' | 'notice' | 'pending' | 'separator' | 'footer' | 'check' | 'account' | 'section' | 'heading' | 'secret' | 'codes' | 'sessions' | 'session';
1287
1289
  interface AuthFormProps {
1288
1290
  /** The language of the built-in sentences. Default: `en`. */
1289
1291
  locale?: 'en' | 'es';
@@ -1291,8 +1293,14 @@ interface AuthFormProps {
1291
1293
  texts?: Partial<Omit<AuthTexts, 'errors'>> & {
1292
1294
  errors?: Record<string, string>;
1293
1295
  };
1294
- /** Called once the person is signed in. Default: go to `returnTo`. */
1295
- onSuccess?: () => void;
1296
+ /**
1297
+ * Called once the person is signed in, instead of going to `returnTo`. The
1298
+ * form stays busy (its button loading, every control disabled) until it
1299
+ * leaves the page. When the application keeps the form on screen while it
1300
+ * moves on (a router's navigation, loading the session), return a Promise:
1301
+ * the form waits for it, then is free again if it is still shown.
1302
+ */
1303
+ onSuccess?: () => void | Promise<unknown>;
1296
1304
  /** Where to go after signing in, and where a provider returns to. Default: `/`. */
1297
1305
  returnTo?: string;
1298
1306
  /**
@@ -1335,7 +1343,9 @@ interface AuthFormProps {
1335
1343
  * other action (a provider, a passkey, a sign-in link, cancel, send again);
1336
1344
  * `danger` removes, leaves, deletes, disconnects or turns something off.
1337
1345
  * `loading` is true on the one button whose action is under way; every
1338
- * button is `disabled` meanwhile, as before.
1346
+ * button is `disabled` meanwhile. Draw a spinner (or the like) on the button
1347
+ * while `loading` is true, and keep its label: after a sign-in it stays true
1348
+ * until the page is left, so the person sees the work go on.
1339
1349
  */
1340
1350
  interface AuthButtonProps {
1341
1351
  type: 'submit' | 'button';
@@ -1376,6 +1386,7 @@ interface AuthInputProps {
1376
1386
  value: string;
1377
1387
  autoComplete?: string;
1378
1388
  required?: boolean;
1389
+ disabled?: boolean;
1379
1390
  className?: string;
1380
1391
  'aria-invalid'?: boolean;
1381
1392
  'aria-describedby'?: string;
@@ -1533,8 +1544,11 @@ interface AcceptInviteProps extends AuthFormProps {
1533
1544
  }
1534
1545
  declare function AcceptInvite(props: AcceptInviteProps): react.JSX.Element;
1535
1546
  interface PendingTermsProps extends Omit<AuthFormProps, 'links' | 'returnTo' | 'createAccounts'> {
1536
- /** Called once the person accepted. Nothing else happens: the application decides what follows. */
1537
- onSuccess?: () => void;
1547
+ /**
1548
+ * Called once the person accepted. Nothing else happens: the application
1549
+ * decides what follows. A Promise keeps the button loading until it settles.
1550
+ */
1551
+ onSuccess?: () => void | Promise<unknown>;
1538
1552
  }
1539
1553
  /**
1540
1554
  * The legal documents the signed-in person still owes (a new version of the
@@ -1587,8 +1601,8 @@ type AccountEvent = {
1587
1601
  kind: 'sessions.ended';
1588
1602
  };
1589
1603
  interface AccountProps extends Omit<AuthFormProps, 'links' | 'returnTo' | 'onSuccess'> {
1590
- /** Called after "sign out everywhere". Default: go to `/`. */
1591
- onSignedOut?: () => void;
1604
+ /** Called after "sign out everywhere". Default: go to `/`. A Promise keeps the button loading until it settles. */
1605
+ onSignedOut?: () => void | Promise<unknown>;
1592
1606
  /** The moment of a session, as the application writes dates. Default: the browser's own way, in the reader's time zone. */
1593
1607
  formatDate?: (iso: string) => string;
1594
1608
  /**
package/dist/react.js CHANGED
@@ -823,6 +823,7 @@ var TEXTS = {
823
823
  savePassword: "Save the password",
824
824
  passwordChanged: "Your password was changed. You can sign in with it now.",
825
825
  confirming: "Confirming your address\u2026",
826
+ loading: "Loading\u2026",
826
827
  emailConfirmed: "Your address is confirmed. You can sign in now.",
827
828
  invitedTo: "You've been invited to {workspace}",
828
829
  invitedToOrganization: "You've been invited to {organization} on {workspace}",
@@ -1004,6 +1005,7 @@ var TEXTS = {
1004
1005
  savePassword: "Guardar la contrase\xF1a",
1005
1006
  passwordChanged: "Tu contrase\xF1a se cambi\xF3. Ya puedes entrar con ella.",
1006
1007
  confirming: "Confirmando tu correo\u2026",
1008
+ loading: "Cargando\u2026",
1007
1009
  emailConfirmed: "Tu correo qued\xF3 confirmado. Ya puedes entrar.",
1008
1010
  invitedTo: "Te invitaron a {workspace}",
1009
1011
  invitedToOrganization: "Te invitaron a {organization} en {workspace}",
@@ -1136,7 +1138,14 @@ var TEXTS = {
1136
1138
  }
1137
1139
  }
1138
1140
  };
1139
- var PlainButton = ({ variant: _variant, loading: _loading, ...p }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ...p });
1141
+ var PlainButton = ({ variant: _variant, loading, children, ...p }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { ...p, "aria-busy": loading || void 0, children: [
1142
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cauthy-spinner", "aria-hidden": "true" }),
1143
+ children
1144
+ ] });
1145
+ var UnstyledButton = ({ variant: _variant, loading, children, ...p }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { ...p, "aria-busy": loading || void 0, children: [
1146
+ children,
1147
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2026" })
1148
+ ] });
1140
1149
  var PlainInput = (p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { ...p });
1141
1150
  var looks = /* @__PURE__ */ new Map();
1142
1151
  var loaded = /* @__PURE__ */ new Set();
@@ -1244,12 +1253,15 @@ function sameOrigin(to) {
1244
1253
  return void 0;
1245
1254
  }
1246
1255
  }
1247
- function useForm(props) {
1256
+ function isPending(v) {
1257
+ return typeof v?.then === "function";
1258
+ }
1259
+ function useForm(props, opts = {}) {
1248
1260
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
1249
1261
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
1250
1262
  const own = (slot) => props.classNames?.[slot];
1251
1263
  const C = props.components ?? {};
1252
- const Button = C.Button ?? PlainButton;
1264
+ const Button = C.Button ?? (props.unstyled ? UnstyledButton : PlainButton);
1253
1265
  const Input = C.Input ?? PlainInput;
1254
1266
  const [error, setOnlyError] = (0, import_react.useState)(null);
1255
1267
  const [problems, setProblems] = (0, import_react.useState)({ list: [], round: 0 });
@@ -1266,9 +1278,32 @@ function useForm(props) {
1266
1278
  const fieldError = (name) => problems.list.find((e) => e.field === name) ?? (error?.field === name ? error : null);
1267
1279
  const firstWrong = (name) => problems.list[0]?.field === name ? problems.round : 0;
1268
1280
  const [acting, setActing] = (0, import_react.useState)(null);
1269
- const busy = acting !== null;
1281
+ const busy = acting !== null || opts.waiting === true;
1270
1282
  const setBusy = (0, import_react.useCallback)((on) => setActing(on === false ? null : on === true ? SUBMIT : on), []);
1271
1283
  const loading = (key = SUBMIT) => acting === key;
1284
+ const shown = (0, import_react.useRef)(true);
1285
+ (0, import_react.useEffect)(() => {
1286
+ shown.current = true;
1287
+ const back = (e) => {
1288
+ if (e.persisted) setActing(null);
1289
+ };
1290
+ window.addEventListener("pageshow", back);
1291
+ return () => {
1292
+ shown.current = false;
1293
+ window.removeEventListener("pageshow", back);
1294
+ };
1295
+ }, []);
1296
+ const hold = (after) => {
1297
+ if (!isPending(after)) return;
1298
+ const free = () => {
1299
+ if (shown.current) setActing(null);
1300
+ };
1301
+ after.then(free, free);
1302
+ };
1303
+ const fail = (e) => {
1304
+ setActing(null);
1305
+ setError(e);
1306
+ };
1272
1307
  const [policy, setPolicy] = (0, import_react.useState)(void 0);
1273
1308
  const locale = props.locale ?? "en";
1274
1309
  const say = (e) => {
@@ -1277,15 +1312,18 @@ function useForm(props) {
1277
1312
  };
1278
1313
  const passwordRule = describePasswordPolicy(policy, locale);
1279
1314
  const done = (to) => {
1280
- if (props.onSuccess) props.onSuccess();
1315
+ setActing((a) => a ?? SUBMIT);
1316
+ if (props.onSuccess) hold(props.onSuccess());
1281
1317
  else window.location.assign(sitePath(to) ?? sameOrigin(props.returnTo) ?? "/");
1282
1318
  };
1283
- const root = useRoot(props);
1284
- return { t, cx, own, C, Button, Input, error, setError, check, fieldError, firstWrong, busy, setBusy, loading, say, done, root, policy, setPolicy, passwordRule };
1319
+ const root = useRoot(opts.part ? { ...props, unstyled: true } : props);
1320
+ const plain = props.unstyled === true;
1321
+ return { t, cx, own, C, Button, Input, error, setError, fail, check, fieldError, firstWrong, busy, setBusy, loading, hold, say, done, root, plain, policy, setPolicy, passwordRule };
1285
1322
  }
1286
1323
  var SUBMIT = "submit";
1287
1324
  function Wrap(p) {
1288
1325
  const { Form: OwnForm } = p.f.C;
1326
+ const busy = p.busy ?? (p.f.busy || void 0);
1289
1327
  if (OwnForm) {
1290
1328
  const own = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1291
1329
  OwnForm,
@@ -1293,7 +1331,7 @@ function Wrap(p) {
1293
1331
  as: p.as === "form" ? "form" : "div",
1294
1332
  onSubmit: p.onSubmit,
1295
1333
  noValidate: p.noValidate,
1296
- "aria-busy": p.busy,
1334
+ "aria-busy": busy,
1297
1335
  className: p.slot ? p.f.own(p.slot) : void 0,
1298
1336
  section: p.section,
1299
1337
  children: p.children
@@ -1302,7 +1340,16 @@ function Wrap(p) {
1302
1340
  return p.root?.["data-cauthy"] !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...p.root, children: own }) : own;
1303
1341
  }
1304
1342
  const Tag2 = p.as;
1305
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag2, { className: p.slot ? p.f.cx(p.slot) : void 0, ...p.root, onSubmit: p.onSubmit, noValidate: p.noValidate, "aria-busy": p.busy, children: p.children });
1343
+ const children = p.busy && p.children === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pending, { f: p.f }) : p.children;
1344
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag2, { className: p.slot ? p.f.cx(p.slot) : void 0, ...p.root, onSubmit: p.onSubmit, noValidate: p.noValidate, "aria-busy": busy, children });
1345
+ }
1346
+ function Pending({ f }) {
1347
+ if (f.plain) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", children: f.t.loading });
1348
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { role: "status", className: f.cx("pending"), children: [
1349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cauthy-spinner", "aria-hidden": "true" }),
1350
+ " ",
1351
+ f.t.loading
1352
+ ] });
1306
1353
  }
1307
1354
  function Note({ f, children, footer }) {
1308
1355
  const { Text } = f.C;
@@ -1343,10 +1390,10 @@ function Check(p) {
1343
1390
  const { f } = p;
1344
1391
  const { Checkbox } = f.C;
1345
1392
  if (Checkbox) {
1346
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox, { id, name: p.name, checked: p.checked, required: p.required, className: f.own("check"), onChange: p.onChange, children: p.children });
1393
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Checkbox, { id, name: p.name, checked: p.checked, required: p.required, disabled: f.busy, className: f.own("check"), onChange: p.onChange, children: p.children });
1347
1394
  }
1348
1395
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: f.cx("check"), children: [
1349
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: p.name, checked: p.checked, required: p.required, onChange: (e) => p.onChange(e.target.checked) }),
1396
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: p.name, checked: p.checked, required: p.required, disabled: f.busy, onChange: (e) => p.onChange(e.target.checked) }),
1350
1397
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: p.children })
1351
1398
  ] });
1352
1399
  }
@@ -1355,10 +1402,11 @@ function Pick(p) {
1355
1402
  const id = p.id ?? made;
1356
1403
  const { f } = p;
1357
1404
  const { Select } = f.C;
1405
+ const disabled = p.disabled || f.busy;
1358
1406
  if (Select) {
1359
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select, { id, name: p.name, value: p.value, options: p.options, disabled: p.disabled, "aria-label": p.label, className: f.own("input"), onChange: p.onChange });
1407
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Select, { id, name: p.name, value: p.value, options: p.options, disabled, "aria-label": p.label, className: f.own("input"), onChange: p.onChange });
1360
1408
  }
1361
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { id, name: p.name, "aria-label": p.label, className: f.cx("input"), value: p.value, disabled: p.disabled, onChange: (e) => p.onChange(e.target.value), children: p.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: o.value, children: o.label }, o.value)) });
1409
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { id, name: p.name, "aria-label": p.label, className: f.cx("input"), value: p.value, disabled, onChange: (e) => p.onChange(e.target.value), children: p.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: o.value, children: o.label }, o.value)) });
1362
1410
  }
1363
1411
  function Labelled({ f, id, label, children }) {
1364
1412
  const { Field: OwnField } = f.C;
@@ -1412,6 +1460,7 @@ function Field(p) {
1412
1460
  value: p.value,
1413
1461
  autoComplete: p.autoComplete,
1414
1462
  required: p.required,
1463
+ disabled: f.busy,
1415
1464
  className: f.cx("input"),
1416
1465
  "aria-invalid": bad ? true : void 0,
1417
1466
  "aria-describedby": described || void 0,
@@ -1501,6 +1550,7 @@ function SecondStepForm(p) {
1501
1550
  if (ctl.signal.aborted) return;
1502
1551
  setPushNumber(0);
1503
1552
  if (!done.ok) return f.setError(done.error);
1553
+ f.setBusy("push");
1504
1554
  f.done();
1505
1555
  };
1506
1556
  const sendCode = async () => {
@@ -1516,8 +1566,7 @@ function SecondStepForm(p) {
1516
1566
  f.setBusy(true);
1517
1567
  f.setError(null);
1518
1568
  const r = await auth.signIn.mfa({ code });
1519
- f.setBusy(false);
1520
- if (!r.ok) return f.setError(r.error);
1569
+ if (!r.ok) return f.fail(r.error);
1521
1570
  f.done();
1522
1571
  };
1523
1572
  const general = f.error && !f.error.field ? f.error : null;
@@ -1545,7 +1594,11 @@ function SecondStepForm(p) {
1545
1594
  }
1546
1595
  function ProviderButtons(p) {
1547
1596
  const { f } = p;
1548
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: p.providers.map((provider) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(f.Button, { type: "button", variant: "secondary", className: f.cx("providerButton"), onClick: () => p.onPick(provider.id), children: [
1597
+ const pick = (id) => {
1598
+ f.setBusy(`provider:${id}`);
1599
+ p.onPick(id);
1600
+ };
1601
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: p.providers.map((provider) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(f.Button, { type: "button", variant: "secondary", loading: f.loading(`provider:${provider.id}`), disabled: f.busy, className: f.cx("providerButton"), onClick: () => pick(provider.id), children: [
1549
1602
  provider.iconUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: provider.iconUrl, alt: "", width: 18, height: 18, className: f.cx("providerIcon") }),
1550
1603
  f.t.continueWith,
1551
1604
  " ",
@@ -1558,8 +1611,8 @@ function signInOffers(methods) {
1558
1611
  }
1559
1612
  function SignIn(props) {
1560
1613
  const auth = useAuth();
1561
- const f = useForm(props);
1562
1614
  const [methods, setMethods] = (0, import_react.useState)(null);
1615
+ const f = useForm(props, { waiting: methods === null });
1563
1616
  const [email, setEmail] = (0, import_react.useState)("");
1564
1617
  const [password, setPassword] = (0, import_react.useState)("");
1565
1618
  const [code, setCode] = (0, import_react.useState)("");
@@ -1631,14 +1684,16 @@ function SignIn(props) {
1631
1684
  f.setBusy(true);
1632
1685
  f.setError(null);
1633
1686
  const r = step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password, captchaToken: challenge.token });
1634
- f.setBusy(false);
1635
1687
  if (step === "password") challenge.renew();
1636
1688
  if (!r.ok && r.error.code === "sso_required") return toCompany();
1637
1689
  if (!r.ok) {
1638
1690
  setOffer((n) => n + 1);
1639
- return f.setError(r.error);
1691
+ return f.fail(r.error);
1692
+ }
1693
+ if (r.status === "mfa_required") {
1694
+ f.setBusy(false);
1695
+ return setStep("code");
1640
1696
  }
1641
- if (r.status === "mfa_required") return setStep("code");
1642
1697
  f.done("returnTo" in r && typeof r.returnTo === "string" ? r.returnTo : void 0);
1643
1698
  };
1644
1699
  const usePasskey = async () => {
@@ -1647,10 +1702,10 @@ function SignIn(props) {
1647
1702
  f.setError(null);
1648
1703
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale, email });
1649
1704
  if (r.ok && r.status === "redirected") return;
1705
+ if (r.ok) return f.done();
1650
1706
  f.setBusy(false);
1651
- if (!r.ok) setOffer((n) => n + 1);
1652
- if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
1653
- f.done();
1707
+ setOffer((n) => n + 1);
1708
+ if (r.error.code !== "cancelled") f.setError(r.error);
1654
1709
  };
1655
1710
  const sendLink = async () => {
1656
1711
  autofill.current?.abort();
@@ -1767,10 +1822,12 @@ function SignUp(props) {
1767
1822
  ...legal ? { acceptLegal, consents } : {},
1768
1823
  captchaToken: challenge.token
1769
1824
  });
1770
- f.setBusy(false);
1771
1825
  challenge.renew();
1772
- if (!r.ok) return f.setError(r.error);
1773
- if (r.status === "verification_required") return setSent(true);
1826
+ if (!r.ok) return f.fail(r.error);
1827
+ if (r.status === "verification_required") {
1828
+ f.setBusy(false);
1829
+ return setSent(true);
1830
+ }
1774
1831
  f.done();
1775
1832
  };
1776
1833
  const general = f.error && !f.error.field ? f.error : null;
@@ -1933,13 +1990,16 @@ function AcceptInvite(props) {
1933
1990
  f.setBusy(true);
1934
1991
  f.setError(null);
1935
1992
  const r = await auth.acceptInvitation(token, password);
1936
- f.setBusy(false);
1937
1993
  if (!r.ok) {
1994
+ f.setBusy(false);
1938
1995
  if (r.error.code === "invalid_token") return setState("invalid");
1939
1996
  return f.setError(r.error);
1940
1997
  }
1941
1998
  props.onSaved?.({ kind: "invitation.accepted", email: invitation?.email ?? "", ...invitation?.organization ? { organization: invitation.organization } : {} });
1942
- if (r.status === "mfa_required") return setState("code");
1999
+ if (r.status === "mfa_required") {
2000
+ f.setBusy(false);
2001
+ return setState("code");
2002
+ }
1943
2003
  f.done();
1944
2004
  };
1945
2005
  const general = f.error && !f.error.field ? f.error : null;
@@ -1977,10 +2037,11 @@ function PendingTerms(props) {
1977
2037
  f.setBusy(true);
1978
2038
  f.setError(null);
1979
2039
  const r = await auth.account.acceptLegal({ kinds: owed.map((d) => d.kind) });
2040
+ if (!r.ok) return f.fail(r.error);
2041
+ const after = props.onSuccess?.();
2042
+ if (isPending(after)) await Promise.resolve(after).catch(() => void 0);
1980
2043
  f.setBusy(false);
1981
- if (!r.ok) return f.setError(r.error);
1982
2044
  setOwed([]);
1983
- props.onSuccess?.();
1984
2045
  };
1985
2046
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Wrap, { f, as: "div", slot: "form", root: f.root, children: [
1986
2047
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Heading, { f, children: f.t.termsChanged }),
@@ -1995,15 +2056,15 @@ function PendingTerms(props) {
1995
2056
  }
1996
2057
  var ACCOUNT_SECTIONS = ["profile", "email", "password", "linked", "secondStep", "phone", "passkeys", "agents", "sessions"];
1997
2058
  function useSection(props) {
1998
- const f = useForm({ ...props, unstyled: true });
2059
+ const f = useForm(props, { part: true });
1999
2060
  const [done, setDone] = (0, import_react.useState)(null);
2000
- const run = async (action, key = SUBMIT) => {
2061
+ const run = async (action, key = SUBMIT, leaves = false) => {
2001
2062
  f.setBusy(key);
2002
2063
  f.setError(null);
2003
2064
  setDone(null);
2004
2065
  const r = await action();
2005
- f.setBusy(false);
2006
- if (!r.ok) f.setError(r.error);
2066
+ if (!r.ok) f.fail(r.error);
2067
+ else if (!leaves) f.setBusy(false);
2007
2068
  return r;
2008
2069
  };
2009
2070
  const general = f.error && !f.error.field ? f.error : null;
@@ -2416,11 +2477,26 @@ function LinkedPart({ props }) {
2416
2477
  " ",
2417
2478
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", variant: "danger", loading: f.loading(`unlink:${x.provider}`), disabled: f.busy, className: f.cx("providerButton"), onClick: () => void unlink(x.provider), children: f.t.unlink })
2418
2479
  ] }, x.provider)) }),
2419
- offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(f.Button, { type: "button", variant: "secondary", className: f.cx("providerButton"), onClick: () => auth.account.link(p.id, { returnTo: here }), children: [
2420
- f.t.linkWith,
2421
- " ",
2422
- p.name
2423
- ] }, p.id))
2480
+ offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2481
+ f.Button,
2482
+ {
2483
+ type: "button",
2484
+ variant: "secondary",
2485
+ loading: f.loading(`link:${p.id}`),
2486
+ disabled: f.busy,
2487
+ className: f.cx("providerButton"),
2488
+ onClick: () => {
2489
+ f.setBusy(`link:${p.id}`);
2490
+ auth.account.link(p.id, { returnTo: here });
2491
+ },
2492
+ children: [
2493
+ f.t.linkWith,
2494
+ " ",
2495
+ p.name
2496
+ ]
2497
+ },
2498
+ p.id
2499
+ ))
2424
2500
  ] });
2425
2501
  }
2426
2502
  function PasswordPart({ props }) {
@@ -2578,7 +2654,7 @@ function PhonePart({ props, account, onChanged }) {
2578
2654
  !ways.phone && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "phone", type: "tel", label: f.t.phone, value: number, autoComplete: "tel", required: true, onChange: setNumber }),
2579
2655
  !ways.phone && ways.phoneChannels.length > 1 && f.C.Select && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChannelPick, { f, value: channel, channels: ways.phoneChannels, onChange: setChannel }),
2580
2656
  !ways.phone && ways.phoneChannels.length > 1 && !f.C.Select && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: f.cx("field"), role: "radiogroup", children: ways.phoneChannels.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: f.cx("label"), children: [
2581
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "radio", name: "channel", value: c, checked: channel === c, onChange: () => setChannel(c) }),
2657
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "radio", name: "channel", value: c, checked: channel === c, disabled: f.busy, onChange: () => setChannel(c) }),
2582
2658
  " ",
2583
2659
  c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
2584
2660
  ] }, c)) }),
@@ -2625,10 +2701,10 @@ function SessionsPart({ props, when }) {
2625
2701
  void auth.account.sessions().then(setSessions);
2626
2702
  }, []);
2627
2703
  const end = async () => {
2628
- const r = await run(() => auth.account.signOutEverywhere());
2704
+ const r = await run(() => auth.account.signOutEverywhere(), SUBMIT, true);
2629
2705
  if (!r.ok) return;
2630
2706
  props.onSaved?.({ kind: "sessions.ended" });
2631
- if (props.onSignedOut) props.onSignedOut();
2707
+ if (props.onSignedOut) f.hold(props.onSignedOut());
2632
2708
  else window.location.assign("/");
2633
2709
  };
2634
2710
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Wrap, { f, as: "section", slot: "section", section: "sessions", children: [