@orangecheck/auth-client 2.25.3 → 2.26.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var React3 = require('react');
3
+ var React4 = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var browser = require('@simplewebauthn/browser');
6
6
 
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var React3__namespace = /*#__PURE__*/_interopNamespace(React3);
25
+ var React4__namespace = /*#__PURE__*/_interopNamespace(React4);
26
26
 
27
27
  // src/provider.tsx
28
28
 
@@ -221,7 +221,7 @@ async function consumeTabAccountHint(authOrigin) {
221
221
  }
222
222
  return null;
223
223
  }
224
- var SessionContext = React3__namespace.createContext(null);
224
+ var SessionContext = React4__namespace.createContext(null);
225
225
  function normalizeDisplayIdentity(raw, didOc) {
226
226
  const di = raw.display_identity ?? raw.displayIdentity;
227
227
  if (di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind)) {
@@ -279,14 +279,14 @@ function OcSessionProvider({
279
279
  config,
280
280
  defaultReturnTo
281
281
  }) {
282
- const cfg = React3__namespace.useMemo(() => resolveConfig(config), [config]);
283
- const [account, setAccount] = React3__namespace.useState(null);
284
- const [roster, setRoster] = React3__namespace.useState([]);
285
- const [status, setStatus] = React3__namespace.useState("loading");
286
- const [error, setError] = React3__namespace.useState(null);
287
- const [tabPinned, setTabPinned] = React3__namespace.useState(false);
288
- const pinInFlightRef = React3__namespace.useRef(false);
289
- const pinThisTab = React3__namespace.useCallback(
282
+ const cfg = React4__namespace.useMemo(() => resolveConfig(config), [config]);
283
+ const [account, setAccount] = React4__namespace.useState(null);
284
+ const [roster, setRoster] = React4__namespace.useState([]);
285
+ const [status, setStatus] = React4__namespace.useState("loading");
286
+ const [error, setError] = React4__namespace.useState(null);
287
+ const [tabPinned, setTabPinned] = React4__namespace.useState(false);
288
+ const pinInFlightRef = React4__namespace.useRef(false);
289
+ const pinThisTab = React4__namespace.useCallback(
290
290
  async (didOc) => {
291
291
  if (pinInFlightRef.current || readTabSession()) return;
292
292
  pinInFlightRef.current = true;
@@ -309,7 +309,7 @@ function OcSessionProvider({
309
309
  },
310
310
  [cfg.authOrigin]
311
311
  );
312
- const refresh = React3__namespace.useCallback(async () => {
312
+ const refresh = React4__namespace.useCallback(async () => {
313
313
  if (typeof window === "undefined") return;
314
314
  try {
315
315
  let pin = readTabSession();
@@ -363,7 +363,7 @@ function OcSessionProvider({
363
363
  setError(err instanceof Error ? err : new Error(String(err)));
364
364
  }
365
365
  }, [cfg, pinThisTab]);
366
- React3__namespace.useEffect(() => {
366
+ React4__namespace.useEffect(() => {
367
367
  let cancelled = false;
368
368
  consumeTabAdoptMarker();
369
369
  void (async () => {
@@ -374,9 +374,9 @@ function OcSessionProvider({
374
374
  cancelled = true;
375
375
  };
376
376
  }, [refresh, cfg.authOrigin]);
377
- React3__namespace.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
378
- React3__namespace.useEffect(() => installTabLinkDecorator(cfg.authOrigin), [cfg.authOrigin]);
379
- const signOut = React3__namespace.useCallback(
377
+ React4__namespace.useEffect(() => installTabFetchInterceptor(cfg.authOrigin), [cfg.authOrigin]);
378
+ React4__namespace.useEffect(() => installTabLinkDecorator(cfg.authOrigin), [cfg.authOrigin]);
379
+ const signOut = React4__namespace.useCallback(
380
380
  async (opts) => {
381
381
  const scope = opts?.scope ?? "all";
382
382
  try {
@@ -412,7 +412,7 @@ function OcSessionProvider({
412
412
  },
413
413
  [cfg.authOrigin, cfg.logoutPath, refresh]
414
414
  );
415
- const switchAccount = React3__namespace.useCallback(
415
+ const switchAccount = React4__namespace.useCallback(
416
416
  async (didOc) => {
417
417
  if (typeof window === "undefined") return;
418
418
  const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
@@ -442,14 +442,14 @@ function OcSessionProvider({
442
442
  },
443
443
  [cfg.authOrigin, refresh]
444
444
  );
445
- const addAccountUrl = React3__namespace.useCallback(
445
+ const addAccountUrl = React4__namespace.useCallback(
446
446
  (returnTo) => {
447
447
  const rt = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
448
448
  return buildAddAccountUrl(cfg, rt);
449
449
  },
450
450
  [cfg]
451
451
  );
452
- const setDisplayIdentity = React3__namespace.useCallback(
452
+ const setDisplayIdentity = React4__namespace.useCallback(
453
453
  async (kind) => {
454
454
  if (typeof window === "undefined") return;
455
455
  const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
@@ -479,7 +479,7 @@ function OcSessionProvider({
479
479
  },
480
480
  [cfg.authOrigin, refresh]
481
481
  );
482
- const value = React3__namespace.useMemo(() => {
482
+ const value = React4__namespace.useMemo(() => {
483
483
  const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
484
484
  return {
485
485
  status,
@@ -511,7 +511,7 @@ function OcSessionProvider({
511
511
  return /* @__PURE__ */ jsxRuntime.jsx(SessionContext.Provider, { value, children });
512
512
  }
513
513
  function useOcSession() {
514
- const ctx = React3__namespace.useContext(SessionContext);
514
+ const ctx = React4__namespace.useContext(SessionContext);
515
515
  if (!ctx) {
516
516
  throw new Error(
517
517
  "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
@@ -520,7 +520,7 @@ function useOcSession() {
520
520
  return ctx;
521
521
  }
522
522
  function useOptionalOcSession() {
523
- return React3__namespace.useContext(SessionContext);
523
+ return React4__namespace.useContext(SessionContext);
524
524
  }
525
525
  function shortenAddress(addr) {
526
526
  if (addr.length <= 12) return addr;
@@ -535,7 +535,7 @@ function isPrefixOf(value, target) {
535
535
  }
536
536
  var listboxIdCounter = 0;
537
537
  function useUniqueId(prefix) {
538
- const [id] = React3__namespace.useState(() => `${prefix}-${++listboxIdCounter}`);
538
+ const [id] = React4__namespace.useState(() => `${prefix}-${++listboxIdCounter}`);
539
539
  return id;
540
540
  }
541
541
  function OcAccountChip({
@@ -549,9 +549,9 @@ function OcAccountChip({
549
549
  }) {
550
550
  const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
551
551
  const signInUrl = signInUrlOverride ?? sessionSignInUrl;
552
- const [open, setOpen] = React3__namespace.useState(false);
553
- const wrapRef = React3__namespace.useRef(null);
554
- React3__namespace.useEffect(() => {
552
+ const [open, setOpen] = React4__namespace.useState(false);
553
+ const wrapRef = React4__namespace.useRef(null);
554
+ React4__namespace.useEffect(() => {
555
555
  if (!open) return;
556
556
  function handleClickOutside(e) {
557
557
  if (wrapRef.current && !wrapRef.current.contains(e.target)) {
@@ -874,9 +874,9 @@ function useOcAddressSuggestion(options) {
874
874
  } = options;
875
875
  const { status, account } = useOcSession();
876
876
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
877
- const [open, setOpen] = React3__namespace.useState(false);
878
- const [highlighted, setHighlighted] = React3__namespace.useState(false);
879
- const blurTimer = React3__namespace.useRef(null);
877
+ const [open, setOpen] = React4__namespace.useState(false);
878
+ const [highlighted, setHighlighted] = React4__namespace.useState(false);
879
+ const blurTimer = React4__namespace.useRef(null);
880
880
  const listboxId = useUniqueId("oc-addr-listbox");
881
881
  const optionId = `${listboxId}-opt`;
882
882
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -888,7 +888,7 @@ function useOcAddressSuggestion(options) {
888
888
  setOpen(false);
889
889
  setHighlighted(false);
890
890
  }
891
- React3__namespace.useEffect(() => {
891
+ React4__namespace.useEffect(() => {
892
892
  return () => {
893
893
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
894
894
  };
@@ -991,7 +991,7 @@ function useOcAddressSuggestion(options) {
991
991
  ) : null;
992
992
  return { inputProps, popover };
993
993
  }
994
- var OcAddressInput = React3__namespace.forwardRef(
994
+ var OcAddressInput = React4__namespace.forwardRef(
995
995
  function OcAddressInput2({
996
996
  value,
997
997
  onValueChange,
@@ -1006,15 +1006,15 @@ var OcAddressInput = React3__namespace.forwardRef(
1006
1006
  }, forwardedRef) {
1007
1007
  const { status, account } = useOcSession();
1008
1008
  const sessionAddress = status === "authenticated" ? account?.primaryBtc ?? null : null;
1009
- const [open, setOpen] = React3__namespace.useState(false);
1010
- const [highlighted, setHighlighted] = React3__namespace.useState(false);
1011
- const innerRef = React3__namespace.useRef(null);
1009
+ const [open, setOpen] = React4__namespace.useState(false);
1010
+ const [highlighted, setHighlighted] = React4__namespace.useState(false);
1011
+ const innerRef = React4__namespace.useRef(null);
1012
1012
  const setRef = (node) => {
1013
1013
  innerRef.current = node;
1014
1014
  if (typeof forwardedRef === "function") forwardedRef(node);
1015
1015
  else if (forwardedRef) forwardedRef.current = node;
1016
1016
  };
1017
- const blurTimer = React3__namespace.useRef(null);
1017
+ const blurTimer = React4__namespace.useRef(null);
1018
1018
  const listboxId = useUniqueId("oc-addr-listbox");
1019
1019
  const optionId = `${listboxId}-opt`;
1020
1020
  const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
@@ -1061,7 +1061,7 @@ var OcAddressInput = React3__namespace.forwardRef(
1061
1061
  }
1062
1062
  onKeyDown?.(e);
1063
1063
  }
1064
- React3__namespace.useEffect(() => {
1064
+ React4__namespace.useEffect(() => {
1065
1065
  return () => {
1066
1066
  if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
1067
1067
  };
@@ -1195,13 +1195,14 @@ async function fetchOcLinkedIdentities(opts) {
1195
1195
  function OcLinkedIdentities({
1196
1196
  authOrigin = DEFAULT_AUTH_ORIGIN,
1197
1197
  className,
1198
- onChange
1198
+ onChange,
1199
+ attestationLink = true
1199
1200
  }) {
1200
- const [resp, setResp] = React3__namespace.useState(null);
1201
- const [error, setError] = React3__namespace.useState(null);
1202
- const [showEmailLink, setShowEmailLink] = React3__namespace.useState(false);
1203
- const [showBtcLink, setShowBtcLink] = React3__namespace.useState(false);
1204
- const refresh = React3__namespace.useCallback(async () => {
1201
+ const [resp, setResp] = React4__namespace.useState(null);
1202
+ const [error, setError] = React4__namespace.useState(null);
1203
+ const [showEmailLink, setShowEmailLink] = React4__namespace.useState(false);
1204
+ const [showBtcLink, setShowBtcLink] = React4__namespace.useState(false);
1205
+ const refresh = React4__namespace.useCallback(async () => {
1205
1206
  setError(null);
1206
1207
  try {
1207
1208
  const r = await fetch(`${authOrigin}/api/auth/identities`, {
@@ -1220,10 +1221,10 @@ function OcLinkedIdentities({
1220
1221
  setError(e instanceof Error ? e.message : "load failed");
1221
1222
  }
1222
1223
  }, [authOrigin]);
1223
- React3__namespace.useEffect(() => {
1224
+ React4__namespace.useEffect(() => {
1224
1225
  void refresh();
1225
1226
  }, [refresh]);
1226
- const afterChange = React3__namespace.useCallback(() => {
1227
+ const afterChange = React4__namespace.useCallback(() => {
1227
1228
  setShowEmailLink(false);
1228
1229
  setShowBtcLink(false);
1229
1230
  void refresh();
@@ -1255,11 +1256,7 @@ function OcLinkedIdentities({
1255
1256
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
1256
1257
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle(), children: [
1257
1258
  /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "\xA7 what this is" }),
1258
- /* @__PURE__ */ jsxRuntime.jsxs("p", { style: bodyStyle, children: [
1259
- "Your public identity is the opaque ",
1260
- /* @__PURE__ */ jsxRuntime.jsx("code", { style: codeStyle, children: "did:oc:" }),
1261
- " DID. Below it sit your private linked identities \u2014 your email and your Bitcoin address. They never appear in dashboard chrome and never reach an integrator unless you grant the matching scope. Linking a second identity is also your account-recovery path: if you lose one, the other still signs you in."
1262
- ] })
1259
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: bodyStyle, children: "These are the ways you can sign in to this account. An app you sign in to sees one only if you grant it. Linking a second one is also your way back in: lose one, and the other still signs you in." })
1263
1260
  ] }),
1264
1261
  mergedFrom.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ConsolidationPanel, { entries: mergedFrom }),
1265
1262
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
@@ -1312,7 +1309,7 @@ function OcLinkedIdentities({
1312
1309
  onCancel: () => setShowBtcLink(false)
1313
1310
  }
1314
1311
  ),
1315
- hasBtc && /* @__PURE__ */ jsxRuntime.jsx(AttestationLink, {})
1312
+ hasBtc && attestationLink && /* @__PURE__ */ jsxRuntime.jsx(AttestationLink, {})
1316
1313
  ] });
1317
1314
  }
1318
1315
  function IdentityGroup({
@@ -1356,10 +1353,10 @@ function IdentityRow({
1356
1353
  authOrigin,
1357
1354
  onChange
1358
1355
  }) {
1359
- const [shown, setShown] = React3__namespace.useState(false);
1360
- const [confirming, setConfirming] = React3__namespace.useState(false);
1361
- const [busy, setBusy] = React3__namespace.useState(false);
1362
- const [err, setErr] = React3__namespace.useState(null);
1356
+ const [shown, setShown] = React4__namespace.useState(false);
1357
+ const [confirming, setConfirming] = React4__namespace.useState(false);
1358
+ const [busy, setBusy] = React4__namespace.useState(false);
1359
+ const [err, setErr] = React4__namespace.useState(null);
1363
1360
  const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
1364
1361
  const display = shown && row.value ? row.value : masked;
1365
1362
  async function unlink() {
@@ -1558,13 +1555,13 @@ function EmailLinkForm({
1558
1555
  onDone,
1559
1556
  onCancel
1560
1557
  }) {
1561
- const [step, setStep] = React3__namespace.useState("email");
1562
- const [email, setEmail] = React3__namespace.useState("");
1563
- const [code, setCode] = React3__namespace.useState("");
1564
- const [token, setToken] = React3__namespace.useState("");
1565
- const [busy, setBusy] = React3__namespace.useState(false);
1566
- const [err, setErr] = React3__namespace.useState(null);
1567
- const [transfer, setTransfer] = React3__namespace.useState(false);
1558
+ const [step, setStep] = React4__namespace.useState("email");
1559
+ const [email, setEmail] = React4__namespace.useState("");
1560
+ const [code, setCode] = React4__namespace.useState("");
1561
+ const [token, setToken] = React4__namespace.useState("");
1562
+ const [busy, setBusy] = React4__namespace.useState(false);
1563
+ const [err, setErr] = React4__namespace.useState(null);
1564
+ const [transfer, setTransfer] = React4__namespace.useState(false);
1568
1565
  async function start() {
1569
1566
  setBusy(true);
1570
1567
  setErr(null);
@@ -1707,12 +1704,12 @@ function BtcLinkForm({
1707
1704
  onDone,
1708
1705
  onCancel
1709
1706
  }) {
1710
- const [step, setStep] = React3__namespace.useState("request");
1711
- const [addr, setAddr] = React3__namespace.useState("");
1712
- const [challenge, setChallenge] = React3__namespace.useState("");
1713
- const [busy, setBusy] = React3__namespace.useState(false);
1714
- const [err, setErr] = React3__namespace.useState(null);
1715
- const [pendingSig, setPendingSig] = React3__namespace.useState(null);
1707
+ const [step, setStep] = React4__namespace.useState("request");
1708
+ const [addr, setAddr] = React4__namespace.useState("");
1709
+ const [challenge, setChallenge] = React4__namespace.useState("");
1710
+ const [busy, setBusy] = React4__namespace.useState(false);
1711
+ const [err, setErr] = React4__namespace.useState(null);
1712
+ const [pendingSig, setPendingSig] = React4__namespace.useState(null);
1716
1713
  async function getChallenge() {
1717
1714
  if (!didOc) {
1718
1715
  setErr("session not loaded yet \xB7 refresh and retry");
@@ -1872,9 +1869,9 @@ function TransferConfirm({
1872
1869
  ] });
1873
1870
  }
1874
1871
  function LazyWalletButton(props) {
1875
- const [Comp, setComp] = React3__namespace.useState(null);
1876
- const [failed, setFailed] = React3__namespace.useState(false);
1877
- React3__namespace.useEffect(() => {
1872
+ const [Comp, setComp] = React4__namespace.useState(null);
1873
+ const [failed, setFailed] = React4__namespace.useState(false);
1874
+ React4__namespace.useEffect(() => {
1878
1875
  let alive = true;
1879
1876
  import('@orangecheck/wallet-adapter/react').then((m) => {
1880
1877
  if (alive) {
@@ -1982,10 +1979,6 @@ var hintStyle = {
1982
1979
  fontSize: 10.5,
1983
1980
  opacity: 0.8
1984
1981
  };
1985
- var codeStyle = {
1986
- fontFamily: MONO,
1987
- color: V.foreground
1988
- };
1989
1982
  var rowStyle = {
1990
1983
  display: "flex",
1991
1984
  flexWrap: "wrap",
@@ -2092,6 +2085,50 @@ function hardNavigate(target) {
2092
2085
  if (typeof window === "undefined") return;
2093
2086
  window.location.assign(target);
2094
2087
  }
2088
+ var SIGNIN_ERRORS = {
2089
+ bad_request: "That doesn't look like a Bitcoin address.",
2090
+ invalid_challenge: "That signature doesn't match this address. Sign with the wallet that holds it.",
2091
+ sig_invalid: "That signature doesn't match this address. Sign with the wallet that holds it.",
2092
+ sig_unsupported_scheme: "We can't check this wallet's signature format yet. Try another wallet, or use email.",
2093
+ expired: "That request timed out. Try again for a fresh one.",
2094
+ not_yet_valid: "That request is not valid yet. Check your device clock, then try again.",
2095
+ nonce_mismatch: "That request didn't match. Try again for a fresh one.",
2096
+ audience_mismatch: "That request didn't match this site. Reload the page and try again.",
2097
+ purpose_mismatch: "That request didn't match. Try again for a fresh one.",
2098
+ malformed: "That signature couldn't be read. Copy it again, whole.",
2099
+ cross_site_blocked: "Your browser blocked this request. Reload the page and try again.",
2100
+ rate_limited: "Too many tries. Wait a minute, then try again.",
2101
+ invalid_email: "That email address doesn't look right.",
2102
+ mailer_unavailable: "We couldn't send the email right now. Try again in a minute.",
2103
+ mailer_error: "We couldn't send the email right now. Try again in a minute.",
2104
+ wrong_code: "That code isn't right. Check the newest email from us.",
2105
+ replay: "That code was already used. Send a new one.",
2106
+ invalid_token: "That code is no longer valid. Send a new one.",
2107
+ email_mismatch: "That code was sent to a different address. Send a new one."
2108
+ };
2109
+ var EMAIL_ERRORS = {
2110
+ expired: "That code expired. Send a new one."
2111
+ };
2112
+ var SUBMIT_ERRORS = {
2113
+ bad_request: "That signature couldn't be read. Copy it again, whole."
2114
+ };
2115
+ var NETWORK_ERROR = "We couldn't reach the server. Check your connection and try again.";
2116
+ function humanSigninError(reason, path, step = "challenge") {
2117
+ if (!/^[a-z0-9_]+$/.test(reason)) return reason;
2118
+ if (path === "wallet" && step === "submit" && SUBMIT_ERRORS[reason]) return SUBMIT_ERRORS[reason];
2119
+ if (path === "email" && EMAIL_ERRORS[reason]) return EMAIL_ERRORS[reason];
2120
+ return SIGNIN_ERRORS[reason] ?? (path === "email" ? "That didn't work. Try again, or send a new code." : "That didn't work. Try again.");
2121
+ }
2122
+ function failureMessage(err, path, step) {
2123
+ if (err instanceof TypeError || err instanceof SyntaxError) return NETWORK_ERROR;
2124
+ return humanSigninError(err instanceof Error ? err.message : "failed", path, step);
2125
+ }
2126
+ var BECH32_ADDRESS = /^(bc1|tb1|bcrt1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{8,87}$/i;
2127
+ var BASE58_ADDRESS = /^[123mn][1-9A-HJ-NP-Za-km-z]{25,34}$/;
2128
+ function looksLikeBitcoinAddress(input) {
2129
+ const s = input.trim();
2130
+ return BECH32_ADDRESS.test(s) || BASE58_ADDRESS.test(s);
2131
+ }
2095
2132
  function OcSignIn({
2096
2133
  audience,
2097
2134
  returnTo,
@@ -2107,10 +2144,10 @@ function OcSignIn({
2107
2144
  }) {
2108
2145
  const walletEnabled = paths?.wallet ?? true;
2109
2146
  const emailEnabled = paths?.email ?? true;
2110
- const [resolvedReturn, setResolvedReturn] = React3__namespace.useState(
2147
+ const [resolvedReturn, setResolvedReturn] = React4__namespace.useState(
2111
2148
  () => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)
2112
2149
  );
2113
- React3__namespace.useEffect(() => {
2150
+ React4__namespace.useEffect(() => {
2114
2151
  const fromProp = familyReturnTarget(returnTo);
2115
2152
  if (fromProp) {
2116
2153
  setResolvedReturn(fromProp);
@@ -2121,8 +2158,8 @@ function OcSignIn({
2121
2158
  const fromQuery = familyReturnTarget(q.get("return_to")) ?? familyReturnTarget(q.get("next"));
2122
2159
  setResolvedReturn(fromQuery ?? "/");
2123
2160
  }, [returnTo]);
2124
- const [addMode, setAddMode] = React3__namespace.useState(Boolean(addProp));
2125
- React3__namespace.useEffect(() => {
2161
+ const [addMode, setAddMode] = React4__namespace.useState(Boolean(addProp));
2162
+ React4__namespace.useEffect(() => {
2126
2163
  if (typeof window === "undefined") return;
2127
2164
  if (addProp !== void 0) {
2128
2165
  setAddMode(Boolean(addProp));
@@ -2130,19 +2167,19 @@ function OcSignIn({
2130
2167
  }
2131
2168
  setAddMode(new URLSearchParams(window.location.search).get("add") === "1");
2132
2169
  }, [addProp]);
2133
- const [path, setPath] = React3__namespace.useState(
2170
+ const [path, setPath] = React4__namespace.useState(
2134
2171
  initialPath ?? (providersFirst ? "email" : "wallet")
2135
2172
  );
2136
- const [signedIn, setSignedIn] = React3__namespace.useState(null);
2137
- const [linkAlso, setLinkAlso] = React3__namespace.useState(false);
2138
- const [oauthError, setOauthError] = React3__namespace.useState(false);
2139
- React3__namespace.useEffect(() => {
2173
+ const [signedIn, setSignedIn] = React4__namespace.useState(null);
2174
+ const [linkAlso, setLinkAlso] = React4__namespace.useState(false);
2175
+ const [oauthError, setOauthError] = React4__namespace.useState(false);
2176
+ React4__namespace.useEffect(() => {
2140
2177
  if (typeof window === "undefined") return;
2141
2178
  if (new URLSearchParams(window.location.search).has("oauth_error")) {
2142
2179
  setOauthError(true);
2143
2180
  }
2144
2181
  }, []);
2145
- const navigate = React3__namespace.useCallback(
2182
+ const navigate = React4__namespace.useCallback(
2146
2183
  async (account) => {
2147
2184
  if (resolveReturnTo) {
2148
2185
  try {
@@ -2156,7 +2193,7 @@ function OcSignIn({
2156
2193
  },
2157
2194
  [resolveReturnTo, resolvedReturn]
2158
2195
  );
2159
- const handleSuccess = React3__namespace.useCallback(
2196
+ const handleSuccess = React4__namespace.useCallback(
2160
2197
  async (account, token, via) => {
2161
2198
  const proceed = () => {
2162
2199
  clearTabSession();
@@ -2242,7 +2279,7 @@ function OcSignIn({
2242
2279
  fontFamily: "ui-monospace, SFMono-Regular, monospace",
2243
2280
  fontSize: 11
2244
2281
  },
2245
- children: "That sign-in didn't complete. Please try again."
2282
+ children: "That sign-in didn't finish. Try again, or use another way below."
2246
2283
  }
2247
2284
  ),
2248
2285
  addMode && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2307,7 +2344,7 @@ function OcSignIn({
2307
2344
  {
2308
2345
  active: path === "email",
2309
2346
  onClick: () => setPath("email"),
2310
- children: "email + otp"
2347
+ children: "email"
2311
2348
  }
2312
2349
  ),
2313
2350
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2315,7 +2352,7 @@ function OcSignIn({
2315
2352
  {
2316
2353
  active: path === "wallet",
2317
2354
  onClick: () => setPath("wallet"),
2318
- children: "bitcoin \xB7 self-custody"
2355
+ children: "bitcoin wallet"
2319
2356
  }
2320
2357
  )
2321
2358
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -2332,7 +2369,7 @@ function OcSignIn({
2332
2369
  {
2333
2370
  active: path === "email",
2334
2371
  onClick: () => setPath("email"),
2335
- children: "email + otp"
2372
+ children: "email"
2336
2373
  }
2337
2374
  )
2338
2375
  ] })
@@ -2365,7 +2402,7 @@ function OcSignIn({
2365
2402
  type: "checkbox",
2366
2403
  checked: linkAlso,
2367
2404
  onChange: (e) => setLinkAlso(e.target.checked),
2368
- style: { marginTop: 2, accentColor: "var(--primary, #f97316)" }
2405
+ style: { margin: 0, accentColor: "var(--primary, #f97316)" }
2369
2406
  }
2370
2407
  ),
2371
2408
  /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
@@ -2373,7 +2410,7 @@ function OcSignIn({
2373
2410
  " ",
2374
2411
  activeMethod === "email" ? "a Bitcoin wallet" : "an email",
2375
2412
  " ",
2376
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { opacity: 0.65 }, children: "\u2014 optional, one more signature." })
2413
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u2014 optional, one more signature." })
2377
2414
  ] })
2378
2415
  ] })
2379
2416
  ] });
@@ -2392,6 +2429,7 @@ function SigninTab({
2392
2429
  onClick,
2393
2430
  "data-oc-signin-tab": active ? "active" : "inactive",
2394
2431
  style: {
2432
+ minHeight: 44,
2395
2433
  padding: "0.6rem 0.875rem",
2396
2434
  background: "transparent",
2397
2435
  border: "none",
@@ -2431,20 +2469,21 @@ function ProviderSignIn({
2431
2469
  add,
2432
2470
  first = false
2433
2471
  }) {
2434
- const [providers, setProviders] = React3__namespace.useState([]);
2435
- const [origin, setOrigin] = React3__namespace.useState("");
2436
- React3__namespace.useEffect(() => {
2472
+ const [providers, setProviders] = React4__namespace.useState(null);
2473
+ const [origin, setOrigin] = React4__namespace.useState("");
2474
+ React4__namespace.useEffect(() => {
2437
2475
  setOrigin(window.location.origin);
2438
2476
  let cancelled = false;
2439
2477
  fetch(`${authOrigin}/api/auth/providers`, { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((body) => {
2440
- if (!cancelled && body?.providers) setProviders(body.providers);
2478
+ if (!cancelled) setProviders(body?.providers ?? []);
2441
2479
  }).catch(() => {
2480
+ if (!cancelled) setProviders([]);
2442
2481
  });
2443
2482
  return () => {
2444
2483
  cancelled = true;
2445
2484
  };
2446
2485
  }, [authOrigin]);
2447
- if (providers.length === 0) return null;
2486
+ if (providers !== null && providers.length === 0) return null;
2448
2487
  const providerReturnTo = returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo;
2449
2488
  const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
2450
2489
  const BLOCK_GAP = 16;
@@ -2473,6 +2512,31 @@ function ProviderSignIn({
2473
2512
  ]
2474
2513
  }
2475
2514
  );
2515
+ const buttonStyle = (i) => ({
2516
+ display: "flex",
2517
+ alignItems: "center",
2518
+ justifyContent: "center",
2519
+ gap: 10,
2520
+ boxSizing: "border-box",
2521
+ width: "100%",
2522
+ minHeight: 44,
2523
+ marginTop: i === 0 ? 0 : BUTTON_GAP,
2524
+ padding: "0.6rem 0.875rem",
2525
+ border: "1px solid var(--border, #27272a)",
2526
+ borderRadius: 6,
2527
+ background: "transparent",
2528
+ color: "var(--muted-foreground, #a1a1aa)",
2529
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
2530
+ fontSize: 12,
2531
+ textDecoration: "none"
2532
+ });
2533
+ if (providers === null) {
2534
+ if (!first) return null;
2535
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-oc-signin-providers": "", "aria-hidden": "true", children: [
2536
+ [0, 1].map((i) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...buttonStyle(i), visibility: "hidden" }, children: "\xA0" }, i)),
2537
+ divider
2538
+ ] });
2539
+ }
2476
2540
  const buttons = providers.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs(
2477
2541
  "a",
2478
2542
  {
@@ -2480,23 +2544,7 @@ function ProviderSignIn({
2480
2544
  providerReturnTo
2481
2545
  )}${add ? "&add=1" : ""}`,
2482
2546
  "data-oc-signin-provider": p.id,
2483
- style: {
2484
- display: "flex",
2485
- alignItems: "center",
2486
- justifyContent: "center",
2487
- gap: 10,
2488
- boxSizing: "border-box",
2489
- width: "100%",
2490
- marginTop: i === 0 ? 0 : BUTTON_GAP,
2491
- padding: "0.6rem 0.875rem",
2492
- border: "1px solid var(--border, #27272a)",
2493
- borderRadius: 6,
2494
- background: "transparent",
2495
- color: "var(--muted-foreground, #a1a1aa)",
2496
- fontFamily: "ui-monospace, SFMono-Regular, monospace",
2497
- fontSize: 12,
2498
- textDecoration: "none"
2499
- },
2547
+ style: buttonStyle(i),
2500
2548
  children: [
2501
2549
  /* @__PURE__ */ jsxRuntime.jsx(ProviderIcon, { id: p.id }),
2502
2550
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: p.label })
@@ -2519,82 +2567,214 @@ function ProviderSignIn({
2519
2567
  }
2520
2568
  );
2521
2569
  }
2570
+ async function loadAdapter() {
2571
+ return await import('@orangecheck/wallet-adapter');
2572
+ }
2573
+ async function detectBrowserWallets() {
2574
+ return (await loadAdapter()).detectWallets().filter((w) => w.detected && w.id !== "manual").map((w) => ({ id: w.id, name: w.name }));
2575
+ }
2522
2576
  function WalletFlow({ authOrigin, audience, add, onSuccess }) {
2523
- const [address, setAddress] = React3__namespace.useState("");
2524
- const [error, setError] = React3__namespace.useState(null);
2525
- const [submitting, setSubmitting] = React3__namespace.useState(false);
2526
- const [stage, setStage] = React3__namespace.useState("enter");
2577
+ const uid = React4__namespace.useId();
2578
+ const ids = {
2579
+ address: `${uid}-address`,
2580
+ wallet: `${uid}-wallet`,
2581
+ message: `${uid}-message`,
2582
+ signature: `${uid}-signature`
2583
+ };
2584
+ const [address, setAddress] = React4__namespace.useState("");
2585
+ const [error, setError] = React4__namespace.useState(null);
2586
+ const [submitting, setSubmitting] = React4__namespace.useState(false);
2587
+ const [wallets, setWallets] = React4__namespace.useState(null);
2588
+ const [walletId, setWalletId] = React4__namespace.useState(null);
2589
+ const [manual, setManual] = React4__namespace.useState(null);
2590
+ const [pasted, setPasted] = React4__namespace.useState("");
2591
+ const [copied, setCopied] = React4__namespace.useState("idle");
2592
+ React4__namespace.useEffect(() => {
2593
+ let cancelled = false;
2594
+ detectBrowserWallets().then((found) => {
2595
+ if (cancelled) return;
2596
+ setWallets(found);
2597
+ setWalletId(found[0]?.id ?? null);
2598
+ }).catch(() => {
2599
+ if (!cancelled) setWallets([]);
2600
+ });
2601
+ return () => {
2602
+ cancelled = true;
2603
+ };
2604
+ }, []);
2605
+ async function fetchChallenge(addr) {
2606
+ const res = await fetch(
2607
+ `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
2608
+ );
2609
+ const json = await res.json();
2610
+ if (!res.ok || !json.message || !json.nonce) {
2611
+ throw new Error(json.error ?? "challenge_failed");
2612
+ }
2613
+ return { message: json.message, nonce: json.nonce };
2614
+ }
2615
+ async function submitSignature(message, nonce, signature, scheme) {
2616
+ const res = await fetch(`${authOrigin}/api/auth/signin`, {
2617
+ method: "POST",
2618
+ credentials: "include",
2619
+ headers: { "Content-Type": "application/json" },
2620
+ body: JSON.stringify({
2621
+ message,
2622
+ signature,
2623
+ ...scheme ? { scheme } : {},
2624
+ expectedNonce: nonce,
2625
+ expectedAudience: audience,
2626
+ expectedPurpose: "login",
2627
+ // Multi-account add-mode · the auth host preserves the current
2628
+ // roster_id when set, instead of minting a fresh one.
2629
+ ...add ? { add: true } : {}
2630
+ })
2631
+ });
2632
+ const json = await res.json();
2633
+ if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2634
+ throw new Error(
2635
+ "reason" in json && json.reason || "error" in json && json.error || "verify_failed"
2636
+ );
2637
+ }
2638
+ onSuccess(json.account, json.token);
2639
+ }
2527
2640
  async function signIn(e) {
2528
2641
  e.preventDefault();
2529
2642
  const addr = address.trim();
2530
2643
  if (!addr) {
2531
- setError("paste a Bitcoin address");
2644
+ setError("Paste your Bitcoin address.");
2645
+ return;
2646
+ }
2647
+ if (!looksLikeBitcoinAddress(addr)) {
2648
+ setError(humanSigninError("bad_request", "wallet"));
2532
2649
  return;
2533
2650
  }
2534
2651
  setError(null);
2535
2652
  setSubmitting(true);
2536
- setStage("signing");
2653
+ let step = "challenge";
2537
2654
  try {
2538
- const challengeRes = await fetch(
2539
- `${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
2540
- );
2541
- const challenge = await challengeRes.json();
2542
- if (!challengeRes.ok || !challenge.message || !challenge.nonce) {
2543
- throw new Error(challenge.error ?? "challenge_failed");
2655
+ let id = walletId;
2656
+ if (wallets === null) {
2657
+ const found = await detectBrowserWallets().catch(() => []);
2658
+ setWallets(found);
2659
+ id = found[0]?.id ?? null;
2660
+ setWalletId(id);
2544
2661
  }
2545
- let adapter;
2546
- try {
2547
- adapter = await import('@orangecheck/wallet-adapter');
2548
- } catch {
2549
- throw new Error(
2550
- "could not load wallet support \xB7 refresh to try again"
2551
- );
2552
- }
2553
- const wallets = adapter.detectWallets().filter((w) => w.detected && w.id !== "manual");
2554
- if (wallets.length === 0) {
2555
- throw new Error("no BIP-322 wallet extension detected \xB7 install one and refresh");
2662
+ const challenge = await fetchChallenge(addr);
2663
+ if (!id) {
2664
+ setManual(challenge);
2665
+ setSubmitting(false);
2666
+ return;
2556
2667
  }
2557
- const wallet = wallets[0];
2558
- const signer = adapter.getSigner(wallet.id, { address: addr });
2668
+ const signer = (await loadAdapter()).getSigner(id, { address: addr });
2559
2669
  const signature = await signer(challenge.message);
2560
- const res = await fetch(`${authOrigin}/api/auth/signin`, {
2561
- method: "POST",
2562
- credentials: "include",
2563
- headers: { "Content-Type": "application/json" },
2564
- body: JSON.stringify({
2565
- message: challenge.message,
2566
- signature,
2567
- scheme: "bip322",
2568
- expectedNonce: challenge.nonce,
2569
- expectedAudience: audience,
2570
- expectedPurpose: "login",
2571
- // Multi-account add-mode · the auth host preserves
2572
- // the current roster_id when set, instead of minting
2573
- // a fresh one. Hosts that haven't deployed the
2574
- // multi-account migration silently ignore the field.
2575
- ...add ? { add: true } : {}
2576
- })
2577
- });
2578
- const json = await res.json();
2579
- if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2580
- throw new Error(
2581
- "reason" in json && json.reason || "error" in json && json.error || `verify_failed_${res.status}`
2582
- );
2583
- }
2584
- onSuccess(json.account, json.token);
2670
+ step = "submit";
2671
+ await submitSignature(challenge.message, challenge.nonce, signature, "bip322");
2585
2672
  } catch (err) {
2586
- const msg = err instanceof Error ? err.message : "sign-in failed";
2587
- setError(msg);
2673
+ setError(failureMessage(err, "wallet", step));
2588
2674
  setSubmitting(false);
2589
- setStage("enter");
2590
2675
  }
2591
2676
  }
2677
+ async function signInManual(e) {
2678
+ e.preventDefault();
2679
+ if (!manual) return;
2680
+ const sig = pasted.trim();
2681
+ if (!sig) {
2682
+ setError("Paste the signature your wallet gave you.");
2683
+ return;
2684
+ }
2685
+ setError(null);
2686
+ setSubmitting(true);
2687
+ try {
2688
+ await submitSignature(manual.message, manual.nonce, sig, void 0);
2689
+ } catch (err) {
2690
+ setError(failureMessage(err, "wallet", "submit"));
2691
+ setSubmitting(false);
2692
+ }
2693
+ }
2694
+ async function copyMessage() {
2695
+ if (!manual) return;
2696
+ try {
2697
+ await navigator.clipboard.writeText(manual.message);
2698
+ setCopied("copied");
2699
+ } catch {
2700
+ setCopied("failed");
2701
+ }
2702
+ }
2703
+ if (manual) {
2704
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: signInManual, "data-oc-signin-wallet-manual": "", children: [
2705
+ /* @__PURE__ */ jsxRuntime.jsxs(FlowHeader, { children: [
2706
+ "In your wallet, use \u201Csign message\u201D (BIP-322) with ",
2707
+ address.trim(),
2708
+ ", sign the text below, and paste the signature back here. Nothing is spent."
2709
+ ] }),
2710
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: ids.message, children: "message to sign" }),
2711
+ /* @__PURE__ */ jsxRuntime.jsx(
2712
+ "textarea",
2713
+ {
2714
+ id: ids.message,
2715
+ readOnly: true,
2716
+ value: manual.message,
2717
+ rows: 5,
2718
+ onFocus: (e) => e.currentTarget.select(),
2719
+ style: { ...inputStyle2, resize: "vertical" }
2720
+ }
2721
+ ),
2722
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void copyMessage(), style: linkButtonStyle, children: copied === "copied" ? "copied" : "copy message" }),
2723
+ copied === "failed" && /* @__PURE__ */ jsxRuntime.jsx(Hint, { children: "Couldn't copy here. Select the text above and copy it." }),
2724
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 14 }, children: [
2725
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: ids.signature, children: "signature" }),
2726
+ /* @__PURE__ */ jsxRuntime.jsx(
2727
+ "textarea",
2728
+ {
2729
+ id: ids.signature,
2730
+ value: pasted,
2731
+ onChange: (e) => {
2732
+ setPasted(e.target.value);
2733
+ if (error) setError(null);
2734
+ },
2735
+ rows: 3,
2736
+ placeholder: "paste the signature",
2737
+ spellCheck: false,
2738
+ autoFocus: true,
2739
+ "aria-invalid": error ? true : void 0,
2740
+ style: { ...inputStyle2, resize: "vertical" }
2741
+ }
2742
+ )
2743
+ ] }),
2744
+ error && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine2, { children: error }),
2745
+ /* @__PURE__ */ jsxRuntime.jsx(
2746
+ "button",
2747
+ {
2748
+ type: "submit",
2749
+ disabled: submitting || !pasted.trim(),
2750
+ style: submitStyle(submitting || !pasted.trim()),
2751
+ children: submitting ? "checking\u2026" : "sign me in \u2192"
2752
+ }
2753
+ ),
2754
+ /* @__PURE__ */ jsxRuntime.jsx(
2755
+ "button",
2756
+ {
2757
+ type: "button",
2758
+ onClick: () => {
2759
+ setManual(null);
2760
+ setPasted("");
2761
+ setError(null);
2762
+ setCopied("idle");
2763
+ },
2764
+ style: linkButtonStyle,
2765
+ children: "use a different address"
2766
+ }
2767
+ )
2768
+ ] });
2769
+ }
2770
+ const noBrowserWallet = wallets !== null && wallets.length === 0;
2592
2771
  return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: signIn, "data-oc-signin-wallet": "", children: [
2593
- /* @__PURE__ */ jsxRuntime.jsx(FlowHeader, { label: "\xA7 bitcoin wallet", children: "Sign in with any BIP-322-capable Bitcoin wallet (Sparrow, Xverse, Leather, UniSat, Alby, OKX, Phantom). Paste your address, click sign \u2014 your wallet extension prompts for a one-time signature on a short challenge. The address becomes your OC identity. Private key material never leaves your wallet." }),
2594
- /* @__PURE__ */ jsxRuntime.jsx(Label, { children: "bitcoin address" }),
2772
+ /* @__PURE__ */ jsxRuntime.jsx(FlowHeader, { children: "Your wallet signs a short message to prove you hold the address. Nothing is spent and your key never leaves the wallet. Works with browser wallets, or any wallet that can sign a message, like Sparrow." }),
2773
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: ids.address, children: "bitcoin address" }),
2595
2774
  /* @__PURE__ */ jsxRuntime.jsx(
2596
2775
  "input",
2597
2776
  {
2777
+ id: ids.address,
2598
2778
  type: "text",
2599
2779
  value: address,
2600
2780
  onChange: (e) => {
@@ -2610,6 +2790,19 @@ function WalletFlow({ authOrigin, audience, add, onSuccess }) {
2610
2790
  style: inputStyle2
2611
2791
  }
2612
2792
  ),
2793
+ wallets && wallets.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 12 }, children: [
2794
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: ids.wallet, children: "sign with" }),
2795
+ /* @__PURE__ */ jsxRuntime.jsx(
2796
+ "select",
2797
+ {
2798
+ id: ids.wallet,
2799
+ value: walletId ?? "",
2800
+ onChange: (e) => setWalletId(e.target.value),
2801
+ style: inputStyle2,
2802
+ children: wallets.map((w) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: w.id, children: w.name }, w.id))
2803
+ }
2804
+ )
2805
+ ] }),
2613
2806
  error && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine2, { children: error }),
2614
2807
  /* @__PURE__ */ jsxRuntime.jsx(
2615
2808
  "button",
@@ -2617,54 +2810,71 @@ function WalletFlow({ authOrigin, audience, add, onSuccess }) {
2617
2810
  type: "submit",
2618
2811
  disabled: submitting || !address.trim(),
2619
2812
  style: submitStyle(submitting || !address.trim()),
2620
- children: stage === "signing" && submitting ? "waiting for wallet\u2026" : "sign challenge \xB7 sign me in \u2192"
2813
+ children: submitting ? "waiting for wallet\u2026" : noBrowserWallet ? "get the message to sign \u2192" : "sign in with wallet \u2192"
2621
2814
  }
2622
2815
  ),
2623
- /* @__PURE__ */ jsxRuntime.jsx(Hint, { children: "Detection picks the first installed BIP-322-capable extension. Address is the one your wallet will sign for." })
2816
+ noBrowserWallet && /* @__PURE__ */ jsxRuntime.jsx(Hint, { children: "No browser wallet found here. You can sign with any wallet and paste." })
2624
2817
  ] });
2625
2818
  }
2626
2819
  function EmailFlow({ authOrigin, add, onSuccess }) {
2627
- const [stage, setStage] = React3__namespace.useState("enter");
2628
- const [email, setEmail] = React3__namespace.useState("");
2629
- const [emailError, setEmailError] = React3__namespace.useState(null);
2630
- const [code, setCode] = React3__namespace.useState("");
2631
- const [codeError, setCodeError] = React3__namespace.useState(null);
2632
- const [token, setToken] = React3__namespace.useState(null);
2633
- const [submitting, setSubmitting] = React3__namespace.useState(false);
2634
- async function start(e) {
2635
- e.preventDefault();
2636
- const trimmed = email.trim().toLowerCase();
2637
- if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
2638
- setEmailError("enter a valid email");
2639
- return;
2640
- }
2641
- setEmailError(null);
2642
- setSubmitting(true);
2820
+ const uid = React4__namespace.useId();
2821
+ const [stage, setStage] = React4__namespace.useState("enter");
2822
+ const [email, setEmail] = React4__namespace.useState("");
2823
+ const [emailError, setEmailError] = React4__namespace.useState(null);
2824
+ const [code, setCode] = React4__namespace.useState("");
2825
+ const [codeError, setCodeError] = React4__namespace.useState(null);
2826
+ const [token, setToken] = React4__namespace.useState(null);
2827
+ const [submitting, setSubmitting] = React4__namespace.useState(false);
2828
+ const [resent, setResent] = React4__namespace.useState(false);
2829
+ async function sendCode(address) {
2643
2830
  try {
2644
2831
  const res = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
2645
2832
  method: "POST",
2646
2833
  credentials: "include",
2647
2834
  headers: { "Content-Type": "application/json" },
2648
- body: JSON.stringify({ email: trimmed })
2835
+ body: JSON.stringify({ email: address })
2649
2836
  });
2650
2837
  const json = await res.json();
2651
- if (!res.ok || !json.token) {
2652
- throw new Error(json.reason ?? `start failed (${res.status})`);
2653
- }
2838
+ if (!res.ok || !json.token) return humanSigninError(json.reason ?? "server_error", "email");
2654
2839
  setToken(json.token);
2655
- setEmail(trimmed);
2656
- setStage("code");
2657
- } catch (err) {
2658
- setEmailError(err instanceof Error ? err.message : "failed to send code");
2659
- } finally {
2660
- setSubmitting(false);
2840
+ return null;
2841
+ } catch {
2842
+ return NETWORK_ERROR;
2843
+ }
2844
+ }
2845
+ async function start(e) {
2846
+ e.preventDefault();
2847
+ const trimmed = email.trim().toLowerCase();
2848
+ if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
2849
+ setEmailError(humanSigninError("invalid_email", "email"));
2850
+ return;
2851
+ }
2852
+ setEmailError(null);
2853
+ setSubmitting(true);
2854
+ const err = await sendCode(trimmed);
2855
+ setSubmitting(false);
2856
+ if (err) {
2857
+ setEmailError(err);
2858
+ return;
2661
2859
  }
2860
+ setEmail(trimmed);
2861
+ setStage("code");
2862
+ }
2863
+ async function resend() {
2864
+ setCodeError(null);
2865
+ setResent(false);
2866
+ setSubmitting(true);
2867
+ const err = await sendCode(email);
2868
+ setSubmitting(false);
2869
+ setCode("");
2870
+ if (err) setCodeError(err);
2871
+ else setResent(true);
2662
2872
  }
2663
2873
  async function verify(e) {
2664
2874
  e.preventDefault();
2665
2875
  if (!token) return;
2666
2876
  if (code.length !== 6) {
2667
- setCodeError("6 digits");
2877
+ setCodeError("The code is 6 digits.");
2668
2878
  return;
2669
2879
  }
2670
2880
  setCodeError(null);
@@ -2685,23 +2895,24 @@ function EmailFlow({ authOrigin, add, onSuccess }) {
2685
2895
  const json = await res.json();
2686
2896
  if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
2687
2897
  throw new Error(
2688
- "reason" in json && json.reason || "error" in json && json.error || `verify failed (${res.status})`
2898
+ "reason" in json && json.reason || "error" in json && json.error || "server_error"
2689
2899
  );
2690
2900
  }
2691
2901
  onSuccess(json.account, json.token);
2692
2902
  } catch (err) {
2693
- setCodeError(err instanceof Error ? err.message : "verify failed");
2903
+ setCodeError(failureMessage(err, "email", "submit"));
2694
2904
  } finally {
2695
2905
  setSubmitting(false);
2696
2906
  }
2697
2907
  }
2698
2908
  if (stage === "enter") {
2699
2909
  return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: start, "data-oc-signin-email": "", children: [
2700
- /* @__PURE__ */ jsxRuntime.jsx(FlowHeader, { label: "\xA7 email + otp", children: "We email you a 6-digit code. No password, and no wallet needed to start. Link a Bitcoin address any time \u2014 that is the identity you hold yourself." }),
2701
- /* @__PURE__ */ jsxRuntime.jsx(Label, { children: "email" }),
2910
+ /* @__PURE__ */ jsxRuntime.jsx(FlowHeader, { children: "We email you a 6-digit code. No password, and no wallet needed to start. Link a Bitcoin address any time \u2014 that is the identity you hold yourself." }),
2911
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `${uid}-email`, children: "email" }),
2702
2912
  /* @__PURE__ */ jsxRuntime.jsx(
2703
2913
  "input",
2704
2914
  {
2915
+ id: `${uid}-email`,
2705
2916
  type: "email",
2706
2917
  autoComplete: "email",
2707
2918
  value: email,
@@ -2716,20 +2927,20 @@ function EmailFlow({ authOrigin, add, onSuccess }) {
2716
2927
  }
2717
2928
  ),
2718
2929
  emailError && /* @__PURE__ */ jsxRuntime.jsx(ErrorLine2, { children: emailError }),
2719
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" }),
2720
- /* @__PURE__ */ jsxRuntime.jsx(Hint, { children: "Codes come from the auth host, expire in 10 minutes, and work once." })
2930
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "sending\u2026" : "send one-time code \u2192" })
2721
2931
  ] });
2722
2932
  }
2723
2933
  return /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: verify, "data-oc-signin-email-verify": "", children: [
2724
- /* @__PURE__ */ jsxRuntime.jsxs(FlowHeader, { label: "\xA7 enter the code", children: [
2725
- "Sent a 6-digit code to ",
2934
+ /* @__PURE__ */ jsxRuntime.jsxs(FlowHeader, { children: [
2935
+ resent ? "Sent a new code to " : "Sent a 6-digit code to ",
2726
2936
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "var(--foreground, #fafafa)" }, children: email }),
2727
- ". Code expires in 10 minutes."
2937
+ ". It expires in 10 minutes."
2728
2938
  ] }),
2729
- /* @__PURE__ */ jsxRuntime.jsx(Label, { children: "one-time code" }),
2939
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: `${uid}-code`, children: "one-time code" }),
2730
2940
  /* @__PURE__ */ jsxRuntime.jsx(
2731
2941
  "input",
2732
2942
  {
2943
+ id: `${uid}-code`,
2733
2944
  type: "text",
2734
2945
  inputMode: "numeric",
2735
2946
  pattern: "\\d{6}",
@@ -2757,31 +2968,32 @@ function EmailFlow({ authOrigin, add, onSuccess }) {
2757
2968
  children: submitting ? "verifying\u2026" : "verify \xB7 sign me in \u2192"
2758
2969
  }
2759
2970
  ),
2760
- /* @__PURE__ */ jsxRuntime.jsx(
2761
- "button",
2762
- {
2763
- type: "button",
2764
- onClick: () => {
2765
- setStage("enter");
2766
- setCode("");
2767
- setToken(null);
2768
- },
2769
- style: {
2770
- marginTop: 12,
2771
- background: "transparent",
2772
- border: "none",
2773
- color: "var(--muted-foreground, #a1a1aa)",
2774
- fontFamily: "ui-monospace, SFMono-Regular, monospace",
2775
- fontSize: 11,
2776
- letterSpacing: "0.12em",
2777
- textTransform: "uppercase",
2778
- cursor: "pointer",
2779
- padding: 0
2780
- },
2781
- children: "use a different email"
2782
- }
2783
- ),
2784
- /* @__PURE__ */ jsxRuntime.jsx(Hint, { children: "On verify, oc_session is set on Domain=.ochk.io family-wide." })
2971
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2972
+ /* @__PURE__ */ jsxRuntime.jsx(
2973
+ "button",
2974
+ {
2975
+ type: "button",
2976
+ onClick: () => void resend(),
2977
+ disabled: submitting,
2978
+ style: linkButtonStyle,
2979
+ children: "send a new code"
2980
+ }
2981
+ ),
2982
+ /* @__PURE__ */ jsxRuntime.jsx(
2983
+ "button",
2984
+ {
2985
+ type: "button",
2986
+ onClick: () => {
2987
+ setStage("enter");
2988
+ setCode("");
2989
+ setToken(null);
2990
+ setResent(false);
2991
+ },
2992
+ style: linkButtonStyle,
2993
+ children: "use a different email"
2994
+ }
2995
+ )
2996
+ ] })
2785
2997
  ] });
2786
2998
  }
2787
2999
  function FlowHeader({
@@ -2789,7 +3001,7 @@ function FlowHeader({
2789
3001
  children
2790
3002
  }) {
2791
3003
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: 16 }, children: [
2792
- /* @__PURE__ */ jsxRuntime.jsx(
3004
+ label && /* @__PURE__ */ jsxRuntime.jsx(
2793
3005
  "div",
2794
3006
  {
2795
3007
  style: {
@@ -2817,10 +3029,30 @@ function FlowHeader({
2817
3029
  )
2818
3030
  ] });
2819
3031
  }
2820
- function Label({ children }) {
3032
+ var linkButtonStyle = {
3033
+ display: "inline-flex",
3034
+ alignItems: "center",
3035
+ minHeight: 44,
3036
+ marginTop: 4,
3037
+ marginRight: 16,
3038
+ background: "transparent",
3039
+ border: "none",
3040
+ color: "var(--muted-foreground, #a1a1aa)",
3041
+ fontFamily: "ui-monospace, SFMono-Regular, monospace",
3042
+ fontSize: 11,
3043
+ letterSpacing: "0.12em",
3044
+ textTransform: "uppercase",
3045
+ cursor: "pointer",
3046
+ padding: 0
3047
+ };
3048
+ function Label({
3049
+ htmlFor,
3050
+ children
3051
+ }) {
2821
3052
  return /* @__PURE__ */ jsxRuntime.jsx(
2822
3053
  "label",
2823
3054
  {
3055
+ htmlFor,
2824
3056
  style: {
2825
3057
  display: "block",
2826
3058
  color: "var(--foreground, #fafafa)",
@@ -2850,7 +3082,9 @@ var inputStyle2 = {
2850
3082
  };
2851
3083
  var linkAlsoStyle = {
2852
3084
  display: "flex",
2853
- alignItems: "flex-start",
3085
+ alignItems: "center",
3086
+ // The whole row is the checkbox's hit area; the 13px box alone was not.
3087
+ minHeight: 44,
2854
3088
  gap: 8,
2855
3089
  marginTop: 16,
2856
3090
  paddingTop: 14,
@@ -2906,8 +3140,7 @@ function Hint({ children }) {
2906
3140
  color: "var(--muted-foreground, #a1a1aa)",
2907
3141
  fontFamily: "ui-monospace, SFMono-Regular, monospace",
2908
3142
  fontSize: 10.5,
2909
- lineHeight: 1.55,
2910
- opacity: 0.7
3143
+ lineHeight: 1.55
2911
3144
  },
2912
3145
  children: [
2913
3146
  "> ",
@@ -2926,13 +3159,13 @@ function isAbortError(err) {
2926
3159
  }
2927
3160
  function useWebAuthnRegister(opts) {
2928
3161
  const authOrigin = resolveAuthOrigin(opts);
2929
- const [status, setStatus] = React3__namespace.useState("idle");
2930
- const [error, setError] = React3__namespace.useState(null);
2931
- const reset = React3__namespace.useCallback(() => {
3162
+ const [status, setStatus] = React4__namespace.useState("idle");
3163
+ const [error, setError] = React4__namespace.useState(null);
3164
+ const reset = React4__namespace.useCallback(() => {
2932
3165
  setStatus("idle");
2933
3166
  setError(null);
2934
3167
  }, []);
2935
- const register = React3__namespace.useCallback(
3168
+ const register = React4__namespace.useCallback(
2936
3169
  async (args) => {
2937
3170
  setError(null);
2938
3171
  setStatus("requesting-options");
@@ -2994,10 +3227,10 @@ function useWebAuthnRegister(opts) {
2994
3227
  function useWebAuthnList(opts) {
2995
3228
  const authOrigin = resolveAuthOrigin(opts);
2996
3229
  const { status: sessionStatus } = useOcSession();
2997
- const [credentials, setCredentials] = React3__namespace.useState([]);
2998
- const [status, setStatus] = React3__namespace.useState("loading");
2999
- const [error, setError] = React3__namespace.useState(null);
3000
- const refetch = React3__namespace.useCallback(async () => {
3230
+ const [credentials, setCredentials] = React4__namespace.useState([]);
3231
+ const [status, setStatus] = React4__namespace.useState("loading");
3232
+ const [error, setError] = React4__namespace.useState(null);
3233
+ const refetch = React4__namespace.useCallback(async () => {
3001
3234
  setError(null);
3002
3235
  if (sessionStatus !== "authenticated") {
3003
3236
  setCredentials([]);
@@ -3025,10 +3258,10 @@ function useWebAuthnList(opts) {
3025
3258
  setStatus("error");
3026
3259
  }
3027
3260
  }, [authOrigin, sessionStatus]);
3028
- React3__namespace.useEffect(() => {
3261
+ React4__namespace.useEffect(() => {
3029
3262
  void refetch();
3030
3263
  }, [refetch]);
3031
- const rename = React3__namespace.useCallback(
3264
+ const rename = React4__namespace.useCallback(
3032
3265
  async (id, label) => {
3033
3266
  try {
3034
3267
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -3053,7 +3286,7 @@ function useWebAuthnList(opts) {
3053
3286
  },
3054
3287
  [authOrigin, refetch]
3055
3288
  );
3056
- const remove = React3__namespace.useCallback(
3289
+ const remove = React4__namespace.useCallback(
3057
3290
  async (id) => {
3058
3291
  try {
3059
3292
  const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
@@ -3081,13 +3314,13 @@ function useWebAuthnList(opts) {
3081
3314
  function useStepUpAuth(opts) {
3082
3315
  const authOrigin = resolveAuthOrigin(opts);
3083
3316
  const { refresh } = useOcSession();
3084
- const [status, setStatus] = React3__namespace.useState("idle");
3085
- const [error, setError] = React3__namespace.useState(null);
3086
- const reset = React3__namespace.useCallback(() => {
3317
+ const [status, setStatus] = React4__namespace.useState("idle");
3318
+ const [error, setError] = React4__namespace.useState(null);
3319
+ const reset = React4__namespace.useCallback(() => {
3087
3320
  setStatus("idle");
3088
3321
  setError(null);
3089
3322
  }, []);
3090
- const stepUp = React3__namespace.useCallback(
3323
+ const stepUp = React4__namespace.useCallback(
3091
3324
  async (args) => {
3092
3325
  setError(null);
3093
3326
  setStatus("requesting-options");