cauthy-auth 0.34.0 → 0.35.1

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 />` show a spinner alone, centred, with `aria-busy` on their wrapper. Loaders are icons, never words, so they read the same in any language. The plain button draws a spinner before its label (`.cauthy-spinner` in `styles.css`, still under `prefers-reduced-motion`); with `unstyled`, an SVG icon that turns by itself (no stylesheet needed) and stands still under `prefers-reduced-motion`. The icon is `aria-hidden`; the button says `aria-busy`.
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
@@ -1283,7 +1283,7 @@ interface AuthTexts {
1283
1283
  /** By error code; one missing shows what the server said. */
1284
1284
  errors: Record<string, string>;
1285
1285
  }
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';
1286
+ 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
1287
  interface AuthFormProps {
1288
1288
  /** The language of the built-in sentences. Default: `en`. */
1289
1289
  locale?: 'en' | 'es';
@@ -1291,8 +1291,14 @@ interface AuthFormProps {
1291
1291
  texts?: Partial<Omit<AuthTexts, 'errors'>> & {
1292
1292
  errors?: Record<string, string>;
1293
1293
  };
1294
- /** Called once the person is signed in. Default: go to `returnTo`. */
1295
- onSuccess?: () => void;
1294
+ /**
1295
+ * Called once the person is signed in, instead of going to `returnTo`. The
1296
+ * form stays busy (its button loading, every control disabled) until it
1297
+ * leaves the page. When the application keeps the form on screen while it
1298
+ * moves on (a router's navigation, loading the session), return a Promise:
1299
+ * the form waits for it, then is free again if it is still shown.
1300
+ */
1301
+ onSuccess?: () => void | Promise<unknown>;
1296
1302
  /** Where to go after signing in, and where a provider returns to. Default: `/`. */
1297
1303
  returnTo?: string;
1298
1304
  /**
@@ -1335,7 +1341,9 @@ interface AuthFormProps {
1335
1341
  * other action (a provider, a passkey, a sign-in link, cancel, send again);
1336
1342
  * `danger` removes, leaves, deletes, disconnects or turns something off.
1337
1343
  * `loading` is true on the one button whose action is under way; every
1338
- * button is `disabled` meanwhile, as before.
1344
+ * button is `disabled` meanwhile. Draw a spinner (or the like) on the button
1345
+ * while `loading` is true, and keep its label: after a sign-in it stays true
1346
+ * until the page is left, so the person sees the work go on.
1339
1347
  */
1340
1348
  interface AuthButtonProps {
1341
1349
  type: 'submit' | 'button';
@@ -1376,6 +1384,7 @@ interface AuthInputProps {
1376
1384
  value: string;
1377
1385
  autoComplete?: string;
1378
1386
  required?: boolean;
1387
+ disabled?: boolean;
1379
1388
  className?: string;
1380
1389
  'aria-invalid'?: boolean;
1381
1390
  'aria-describedby'?: string;
@@ -1533,8 +1542,11 @@ interface AcceptInviteProps extends AuthFormProps {
1533
1542
  }
1534
1543
  declare function AcceptInvite(props: AcceptInviteProps): react.JSX.Element;
1535
1544
  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;
1545
+ /**
1546
+ * Called once the person accepted. Nothing else happens: the application
1547
+ * decides what follows. A Promise keeps the button loading until it settles.
1548
+ */
1549
+ onSuccess?: () => void | Promise<unknown>;
1538
1550
  }
1539
1551
  /**
1540
1552
  * The legal documents the signed-in person still owes (a new version of the
@@ -1587,8 +1599,8 @@ type AccountEvent = {
1587
1599
  kind: 'sessions.ended';
1588
1600
  };
1589
1601
  interface AccountProps extends Omit<AuthFormProps, 'links' | 'returnTo' | 'onSuccess'> {
1590
- /** Called after "sign out everywhere". Default: go to `/`. */
1591
- onSignedOut?: () => void;
1602
+ /** Called after "sign out everywhere". Default: go to `/`. A Promise keeps the button loading until it settles. */
1603
+ onSignedOut?: () => void | Promise<unknown>;
1592
1604
  /** The moment of a session, as the application writes dates. Default: the browser's own way, in the reader's time zone. */
1593
1605
  formatDate?: (iso: string) => string;
1594
1606
  /**
package/dist/react.d.ts CHANGED
@@ -1283,7 +1283,7 @@ interface AuthTexts {
1283
1283
  /** By error code; one missing shows what the server said. */
1284
1284
  errors: Record<string, string>;
1285
1285
  }
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';
1286
+ 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
1287
  interface AuthFormProps {
1288
1288
  /** The language of the built-in sentences. Default: `en`. */
1289
1289
  locale?: 'en' | 'es';
@@ -1291,8 +1291,14 @@ interface AuthFormProps {
1291
1291
  texts?: Partial<Omit<AuthTexts, 'errors'>> & {
1292
1292
  errors?: Record<string, string>;
1293
1293
  };
1294
- /** Called once the person is signed in. Default: go to `returnTo`. */
1295
- onSuccess?: () => void;
1294
+ /**
1295
+ * Called once the person is signed in, instead of going to `returnTo`. The
1296
+ * form stays busy (its button loading, every control disabled) until it
1297
+ * leaves the page. When the application keeps the form on screen while it
1298
+ * moves on (a router's navigation, loading the session), return a Promise:
1299
+ * the form waits for it, then is free again if it is still shown.
1300
+ */
1301
+ onSuccess?: () => void | Promise<unknown>;
1296
1302
  /** Where to go after signing in, and where a provider returns to. Default: `/`. */
1297
1303
  returnTo?: string;
1298
1304
  /**
@@ -1335,7 +1341,9 @@ interface AuthFormProps {
1335
1341
  * other action (a provider, a passkey, a sign-in link, cancel, send again);
1336
1342
  * `danger` removes, leaves, deletes, disconnects or turns something off.
1337
1343
  * `loading` is true on the one button whose action is under way; every
1338
- * button is `disabled` meanwhile, as before.
1344
+ * button is `disabled` meanwhile. Draw a spinner (or the like) on the button
1345
+ * while `loading` is true, and keep its label: after a sign-in it stays true
1346
+ * until the page is left, so the person sees the work go on.
1339
1347
  */
1340
1348
  interface AuthButtonProps {
1341
1349
  type: 'submit' | 'button';
@@ -1376,6 +1384,7 @@ interface AuthInputProps {
1376
1384
  value: string;
1377
1385
  autoComplete?: string;
1378
1386
  required?: boolean;
1387
+ disabled?: boolean;
1379
1388
  className?: string;
1380
1389
  'aria-invalid'?: boolean;
1381
1390
  'aria-describedby'?: string;
@@ -1533,8 +1542,11 @@ interface AcceptInviteProps extends AuthFormProps {
1533
1542
  }
1534
1543
  declare function AcceptInvite(props: AcceptInviteProps): react.JSX.Element;
1535
1544
  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;
1545
+ /**
1546
+ * Called once the person accepted. Nothing else happens: the application
1547
+ * decides what follows. A Promise keeps the button loading until it settles.
1548
+ */
1549
+ onSuccess?: () => void | Promise<unknown>;
1538
1550
  }
1539
1551
  /**
1540
1552
  * The legal documents the signed-in person still owes (a new version of the
@@ -1587,8 +1599,8 @@ type AccountEvent = {
1587
1599
  kind: 'sessions.ended';
1588
1600
  };
1589
1601
  interface AccountProps extends Omit<AuthFormProps, 'links' | 'returnTo' | 'onSuccess'> {
1590
- /** Called after "sign out everywhere". Default: go to `/`. */
1591
- onSignedOut?: () => void;
1602
+ /** Called after "sign out everywhere". Default: go to `/`. A Promise keeps the button loading until it settles. */
1603
+ onSignedOut?: () => void | Promise<unknown>;
1592
1604
  /** The moment of a session, as the application writes dates. Default: the browser's own way, in the reader's time zone. */
1593
1605
  formatDate?: (iso: string) => string;
1594
1606
  /**
package/dist/react.js CHANGED
@@ -1136,7 +1136,38 @@ var TEXTS = {
1136
1136
  }
1137
1137
  }
1138
1138
  };
1139
- var PlainButton = ({ variant: _variant, loading: _loading, ...p }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ...p });
1139
+ var PlainButton = ({ variant: _variant, loading, children, ...p }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { ...p, "aria-busy": loading || void 0, children: [
1140
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
1141
+ children
1142
+ ] });
1143
+ var UnstyledButton = ({ variant: _variant, loading, children, ...p }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { ...p, "aria-busy": loading || void 0, children: [
1144
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { plain: true }),
1146
+ " "
1147
+ ] }),
1148
+ children
1149
+ ] });
1150
+ function Spinner({ plain }) {
1151
+ if (!plain) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "cauthy-spinner", "aria-hidden": "true" });
1152
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SpinnerIcon, {});
1153
+ }
1154
+ var LESS_MOTION = "(prefers-reduced-motion: reduce)";
1155
+ function watchMotion(changed) {
1156
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {
1157
+ };
1158
+ const query = window.matchMedia(LESS_MOTION);
1159
+ query.addEventListener?.("change", changed);
1160
+ return () => query.removeEventListener?.("change", changed);
1161
+ }
1162
+ var stillHere = () => typeof window.matchMedia === "function" && window.matchMedia(LESS_MOTION).matches;
1163
+ var stillOnServer = () => true;
1164
+ function SpinnerIcon() {
1165
+ const still = (0, import_react.useSyncExternalStore)(watchMotion, stillHere, stillOnServer);
1166
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "cauthy-spinner-icon", width: "1em", height: "1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: [
1167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }),
1168
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14.5 8A6.5 6.5 0 0 0 8 1.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: !still && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("animateTransform", { attributeName: "transform", type: "rotate", from: "0 8 8", to: "360 8 8", dur: "0.7s", repeatCount: "indefinite" }) })
1169
+ ] });
1170
+ }
1140
1171
  var PlainInput = (p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { ...p });
1141
1172
  var looks = /* @__PURE__ */ new Map();
1142
1173
  var loaded = /* @__PURE__ */ new Set();
@@ -1244,12 +1275,15 @@ function sameOrigin(to) {
1244
1275
  return void 0;
1245
1276
  }
1246
1277
  }
1247
- function useForm(props) {
1278
+ function isPending(v) {
1279
+ return typeof v?.then === "function";
1280
+ }
1281
+ function useForm(props, opts = {}) {
1248
1282
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
1249
1283
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
1250
1284
  const own = (slot) => props.classNames?.[slot];
1251
1285
  const C = props.components ?? {};
1252
- const Button = C.Button ?? PlainButton;
1286
+ const Button = C.Button ?? (props.unstyled ? UnstyledButton : PlainButton);
1253
1287
  const Input = C.Input ?? PlainInput;
1254
1288
  const [error, setOnlyError] = (0, import_react.useState)(null);
1255
1289
  const [problems, setProblems] = (0, import_react.useState)({ list: [], round: 0 });
@@ -1266,9 +1300,32 @@ function useForm(props) {
1266
1300
  const fieldError = (name) => problems.list.find((e) => e.field === name) ?? (error?.field === name ? error : null);
1267
1301
  const firstWrong = (name) => problems.list[0]?.field === name ? problems.round : 0;
1268
1302
  const [acting, setActing] = (0, import_react.useState)(null);
1269
- const busy = acting !== null;
1303
+ const busy = acting !== null || opts.waiting === true;
1270
1304
  const setBusy = (0, import_react.useCallback)((on) => setActing(on === false ? null : on === true ? SUBMIT : on), []);
1271
1305
  const loading = (key = SUBMIT) => acting === key;
1306
+ const shown = (0, import_react.useRef)(true);
1307
+ (0, import_react.useEffect)(() => {
1308
+ shown.current = true;
1309
+ const back = (e) => {
1310
+ if (e.persisted) setActing(null);
1311
+ };
1312
+ window.addEventListener("pageshow", back);
1313
+ return () => {
1314
+ shown.current = false;
1315
+ window.removeEventListener("pageshow", back);
1316
+ };
1317
+ }, []);
1318
+ const hold = (after) => {
1319
+ if (!isPending(after)) return;
1320
+ const free = () => {
1321
+ if (shown.current) setActing(null);
1322
+ };
1323
+ after.then(free, free);
1324
+ };
1325
+ const fail = (e) => {
1326
+ setActing(null);
1327
+ setError(e);
1328
+ };
1272
1329
  const [policy, setPolicy] = (0, import_react.useState)(void 0);
1273
1330
  const locale = props.locale ?? "en";
1274
1331
  const say = (e) => {
@@ -1277,15 +1334,18 @@ function useForm(props) {
1277
1334
  };
1278
1335
  const passwordRule = describePasswordPolicy(policy, locale);
1279
1336
  const done = (to) => {
1280
- if (props.onSuccess) props.onSuccess();
1337
+ setActing((a) => a ?? SUBMIT);
1338
+ if (props.onSuccess) hold(props.onSuccess());
1281
1339
  else window.location.assign(sitePath(to) ?? sameOrigin(props.returnTo) ?? "/");
1282
1340
  };
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 };
1341
+ const root = useRoot(opts.part ? { ...props, unstyled: true } : props);
1342
+ const plain = props.unstyled === true;
1343
+ 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
1344
  }
1286
1345
  var SUBMIT = "submit";
1287
1346
  function Wrap(p) {
1288
1347
  const { Form: OwnForm } = p.f.C;
1348
+ const busy = p.busy ?? (p.f.busy || void 0);
1289
1349
  if (OwnForm) {
1290
1350
  const own = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1291
1351
  OwnForm,
@@ -1293,7 +1353,7 @@ function Wrap(p) {
1293
1353
  as: p.as === "form" ? "form" : "div",
1294
1354
  onSubmit: p.onSubmit,
1295
1355
  noValidate: p.noValidate,
1296
- "aria-busy": p.busy,
1356
+ "aria-busy": busy,
1297
1357
  className: p.slot ? p.f.own(p.slot) : void 0,
1298
1358
  section: p.section,
1299
1359
  children: p.children
@@ -1302,7 +1362,11 @@ function Wrap(p) {
1302
1362
  return p.root?.["data-cauthy"] !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...p.root, children: own }) : own;
1303
1363
  }
1304
1364
  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 });
1365
+ const children = p.busy && p.children === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pending, { f: p.f }) : p.children;
1366
+ 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 });
1367
+ }
1368
+ function Pending({ f }) {
1369
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: f.cx("pending"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { plain: f.plain }) });
1306
1370
  }
1307
1371
  function Note({ f, children, footer }) {
1308
1372
  const { Text } = f.C;
@@ -1343,10 +1407,10 @@ function Check(p) {
1343
1407
  const { f } = p;
1344
1408
  const { Checkbox } = f.C;
1345
1409
  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 });
1410
+ 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
1411
  }
1348
1412
  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) }),
1413
+ /* @__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
1414
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: p.children })
1351
1415
  ] });
1352
1416
  }
@@ -1355,10 +1419,11 @@ function Pick(p) {
1355
1419
  const id = p.id ?? made;
1356
1420
  const { f } = p;
1357
1421
  const { Select } = f.C;
1422
+ const disabled = p.disabled || f.busy;
1358
1423
  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 });
1424
+ 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
1425
  }
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)) });
1426
+ 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
1427
  }
1363
1428
  function Labelled({ f, id, label, children }) {
1364
1429
  const { Field: OwnField } = f.C;
@@ -1412,6 +1477,7 @@ function Field(p) {
1412
1477
  value: p.value,
1413
1478
  autoComplete: p.autoComplete,
1414
1479
  required: p.required,
1480
+ disabled: f.busy,
1415
1481
  className: f.cx("input"),
1416
1482
  "aria-invalid": bad ? true : void 0,
1417
1483
  "aria-describedby": described || void 0,
@@ -1501,6 +1567,7 @@ function SecondStepForm(p) {
1501
1567
  if (ctl.signal.aborted) return;
1502
1568
  setPushNumber(0);
1503
1569
  if (!done.ok) return f.setError(done.error);
1570
+ f.setBusy("push");
1504
1571
  f.done();
1505
1572
  };
1506
1573
  const sendCode = async () => {
@@ -1516,8 +1583,7 @@ function SecondStepForm(p) {
1516
1583
  f.setBusy(true);
1517
1584
  f.setError(null);
1518
1585
  const r = await auth.signIn.mfa({ code });
1519
- f.setBusy(false);
1520
- if (!r.ok) return f.setError(r.error);
1586
+ if (!r.ok) return f.fail(r.error);
1521
1587
  f.done();
1522
1588
  };
1523
1589
  const general = f.error && !f.error.field ? f.error : null;
@@ -1545,7 +1611,11 @@ function SecondStepForm(p) {
1545
1611
  }
1546
1612
  function ProviderButtons(p) {
1547
1613
  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: [
1614
+ const pick = (id) => {
1615
+ f.setBusy(`provider:${id}`);
1616
+ p.onPick(id);
1617
+ };
1618
+ 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
1619
  provider.iconUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: provider.iconUrl, alt: "", width: 18, height: 18, className: f.cx("providerIcon") }),
1550
1620
  f.t.continueWith,
1551
1621
  " ",
@@ -1558,8 +1628,8 @@ function signInOffers(methods) {
1558
1628
  }
1559
1629
  function SignIn(props) {
1560
1630
  const auth = useAuth();
1561
- const f = useForm(props);
1562
1631
  const [methods, setMethods] = (0, import_react.useState)(null);
1632
+ const f = useForm(props, { waiting: methods === null });
1563
1633
  const [email, setEmail] = (0, import_react.useState)("");
1564
1634
  const [password, setPassword] = (0, import_react.useState)("");
1565
1635
  const [code, setCode] = (0, import_react.useState)("");
@@ -1631,14 +1701,16 @@ function SignIn(props) {
1631
1701
  f.setBusy(true);
1632
1702
  f.setError(null);
1633
1703
  const r = step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password, captchaToken: challenge.token });
1634
- f.setBusy(false);
1635
1704
  if (step === "password") challenge.renew();
1636
1705
  if (!r.ok && r.error.code === "sso_required") return toCompany();
1637
1706
  if (!r.ok) {
1638
1707
  setOffer((n) => n + 1);
1639
- return f.setError(r.error);
1708
+ return f.fail(r.error);
1709
+ }
1710
+ if (r.status === "mfa_required") {
1711
+ f.setBusy(false);
1712
+ return setStep("code");
1640
1713
  }
1641
- if (r.status === "mfa_required") return setStep("code");
1642
1714
  f.done("returnTo" in r && typeof r.returnTo === "string" ? r.returnTo : void 0);
1643
1715
  };
1644
1716
  const usePasskey = async () => {
@@ -1647,10 +1719,10 @@ function SignIn(props) {
1647
1719
  f.setError(null);
1648
1720
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale, email });
1649
1721
  if (r.ok && r.status === "redirected") return;
1722
+ if (r.ok) return f.done();
1650
1723
  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();
1724
+ setOffer((n) => n + 1);
1725
+ if (r.error.code !== "cancelled") f.setError(r.error);
1654
1726
  };
1655
1727
  const sendLink = async () => {
1656
1728
  autofill.current?.abort();
@@ -1767,10 +1839,12 @@ function SignUp(props) {
1767
1839
  ...legal ? { acceptLegal, consents } : {},
1768
1840
  captchaToken: challenge.token
1769
1841
  });
1770
- f.setBusy(false);
1771
1842
  challenge.renew();
1772
- if (!r.ok) return f.setError(r.error);
1773
- if (r.status === "verification_required") return setSent(true);
1843
+ if (!r.ok) return f.fail(r.error);
1844
+ if (r.status === "verification_required") {
1845
+ f.setBusy(false);
1846
+ return setSent(true);
1847
+ }
1774
1848
  f.done();
1775
1849
  };
1776
1850
  const general = f.error && !f.error.field ? f.error : null;
@@ -1933,13 +2007,16 @@ function AcceptInvite(props) {
1933
2007
  f.setBusy(true);
1934
2008
  f.setError(null);
1935
2009
  const r = await auth.acceptInvitation(token, password);
1936
- f.setBusy(false);
1937
2010
  if (!r.ok) {
2011
+ f.setBusy(false);
1938
2012
  if (r.error.code === "invalid_token") return setState("invalid");
1939
2013
  return f.setError(r.error);
1940
2014
  }
1941
2015
  props.onSaved?.({ kind: "invitation.accepted", email: invitation?.email ?? "", ...invitation?.organization ? { organization: invitation.organization } : {} });
1942
- if (r.status === "mfa_required") return setState("code");
2016
+ if (r.status === "mfa_required") {
2017
+ f.setBusy(false);
2018
+ return setState("code");
2019
+ }
1943
2020
  f.done();
1944
2021
  };
1945
2022
  const general = f.error && !f.error.field ? f.error : null;
@@ -1977,10 +2054,11 @@ function PendingTerms(props) {
1977
2054
  f.setBusy(true);
1978
2055
  f.setError(null);
1979
2056
  const r = await auth.account.acceptLegal({ kinds: owed.map((d) => d.kind) });
2057
+ if (!r.ok) return f.fail(r.error);
2058
+ const after = props.onSuccess?.();
2059
+ if (isPending(after)) await Promise.resolve(after).catch(() => void 0);
1980
2060
  f.setBusy(false);
1981
- if (!r.ok) return f.setError(r.error);
1982
2061
  setOwed([]);
1983
- props.onSuccess?.();
1984
2062
  };
1985
2063
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Wrap, { f, as: "div", slot: "form", root: f.root, children: [
1986
2064
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Heading, { f, children: f.t.termsChanged }),
@@ -1995,15 +2073,15 @@ function PendingTerms(props) {
1995
2073
  }
1996
2074
  var ACCOUNT_SECTIONS = ["profile", "email", "password", "linked", "secondStep", "phone", "passkeys", "agents", "sessions"];
1997
2075
  function useSection(props) {
1998
- const f = useForm({ ...props, unstyled: true });
2076
+ const f = useForm(props, { part: true });
1999
2077
  const [done, setDone] = (0, import_react.useState)(null);
2000
- const run = async (action, key = SUBMIT) => {
2078
+ const run = async (action, key = SUBMIT, leaves = false) => {
2001
2079
  f.setBusy(key);
2002
2080
  f.setError(null);
2003
2081
  setDone(null);
2004
2082
  const r = await action();
2005
- f.setBusy(false);
2006
- if (!r.ok) f.setError(r.error);
2083
+ if (!r.ok) f.fail(r.error);
2084
+ else if (!leaves) f.setBusy(false);
2007
2085
  return r;
2008
2086
  };
2009
2087
  const general = f.error && !f.error.field ? f.error : null;
@@ -2416,11 +2494,26 @@ function LinkedPart({ props }) {
2416
2494
  " ",
2417
2495
  /* @__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
2496
  ] }, 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))
2497
+ offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2498
+ f.Button,
2499
+ {
2500
+ type: "button",
2501
+ variant: "secondary",
2502
+ loading: f.loading(`link:${p.id}`),
2503
+ disabled: f.busy,
2504
+ className: f.cx("providerButton"),
2505
+ onClick: () => {
2506
+ f.setBusy(`link:${p.id}`);
2507
+ auth.account.link(p.id, { returnTo: here });
2508
+ },
2509
+ children: [
2510
+ f.t.linkWith,
2511
+ " ",
2512
+ p.name
2513
+ ]
2514
+ },
2515
+ p.id
2516
+ ))
2424
2517
  ] });
2425
2518
  }
2426
2519
  function PasswordPart({ props }) {
@@ -2578,7 +2671,7 @@ function PhonePart({ props, account, onChanged }) {
2578
2671
  !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
2672
  !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
2673
  !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) }),
2674
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "radio", name: "channel", value: c, checked: channel === c, disabled: f.busy, onChange: () => setChannel(c) }),
2582
2675
  " ",
2583
2676
  c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
2584
2677
  ] }, c)) }),
@@ -2625,10 +2718,10 @@ function SessionsPart({ props, when }) {
2625
2718
  void auth.account.sessions().then(setSessions);
2626
2719
  }, []);
2627
2720
  const end = async () => {
2628
- const r = await run(() => auth.account.signOutEverywhere());
2721
+ const r = await run(() => auth.account.signOutEverywhere(), SUBMIT, true);
2629
2722
  if (!r.ok) return;
2630
2723
  props.onSaved?.({ kind: "sessions.ended" });
2631
- if (props.onSignedOut) props.onSignedOut();
2724
+ if (props.onSignedOut) f.hold(props.onSignedOut());
2632
2725
  else window.location.assign("/");
2633
2726
  };
2634
2727
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Wrap, { f, as: "section", slot: "section", section: "sessions", children: [