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/dist/react.mjs CHANGED
@@ -721,7 +721,7 @@ function definePolicy(given) {
721
721
  }
722
722
 
723
723
  // src/components.tsx
724
- import { Fragment, useCallback, useEffect, useId, useRef, useState } from "react";
724
+ import { Fragment, useCallback, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
725
725
  import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
726
726
  var TEXTS = {
727
727
  en: {
@@ -1087,7 +1087,38 @@ var TEXTS = {
1087
1087
  }
1088
1088
  }
1089
1089
  };
1090
- var PlainButton = ({ variant: _variant, loading: _loading, ...p }) => /* @__PURE__ */ jsx("button", { ...p });
1090
+ var PlainButton = ({ variant: _variant, loading, children, ...p }) => /* @__PURE__ */ jsxs("button", { ...p, "aria-busy": loading || void 0, children: [
1091
+ loading && /* @__PURE__ */ jsx(Spinner, {}),
1092
+ children
1093
+ ] });
1094
+ var UnstyledButton = ({ variant: _variant, loading, children, ...p }) => /* @__PURE__ */ jsxs("button", { ...p, "aria-busy": loading || void 0, children: [
1095
+ loading && /* @__PURE__ */ jsxs(Fragment2, { children: [
1096
+ /* @__PURE__ */ jsx(Spinner, { plain: true }),
1097
+ " "
1098
+ ] }),
1099
+ children
1100
+ ] });
1101
+ function Spinner({ plain }) {
1102
+ if (!plain) return /* @__PURE__ */ jsx("span", { className: "cauthy-spinner", "aria-hidden": "true" });
1103
+ return /* @__PURE__ */ jsx(SpinnerIcon, {});
1104
+ }
1105
+ var LESS_MOTION = "(prefers-reduced-motion: reduce)";
1106
+ function watchMotion(changed) {
1107
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {
1108
+ };
1109
+ const query = window.matchMedia(LESS_MOTION);
1110
+ query.addEventListener?.("change", changed);
1111
+ return () => query.removeEventListener?.("change", changed);
1112
+ }
1113
+ var stillHere = () => typeof window.matchMedia === "function" && window.matchMedia(LESS_MOTION).matches;
1114
+ var stillOnServer = () => true;
1115
+ function SpinnerIcon() {
1116
+ const still = useSyncExternalStore(watchMotion, stillHere, stillOnServer);
1117
+ return /* @__PURE__ */ jsxs("svg", { className: "cauthy-spinner-icon", width: "1em", height: "1em", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: [
1118
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }),
1119
+ /* @__PURE__ */ jsx("path", { d: "M14.5 8A6.5 6.5 0 0 0 8 1.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: !still && /* @__PURE__ */ jsx("animateTransform", { attributeName: "transform", type: "rotate", from: "0 8 8", to: "360 8 8", dur: "0.7s", repeatCount: "indefinite" }) })
1120
+ ] });
1121
+ }
1091
1122
  var PlainInput = (p) => /* @__PURE__ */ jsx("input", { ...p });
1092
1123
  var looks = /* @__PURE__ */ new Map();
1093
1124
  var loaded = /* @__PURE__ */ new Set();
@@ -1195,12 +1226,15 @@ function sameOrigin(to) {
1195
1226
  return void 0;
1196
1227
  }
1197
1228
  }
1198
- function useForm(props) {
1229
+ function isPending(v) {
1230
+ return typeof v?.then === "function";
1231
+ }
1232
+ function useForm(props, opts = {}) {
1199
1233
  const t = { ...TEXTS[props.locale ?? "en"], ...props.texts, errors: { ...TEXTS[props.locale ?? "en"].errors, ...props.texts?.errors } };
1200
1234
  const cx = (slot) => props.classNames?.[slot] ?? `cauthy-${slot}`;
1201
1235
  const own = (slot) => props.classNames?.[slot];
1202
1236
  const C = props.components ?? {};
1203
- const Button = C.Button ?? PlainButton;
1237
+ const Button = C.Button ?? (props.unstyled ? UnstyledButton : PlainButton);
1204
1238
  const Input = C.Input ?? PlainInput;
1205
1239
  const [error, setOnlyError] = useState(null);
1206
1240
  const [problems, setProblems] = useState({ list: [], round: 0 });
@@ -1217,9 +1251,32 @@ function useForm(props) {
1217
1251
  const fieldError = (name) => problems.list.find((e) => e.field === name) ?? (error?.field === name ? error : null);
1218
1252
  const firstWrong = (name) => problems.list[0]?.field === name ? problems.round : 0;
1219
1253
  const [acting, setActing] = useState(null);
1220
- const busy = acting !== null;
1254
+ const busy = acting !== null || opts.waiting === true;
1221
1255
  const setBusy = useCallback((on) => setActing(on === false ? null : on === true ? SUBMIT : on), []);
1222
1256
  const loading = (key = SUBMIT) => acting === key;
1257
+ const shown = useRef(true);
1258
+ useEffect(() => {
1259
+ shown.current = true;
1260
+ const back = (e) => {
1261
+ if (e.persisted) setActing(null);
1262
+ };
1263
+ window.addEventListener("pageshow", back);
1264
+ return () => {
1265
+ shown.current = false;
1266
+ window.removeEventListener("pageshow", back);
1267
+ };
1268
+ }, []);
1269
+ const hold = (after) => {
1270
+ if (!isPending(after)) return;
1271
+ const free = () => {
1272
+ if (shown.current) setActing(null);
1273
+ };
1274
+ after.then(free, free);
1275
+ };
1276
+ const fail = (e) => {
1277
+ setActing(null);
1278
+ setError(e);
1279
+ };
1223
1280
  const [policy, setPolicy] = useState(void 0);
1224
1281
  const locale = props.locale ?? "en";
1225
1282
  const say = (e) => {
@@ -1228,15 +1285,18 @@ function useForm(props) {
1228
1285
  };
1229
1286
  const passwordRule = describePasswordPolicy(policy, locale);
1230
1287
  const done = (to) => {
1231
- if (props.onSuccess) props.onSuccess();
1288
+ setActing((a) => a ?? SUBMIT);
1289
+ if (props.onSuccess) hold(props.onSuccess());
1232
1290
  else window.location.assign(sitePath(to) ?? sameOrigin(props.returnTo) ?? "/");
1233
1291
  };
1234
- const root = useRoot(props);
1235
- return { t, cx, own, C, Button, Input, error, setError, check, fieldError, firstWrong, busy, setBusy, loading, say, done, root, policy, setPolicy, passwordRule };
1292
+ const root = useRoot(opts.part ? { ...props, unstyled: true } : props);
1293
+ const plain = props.unstyled === true;
1294
+ return { t, cx, own, C, Button, Input, error, setError, fail, check, fieldError, firstWrong, busy, setBusy, loading, hold, say, done, root, plain, policy, setPolicy, passwordRule };
1236
1295
  }
1237
1296
  var SUBMIT = "submit";
1238
1297
  function Wrap(p) {
1239
1298
  const { Form: OwnForm } = p.f.C;
1299
+ const busy = p.busy ?? (p.f.busy || void 0);
1240
1300
  if (OwnForm) {
1241
1301
  const own = /* @__PURE__ */ jsx(
1242
1302
  OwnForm,
@@ -1244,7 +1304,7 @@ function Wrap(p) {
1244
1304
  as: p.as === "form" ? "form" : "div",
1245
1305
  onSubmit: p.onSubmit,
1246
1306
  noValidate: p.noValidate,
1247
- "aria-busy": p.busy,
1307
+ "aria-busy": busy,
1248
1308
  className: p.slot ? p.f.own(p.slot) : void 0,
1249
1309
  section: p.section,
1250
1310
  children: p.children
@@ -1253,7 +1313,11 @@ function Wrap(p) {
1253
1313
  return p.root?.["data-cauthy"] !== void 0 ? /* @__PURE__ */ jsx("div", { ...p.root, children: own }) : own;
1254
1314
  }
1255
1315
  const Tag2 = p.as;
1256
- return /* @__PURE__ */ 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 });
1316
+ const children = p.busy && p.children === void 0 ? /* @__PURE__ */ jsx(Pending, { f: p.f }) : p.children;
1317
+ return /* @__PURE__ */ jsx(Tag2, { className: p.slot ? p.f.cx(p.slot) : void 0, ...p.root, onSubmit: p.onSubmit, noValidate: p.noValidate, "aria-busy": busy, children });
1318
+ }
1319
+ function Pending({ f }) {
1320
+ return /* @__PURE__ */ jsx("div", { className: f.cx("pending"), children: /* @__PURE__ */ jsx(Spinner, { plain: f.plain }) });
1257
1321
  }
1258
1322
  function Note({ f, children, footer }) {
1259
1323
  const { Text } = f.C;
@@ -1294,10 +1358,10 @@ function Check(p) {
1294
1358
  const { f } = p;
1295
1359
  const { Checkbox } = f.C;
1296
1360
  if (Checkbox) {
1297
- return /* @__PURE__ */ jsx(Checkbox, { id, name: p.name, checked: p.checked, required: p.required, className: f.own("check"), onChange: p.onChange, children: p.children });
1361
+ return /* @__PURE__ */ 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 });
1298
1362
  }
1299
1363
  return /* @__PURE__ */ jsxs("label", { className: f.cx("check"), children: [
1300
- /* @__PURE__ */ jsx("input", { type: "checkbox", name: p.name, checked: p.checked, required: p.required, onChange: (e) => p.onChange(e.target.checked) }),
1364
+ /* @__PURE__ */ jsx("input", { type: "checkbox", name: p.name, checked: p.checked, required: p.required, disabled: f.busy, onChange: (e) => p.onChange(e.target.checked) }),
1301
1365
  /* @__PURE__ */ jsx("span", { children: p.children })
1302
1366
  ] });
1303
1367
  }
@@ -1306,10 +1370,11 @@ function Pick(p) {
1306
1370
  const id = p.id ?? made;
1307
1371
  const { f } = p;
1308
1372
  const { Select } = f.C;
1373
+ const disabled = p.disabled || f.busy;
1309
1374
  if (Select) {
1310
- return /* @__PURE__ */ 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 });
1375
+ return /* @__PURE__ */ jsx(Select, { id, name: p.name, value: p.value, options: p.options, disabled, "aria-label": p.label, className: f.own("input"), onChange: p.onChange });
1311
1376
  }
1312
- return /* @__PURE__ */ 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__ */ jsx("option", { value: o.value, children: o.label }, o.value)) });
1377
+ return /* @__PURE__ */ 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__ */ jsx("option", { value: o.value, children: o.label }, o.value)) });
1313
1378
  }
1314
1379
  function Labelled({ f, id, label, children }) {
1315
1380
  const { Field: OwnField } = f.C;
@@ -1363,6 +1428,7 @@ function Field(p) {
1363
1428
  value: p.value,
1364
1429
  autoComplete: p.autoComplete,
1365
1430
  required: p.required,
1431
+ disabled: f.busy,
1366
1432
  className: f.cx("input"),
1367
1433
  "aria-invalid": bad ? true : void 0,
1368
1434
  "aria-describedby": described || void 0,
@@ -1452,6 +1518,7 @@ function SecondStepForm(p) {
1452
1518
  if (ctl.signal.aborted) return;
1453
1519
  setPushNumber(0);
1454
1520
  if (!done.ok) return f.setError(done.error);
1521
+ f.setBusy("push");
1455
1522
  f.done();
1456
1523
  };
1457
1524
  const sendCode = async () => {
@@ -1467,8 +1534,7 @@ function SecondStepForm(p) {
1467
1534
  f.setBusy(true);
1468
1535
  f.setError(null);
1469
1536
  const r = await auth.signIn.mfa({ code });
1470
- f.setBusy(false);
1471
- if (!r.ok) return f.setError(r.error);
1537
+ if (!r.ok) return f.fail(r.error);
1472
1538
  f.done();
1473
1539
  };
1474
1540
  const general = f.error && !f.error.field ? f.error : null;
@@ -1496,7 +1562,11 @@ function SecondStepForm(p) {
1496
1562
  }
1497
1563
  function ProviderButtons(p) {
1498
1564
  const { f } = p;
1499
- return /* @__PURE__ */ jsx(Fragment2, { children: p.providers.map((provider) => /* @__PURE__ */ jsxs(f.Button, { type: "button", variant: "secondary", className: f.cx("providerButton"), onClick: () => p.onPick(provider.id), children: [
1565
+ const pick = (id) => {
1566
+ f.setBusy(`provider:${id}`);
1567
+ p.onPick(id);
1568
+ };
1569
+ return /* @__PURE__ */ jsx(Fragment2, { children: p.providers.map((provider) => /* @__PURE__ */ 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: [
1500
1570
  provider.iconUrl && /* @__PURE__ */ jsx("img", { src: provider.iconUrl, alt: "", width: 18, height: 18, className: f.cx("providerIcon") }),
1501
1571
  f.t.continueWith,
1502
1572
  " ",
@@ -1509,8 +1579,8 @@ function signInOffers(methods) {
1509
1579
  }
1510
1580
  function SignIn(props) {
1511
1581
  const auth = useAuth();
1512
- const f = useForm(props);
1513
1582
  const [methods, setMethods] = useState(null);
1583
+ const f = useForm(props, { waiting: methods === null });
1514
1584
  const [email, setEmail] = useState("");
1515
1585
  const [password, setPassword] = useState("");
1516
1586
  const [code, setCode] = useState("");
@@ -1582,14 +1652,16 @@ function SignIn(props) {
1582
1652
  f.setBusy(true);
1583
1653
  f.setError(null);
1584
1654
  const r = step === "sent" ? await auth.signIn.emailCode({ code }) : await auth.signIn.email({ email, password, captchaToken: challenge.token });
1585
- f.setBusy(false);
1586
1655
  if (step === "password") challenge.renew();
1587
1656
  if (!r.ok && r.error.code === "sso_required") return toCompany();
1588
1657
  if (!r.ok) {
1589
1658
  setOffer((n) => n + 1);
1590
- return f.setError(r.error);
1659
+ return f.fail(r.error);
1660
+ }
1661
+ if (r.status === "mfa_required") {
1662
+ f.setBusy(false);
1663
+ return setStep("code");
1591
1664
  }
1592
- if (r.status === "mfa_required") return setStep("code");
1593
1665
  f.done("returnTo" in r && typeof r.returnTo === "string" ? r.returnTo : void 0);
1594
1666
  };
1595
1667
  const usePasskey = async () => {
@@ -1598,10 +1670,10 @@ function SignIn(props) {
1598
1670
  f.setError(null);
1599
1671
  const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale, email });
1600
1672
  if (r.ok && r.status === "redirected") return;
1673
+ if (r.ok) return f.done();
1601
1674
  f.setBusy(false);
1602
- if (!r.ok) setOffer((n) => n + 1);
1603
- if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
1604
- f.done();
1675
+ setOffer((n) => n + 1);
1676
+ if (r.error.code !== "cancelled") f.setError(r.error);
1605
1677
  };
1606
1678
  const sendLink = async () => {
1607
1679
  autofill.current?.abort();
@@ -1718,10 +1790,12 @@ function SignUp(props) {
1718
1790
  ...legal ? { acceptLegal, consents } : {},
1719
1791
  captchaToken: challenge.token
1720
1792
  });
1721
- f.setBusy(false);
1722
1793
  challenge.renew();
1723
- if (!r.ok) return f.setError(r.error);
1724
- if (r.status === "verification_required") return setSent(true);
1794
+ if (!r.ok) return f.fail(r.error);
1795
+ if (r.status === "verification_required") {
1796
+ f.setBusy(false);
1797
+ return setSent(true);
1798
+ }
1725
1799
  f.done();
1726
1800
  };
1727
1801
  const general = f.error && !f.error.field ? f.error : null;
@@ -1884,13 +1958,16 @@ function AcceptInvite(props) {
1884
1958
  f.setBusy(true);
1885
1959
  f.setError(null);
1886
1960
  const r = await auth.acceptInvitation(token, password);
1887
- f.setBusy(false);
1888
1961
  if (!r.ok) {
1962
+ f.setBusy(false);
1889
1963
  if (r.error.code === "invalid_token") return setState("invalid");
1890
1964
  return f.setError(r.error);
1891
1965
  }
1892
1966
  props.onSaved?.({ kind: "invitation.accepted", email: invitation?.email ?? "", ...invitation?.organization ? { organization: invitation.organization } : {} });
1893
- if (r.status === "mfa_required") return setState("code");
1967
+ if (r.status === "mfa_required") {
1968
+ f.setBusy(false);
1969
+ return setState("code");
1970
+ }
1894
1971
  f.done();
1895
1972
  };
1896
1973
  const general = f.error && !f.error.field ? f.error : null;
@@ -1928,10 +2005,11 @@ function PendingTerms(props) {
1928
2005
  f.setBusy(true);
1929
2006
  f.setError(null);
1930
2007
  const r = await auth.account.acceptLegal({ kinds: owed.map((d) => d.kind) });
2008
+ if (!r.ok) return f.fail(r.error);
2009
+ const after = props.onSuccess?.();
2010
+ if (isPending(after)) await Promise.resolve(after).catch(() => void 0);
1931
2011
  f.setBusy(false);
1932
- if (!r.ok) return f.setError(r.error);
1933
2012
  setOwed([]);
1934
- props.onSuccess?.();
1935
2013
  };
1936
2014
  return /* @__PURE__ */ jsxs(Wrap, { f, as: "div", slot: "form", root: f.root, children: [
1937
2015
  /* @__PURE__ */ jsx(Heading, { f, children: f.t.termsChanged }),
@@ -1946,15 +2024,15 @@ function PendingTerms(props) {
1946
2024
  }
1947
2025
  var ACCOUNT_SECTIONS = ["profile", "email", "password", "linked", "secondStep", "phone", "passkeys", "agents", "sessions"];
1948
2026
  function useSection(props) {
1949
- const f = useForm({ ...props, unstyled: true });
2027
+ const f = useForm(props, { part: true });
1950
2028
  const [done, setDone] = useState(null);
1951
- const run = async (action, key = SUBMIT) => {
2029
+ const run = async (action, key = SUBMIT, leaves = false) => {
1952
2030
  f.setBusy(key);
1953
2031
  f.setError(null);
1954
2032
  setDone(null);
1955
2033
  const r = await action();
1956
- f.setBusy(false);
1957
- if (!r.ok) f.setError(r.error);
2034
+ if (!r.ok) f.fail(r.error);
2035
+ else if (!leaves) f.setBusy(false);
1958
2036
  return r;
1959
2037
  };
1960
2038
  const general = f.error && !f.error.field ? f.error : null;
@@ -2367,11 +2445,26 @@ function LinkedPart({ props }) {
2367
2445
  " ",
2368
2446
  /* @__PURE__ */ 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 })
2369
2447
  ] }, x.provider)) }),
2370
- offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ jsxs(f.Button, { type: "button", variant: "secondary", className: f.cx("providerButton"), onClick: () => auth.account.link(p.id, { returnTo: here }), children: [
2371
- f.t.linkWith,
2372
- " ",
2373
- p.name
2374
- ] }, p.id))
2448
+ offered.filter((p) => !linked.some((x) => x.provider === p.id)).map((p) => /* @__PURE__ */ jsxs(
2449
+ f.Button,
2450
+ {
2451
+ type: "button",
2452
+ variant: "secondary",
2453
+ loading: f.loading(`link:${p.id}`),
2454
+ disabled: f.busy,
2455
+ className: f.cx("providerButton"),
2456
+ onClick: () => {
2457
+ f.setBusy(`link:${p.id}`);
2458
+ auth.account.link(p.id, { returnTo: here });
2459
+ },
2460
+ children: [
2461
+ f.t.linkWith,
2462
+ " ",
2463
+ p.name
2464
+ ]
2465
+ },
2466
+ p.id
2467
+ ))
2375
2468
  ] });
2376
2469
  }
2377
2470
  function PasswordPart({ props }) {
@@ -2529,7 +2622,7 @@ function PhonePart({ props, account, onChanged }) {
2529
2622
  !ways.phone && /* @__PURE__ */ jsx(Field, { f, name: "phone", type: "tel", label: f.t.phone, value: number, autoComplete: "tel", required: true, onChange: setNumber }),
2530
2623
  !ways.phone && ways.phoneChannels.length > 1 && f.C.Select && /* @__PURE__ */ jsx(ChannelPick, { f, value: channel, channels: ways.phoneChannels, onChange: setChannel }),
2531
2624
  !ways.phone && ways.phoneChannels.length > 1 && !f.C.Select && /* @__PURE__ */ jsx("div", { className: f.cx("field"), role: "radiogroup", children: ways.phoneChannels.map((c) => /* @__PURE__ */ jsxs("label", { className: f.cx("label"), children: [
2532
- /* @__PURE__ */ jsx("input", { type: "radio", name: "channel", value: c, checked: channel === c, onChange: () => setChannel(c) }),
2625
+ /* @__PURE__ */ jsx("input", { type: "radio", name: "channel", value: c, checked: channel === c, disabled: f.busy, onChange: () => setChannel(c) }),
2533
2626
  " ",
2534
2627
  c === "whatsapp" ? f.t.byWhatsapp : f.t.bySms
2535
2628
  ] }, c)) }),
@@ -2576,10 +2669,10 @@ function SessionsPart({ props, when }) {
2576
2669
  void auth.account.sessions().then(setSessions);
2577
2670
  }, []);
2578
2671
  const end = async () => {
2579
- const r = await run(() => auth.account.signOutEverywhere());
2672
+ const r = await run(() => auth.account.signOutEverywhere(), SUBMIT, true);
2580
2673
  if (!r.ok) return;
2581
2674
  props.onSaved?.({ kind: "sessions.ended" });
2582
- if (props.onSignedOut) props.onSignedOut();
2675
+ if (props.onSignedOut) f.hold(props.onSignedOut());
2583
2676
  else window.location.assign("/");
2584
2677
  };
2585
2678
  return /* @__PURE__ */ jsxs(Wrap, { f, as: "section", slot: "section", section: "sessions", children: [