@bwalletx/connect 0.5.4 → 0.5.6

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +76 -8
  3. package/dist/bwalletx-connect.global.js +92 -25
  4. package/dist/bwalletx-connect.global.js.map +1 -1
  5. package/dist/{wallets-CbzMtxrf.d.ts → choice-DCh-tgpT.d.cts} +112 -4
  6. package/dist/{wallets-CbzMtxrf.d.cts → choice-DCh-tgpT.d.ts} +112 -4
  7. package/dist/chunk-34DKBQNN.js +222 -0
  8. package/dist/chunk-34DKBQNN.js.map +1 -0
  9. package/dist/{chunk-QRXFL66W.js → chunk-A2WLGS25.js} +6 -4
  10. package/dist/chunk-A2WLGS25.js.map +1 -0
  11. package/dist/chunk-IFGOAROM.js +255 -0
  12. package/dist/chunk-IFGOAROM.js.map +1 -0
  13. package/dist/chunk-LS6AYEBO.js +1886 -0
  14. package/dist/chunk-LS6AYEBO.js.map +1 -0
  15. package/dist/element-D9Zl-x12.d.cts +46 -0
  16. package/dist/element-D9Zl-x12.d.ts +46 -0
  17. package/dist/element.cjs +1147 -61
  18. package/dist/element.cjs.map +1 -1
  19. package/dist/element.d.cts +23 -28
  20. package/dist/element.d.ts +23 -28
  21. package/dist/element.js +29 -2
  22. package/dist/element.js.map +1 -1
  23. package/dist/index.cjs +1469 -453
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +163 -14
  26. package/dist/index.d.ts +163 -14
  27. package/dist/index.js +100 -12
  28. package/dist/index.js.map +1 -1
  29. package/dist/next.cjs +5 -3
  30. package/dist/next.cjs.map +1 -1
  31. package/dist/next.js +1 -1
  32. package/dist/react.cjs +994 -183
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +75 -8
  35. package/dist/react.d.ts +75 -8
  36. package/dist/react.js +121 -40
  37. package/dist/react.js.map +1 -1
  38. package/dist/server.cjs +5 -3
  39. package/dist/server.cjs.map +1 -1
  40. package/dist/server.d.cts +8 -3
  41. package/dist/server.d.ts +8 -3
  42. package/dist/server.js +1 -1
  43. package/package.json +10 -9
  44. package/dist/chunk-EXRQJTXF.js +0 -910
  45. package/dist/chunk-EXRQJTXF.js.map +0 -1
  46. package/dist/chunk-QRXFL66W.js.map +0 -1
  47. package/dist/chunk-RIOHG5JH.js +0 -459
  48. package/dist/chunk-RIOHG5JH.js.map +0 -1
package/dist/react.cjs CHANGED
@@ -34,12 +34,16 @@ __export(react_exports, {
34
34
  BwalletxProvider: () => BwalletxProvider,
35
35
  BwalletxSignin: () => BwalletxSignin,
36
36
  PaymentError: () => PaymentError,
37
+ accountFor: () => accountFor,
38
+ autoChoice: () => autoChoice,
37
39
  brc100Option: () => brc100Option,
38
40
  default: () => react_default,
39
41
  discoverWallets: () => discoverWallets,
40
42
  handcashAdapter: () => handcashAdapter,
41
43
  pairingOption: () => pairingOption,
42
- useBwalletX: () => useBwalletX
44
+ signinStateFor: () => signinStateFor,
45
+ useBwalletX: () => useBwalletX,
46
+ useBwalletXAccount: () => useBwalletXAccount
43
47
  });
44
48
  module.exports = __toCommonJS(react_exports);
45
49
  var import_react = require("react");
@@ -446,6 +450,73 @@ function declareWalletTheme(theme) {
446
450
  if (meta.content !== id) meta.content = id;
447
451
  }
448
452
 
453
+ // src/account.ts
454
+ var PAYMAIL_LOOKUP_URL = "https://pay.bwallet.space/api/paymail/lookup?key=";
455
+ var BAVATAR_URL = "https://chat.bwalletx.com/api/bavatar/";
456
+ function shortKey(key) {
457
+ return key.length > 12 ? `${key.slice(0, 6)}\u2026${key.slice(-4)}` : key;
458
+ }
459
+ function accountLabel(a) {
460
+ if (a.paymail && /@bwalletx\.com$/i.test(a.paymail)) return "$" + a.paymail.split("@")[0];
461
+ if (a.handle) return a.handle;
462
+ if (a.paymail) return a.paymail;
463
+ return a.identityKey ? shortKey(a.identityKey) : "";
464
+ }
465
+ function avatarUrlFor(identityKey) {
466
+ return identityKey && /^0[23][0-9a-fA-F]{64}$/.test(identityKey) ? `${BAVATAR_URL}${identityKey.toLowerCase()}.png` : null;
467
+ }
468
+ var cache = /* @__PURE__ */ new Map();
469
+ function lookupPaymail(identityKey, url = PAYMAIL_LOOKUP_URL) {
470
+ if (url === "off" || typeof fetch === "undefined") return Promise.resolve(null);
471
+ const k = `${url}|${identityKey}`;
472
+ let p = cache.get(k);
473
+ if (!p) {
474
+ p = fetch(url + encodeURIComponent(identityKey)).then((r) => r.ok ? r.json() : null).then((j) => j && typeof j.paymail === "string" ? j.paymail : null).catch(() => null);
475
+ cache.set(k, p);
476
+ }
477
+ return p;
478
+ }
479
+ function isUserRejection(e) {
480
+ const code = e?.code;
481
+ if (code === "ERR_PERMISSION_DENIED" || code === 4001) return true;
482
+ const msg = e instanceof Error ? e.message : String(e ?? "");
483
+ return /denied|reject|cancel|declin|refused|not now|user closed/i.test(msg);
484
+ }
485
+ function signInErrorText(e) {
486
+ if (isUserRejection(e)) return "Sign-in cancelled";
487
+ const code = e?.code;
488
+ if (code === "NOT_FOUND") return "bWalletX was not found in this browser";
489
+ const msg = e instanceof Error ? e.message : String(e ?? "");
490
+ if (/lock/i.test(msg)) return "Your wallet is locked. Unlock it and try again";
491
+ if (/timeout|timed out|expired/i.test(msg)) return "The wallet did not answer in time";
492
+ if (/origin/i.test(msg)) return "This page is not on the site\u2019s configured origin";
493
+ return "Sign-in failed";
494
+ }
495
+ async function copyText(text) {
496
+ try {
497
+ await navigator.clipboard.writeText(text);
498
+ return true;
499
+ } catch {
500
+ return false;
501
+ }
502
+ }
503
+ var IDENTITY_HINT_KEY = "bwx:identityKey";
504
+ function rememberedIdentity() {
505
+ try {
506
+ const k = globalThis.localStorage?.getItem(IDENTITY_HINT_KEY) ?? "";
507
+ return /^0[23][0-9a-f]{64}$/i.test(k) ? k : void 0;
508
+ } catch {
509
+ return void 0;
510
+ }
511
+ }
512
+ function rememberIdentity(key) {
513
+ try {
514
+ if (key) globalThis.localStorage?.setItem(IDENTITY_HINT_KEY, key);
515
+ else globalThis.localStorage?.removeItem(IDENTITY_HINT_KEY);
516
+ } catch {
517
+ }
518
+ }
519
+
449
520
  // src/discover.ts
450
521
  var import_sdk3 = require("@bsv/sdk");
451
522
  var RDNS_EXTENSION = "com.bwalletx.extension";
@@ -471,21 +542,16 @@ function discoverBwalletX(ms = DEFAULT_DISCOVER_MS) {
471
542
  finish({ wallet: d.wallet, method: rdns === RDNS_EXTENSION ? "extension" : "in-app", rdns });
472
543
  }
473
544
  };
474
- const cwiNow = () => {
475
- const cwi = window.CWI;
476
- if (cwi) finish({ wallet: cwi, method: "in-app", rdns: null });
477
- return !!cwi;
478
- };
479
545
  window.addEventListener("brc100:announceWallet", onAnnounce);
480
546
  window.dispatchEvent(new Event("brc100:requestWallet"));
481
- if (done || cwiNow()) return;
547
+ if (done) return;
482
548
  if (isFramed()) {
483
549
  void probeXdm(Math.max(ms, 1200)).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
484
550
  return;
485
551
  }
486
552
  timer = setTimeout(() => {
487
553
  timer = void 0;
488
- if (!cwiNow()) finish(null);
554
+ finish(null);
489
555
  }, ms);
490
556
  });
491
557
  }
@@ -589,27 +655,68 @@ async function connectBwalletX(opts = {}) {
589
655
  status("pairing");
590
656
  return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
591
657
  }
658
+ async function waitIfLocked(wallet) {
659
+ let ok = false;
660
+ try {
661
+ ok = !!(await wallet.isAuthenticated({}))?.authenticated;
662
+ } catch {
663
+ }
664
+ if (!ok) await Promise.resolve().then(() => wallet.waitForAuthentication({})).catch(() => {
665
+ });
666
+ }
667
+ function challengeWantsIdentityKey(e) {
668
+ const msg = e instanceof Error ? e.message : String(e ?? "");
669
+ return /identityKey/i.test(msg);
670
+ }
592
671
  async function signInWithBwalletX(opts) {
593
672
  const { wallet, method } = await connectBwalletX(opts);
594
- if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
673
+ if (method !== "pairing" && !opts.skipWaitForAuthentication) await waitIfLocked(wallet);
674
+ const status = () => {
675
+ try {
676
+ opts.onStatus?.("signing");
677
+ } catch {
678
+ }
679
+ };
680
+ const sign = (c2) => wallet.createSignature({
681
+ protocolID: c2.protocolID ?? LOGIN_PROTOCOL,
682
+ keyID: c2.keyID,
683
+ counterparty: "anyone",
684
+ data: Array.from(new TextEncoder().encode(c2.message))
595
685
  });
596
- const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
686
+ const keyFirst = async () => {
687
+ const { publicKey: identityKey2 } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
688
+ status();
689
+ const c2 = await atStage("challenge", () => opts.challenge(identityKey2));
690
+ const { signature: signature2 } = await atStage("signature", () => sign(c2));
691
+ return { identityKey: identityKey2, signature: Array.from(signature2), nonce: c2.nonce, method, wallet };
692
+ };
693
+ if (opts.challengeNeedsIdentityKey && !opts.identityKey) return keyFirst();
694
+ let c;
597
695
  try {
598
- opts.onStatus?.("signing");
599
- } catch {
696
+ c = await atStage("challenge", () => opts.challenge(opts.identityKey));
697
+ } catch (e) {
698
+ if (!opts.identityKey && challengeWantsIdentityKey(e)) return keyFirst();
699
+ throw e;
600
700
  }
601
- const c = await atStage("challenge", () => opts.challenge(identityKey));
602
- const { signature } = await atStage(
603
- "signature",
604
- () => wallet.createSignature({
605
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
606
- keyID: c.keyID,
607
- counterparty: "anyone",
608
- data: Array.from(new TextEncoder().encode(c.message))
609
- })
610
- );
701
+ status();
702
+ const [{ publicKey: identityKey }, { signature }] = await Promise.all([
703
+ opts.identityKey ? Promise.resolve({ publicKey: opts.identityKey }) : atStage("identity", () => wallet.getPublicKey({ identityKey: true })),
704
+ atStage("signature", () => sign(c))
705
+ ]);
611
706
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
612
707
  }
708
+ async function signInAndVerify(opts) {
709
+ const hinted = !opts.identityKey && !!opts.identityKeyHint;
710
+ const r = await signInWithBwalletX({ ...opts, identityKey: opts.identityKey ?? opts.identityKeyHint });
711
+ try {
712
+ return { result: r, verified: await atStage("verify", () => opts.verify(r)) };
713
+ } catch (e) {
714
+ const status = e?.status;
715
+ if (!hinted || status !== 401) throw e;
716
+ const again = await signInWithBwalletX({ ...opts, wallet: r.wallet, identityKey: void 0, skipWaitForAuthentication: true });
717
+ return { result: again, verified: await atStage("verify", () => opts.verify(again)) };
718
+ }
719
+ }
613
720
  async function atStage(stage, run) {
614
721
  try {
615
722
  return await run();
@@ -620,11 +727,272 @@ async function atStage(stage, run) {
620
727
  }
621
728
  }
622
729
 
730
+ // src/wallets.ts
731
+ var import_sdk4 = require("@bsv/sdk");
732
+ async function brc100Connected(wallet, o, c = {}) {
733
+ if (c.identity === false) return { wallet, payer: wallet, identityKey: null, optionId: o.id, kind: o.kind };
734
+ await waitIfLocked(wallet);
735
+ const { publicKey } = await wallet.getPublicKey({ identityKey: true });
736
+ return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
737
+ }
738
+ function brc100Option(wallet, info) {
739
+ const opt = {
740
+ id: info.id,
741
+ name: info.name,
742
+ icon: info.icon,
743
+ rdns: info.rdns,
744
+ kind: "brc100",
745
+ isBwalletX: info.isBwalletX ?? false,
746
+ substrate: info.substrate,
747
+ wallet,
748
+ connect: (c) => brc100Connected(wallet, opt, c)
749
+ };
750
+ return opt;
751
+ }
752
+ function pairingOption(pairing) {
753
+ const opt = {
754
+ id: "bwalletx-pairing",
755
+ name: "bWalletX on your phone",
756
+ kind: "pairing",
757
+ isBwalletX: true,
758
+ substrate: "pairing",
759
+ async connect(c) {
760
+ const p = typeof pairing === "function" ? pairing() : pairing;
761
+ const w = await restorePairing(p) ?? await pairBwalletX(p);
762
+ return brc100Connected(w, opt, c);
763
+ }
764
+ };
765
+ return opt;
766
+ }
767
+ function handcashAdapter(o) {
768
+ const opt = {
769
+ id: "handcash",
770
+ name: "HandCash",
771
+ icon: o.icon,
772
+ kind: "handcash",
773
+ isBwalletX: false,
774
+ substrate: "adapter",
775
+ async connect() {
776
+ const p = await o.login();
777
+ return {
778
+ wallet: null,
779
+ payer: o.pay ? { pay: o.pay } : null,
780
+ identityKey: p.identityKey ?? null,
781
+ handle: p.handle,
782
+ paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
783
+ optionId: opt.id,
784
+ kind: "handcash",
785
+ transitional: true
786
+ };
787
+ }
788
+ };
789
+ return opt;
790
+ }
791
+ var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
792
+ async function probe(substrate, ms) {
793
+ try {
794
+ const w = new import_sdk4.WalletClient(substrate);
795
+ const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
796
+ return ok ? w : null;
797
+ } catch {
798
+ return null;
799
+ }
800
+ }
801
+ function cwiIsAnnounced(cwi, announced) {
802
+ return announced.some((o) => o.wallet === cwi || o.rdns === RDNS_MOBILE);
803
+ }
804
+ async function discoverWallets(o = {}) {
805
+ const out = [];
806
+ const ms = o.ms ?? 300;
807
+ if (typeof window !== "undefined") {
808
+ const seen = /* @__PURE__ */ new Map();
809
+ const onAnnounce = (e) => {
810
+ const d = e.detail;
811
+ if (!d?.wallet) return;
812
+ const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
813
+ if (seen.has(rdns)) return;
814
+ const isB = BWALLETX_RDNS.includes(rdns);
815
+ const substrate = rdns === RDNS_EXTENSION ? "extension" : rdns === RDNS_MOBILE ? "in-app" : "injected";
816
+ seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB, substrate }));
817
+ };
818
+ window.addEventListener("brc100:announceWallet", onAnnounce);
819
+ window.dispatchEvent(new Event("brc100:requestWallet"));
820
+ const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
821
+ await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
822
+ window.removeEventListener("brc100:announceWallet", onAnnounce);
823
+ out.push(...seen.values());
824
+ if (![...seen.values()].some((w) => w.isBwalletX) && isFramed()) {
825
+ const x = await probeXdm(Math.max(ms, 1200));
826
+ if (x) out.push(brc100Option(x, { id: "bwalletx-xdm", name: "bWalletX", isBwalletX: true, substrate: "xdm" }));
827
+ }
828
+ const cwi = window.CWI;
829
+ if (cwi && !cwiIsAnnounced(cwi, [...seen.values()])) {
830
+ out.push(brc100Option(cwi, { id: "window.CWI", name: seen.size ? "Another browser wallet" : "Browser wallet", isBwalletX: false, substrate: "window.CWI" }));
831
+ }
832
+ const [json, cicada] = await local;
833
+ if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)", substrate: "json-api" }));
834
+ if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)", substrate: "cicada" }));
835
+ }
836
+ if (o.pairing) out.push(pairingOption(o.pairing));
837
+ out.push(...o.extra ?? []);
838
+ const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
839
+ return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
840
+ }
841
+ var DESKTOP_OPTION_ID = "desktop";
842
+ function desktopOption(ms = 1500) {
843
+ const opt = {
844
+ id: DESKTOP_OPTION_ID,
845
+ name: "Desktop wallet",
846
+ kind: "brc100",
847
+ isBwalletX: false,
848
+ substrate: "json-api",
849
+ async connect(c) {
850
+ const [json, cicada] = await Promise.all([probe("json-api", ms), probe("Cicada", ms)]);
851
+ const w = json ?? cicada;
852
+ if (!w) throw Object.assign(new Error("No desktop wallet answered on this computer"), { code: "NOT_FOUND" });
853
+ opt.wallet = w;
854
+ opt.substrate = json ? "json-api" : "cicada";
855
+ return brc100Connected(w, opt, c);
856
+ }
857
+ };
858
+ return opt;
859
+ }
860
+ function autoChoice(list, force = false) {
861
+ const detected = list.filter((w) => w.kind === "brc100");
862
+ const others = list.filter((w) => w.kind === "handcash" || w.kind === "custom");
863
+ if (force) return detected[0] ?? list.find((w) => w.kind !== "pairing") ?? list[0] ?? null;
864
+ if (!list.length) return null;
865
+ if (list.length === 1) return list[0];
866
+ if (detected.length === 1 && others.length === 0) return detected[0];
867
+ if (detected.length === 0 && others.length === 0) return list[0];
868
+ return void 0;
869
+ }
870
+
871
+ // src/choice.ts
872
+ var BWALLETX_TYPE = "bwalletx";
873
+ function substrateOf(o) {
874
+ if (o.substrate) return o.substrate;
875
+ if (o.kind === "pairing") return "pairing";
876
+ if (o.isBwalletX && o.kind === "brc100") return o.rdns === RDNS_MOBILE ? "in-app" : o.id === "bwalletx-xdm" ? "xdm" : "extension";
877
+ if (o.kind === "handcash" || o.kind === "custom") return "adapter";
878
+ if (o.rdns === RDNS_EXTENSION) return "extension";
879
+ if (o.rdns === RDNS_MOBILE) return "in-app";
880
+ if (o.id === "window.CWI") return "window.CWI";
881
+ if (o.id === "localhost:3321" || o.id === DESKTOP_OPTION_ID) return "json-api";
882
+ if (o.id === "localhost:3301") return "cicada";
883
+ return "injected";
884
+ }
885
+ function walletTypeOf(o) {
886
+ return o.isBwalletX ? BWALLETX_TYPE : o.rdns ?? o.id;
887
+ }
888
+ var OWN_MARKERS = ["extension", "in-app", "xdm"];
889
+ function isPresentBwalletX(o) {
890
+ return o.isBwalletX && o.kind === "brc100" && !!o.wallet && OWN_MARKERS.includes(substrateOf(o));
891
+ }
892
+ function hasBwalletX(list) {
893
+ return list.some(isPresentBwalletX);
894
+ }
895
+ function otherDetected(list) {
896
+ return list.filter((o) => !o.isBwalletX && o.kind === "brc100" && !!o.wallet);
897
+ }
898
+ function chooserOrder(list) {
899
+ const rank = (o) => isPresentBwalletX(o) ? 0 : o.isBwalletX ? 1 : o.kind === "brc100" ? 2 : o.kind === "custom" ? 3 : 4;
900
+ return list.map((o, i) => ({ o, i })).sort((a, b) => rank(a.o) - rank(b.o) || a.i - b.i).map((x) => x.o);
901
+ }
902
+ function decideWallet(list, o = {}) {
903
+ if (o.walletId) {
904
+ const w = list.find((x) => x.id === o.walletId);
905
+ if (w) return { option: w, reason: "requested" };
906
+ }
907
+ if (o.chooser) return { option: null, reason: list.length || o.picker !== false ? "chooser" : "none" };
908
+ if (o.fastPath === false) {
909
+ const a = autoChoice([...list], o.picker === false);
910
+ return a === void 0 ? { option: null, reason: "chooser" } : a === null ? { option: null, reason: "none" } : { option: a, reason: "legacy" };
911
+ }
912
+ const b = list.find(isPresentBwalletX);
913
+ if (b) return { option: b, reason: "bwalletx" };
914
+ if (o.picker === false) {
915
+ const phone = list.find((x) => x.isBwalletX);
916
+ return phone ? { option: phone, reason: "bwalletx-phone" } : { option: null, reason: "none" };
917
+ }
918
+ return list.length ? { option: null, reason: "chooser" } : { option: null, reason: "none" };
919
+ }
920
+ function outcomeOf(e) {
921
+ const err = e;
922
+ if (err?.code === "NOT_FOUND") return "not-found";
923
+ if (err?.name === "AbortError" || err?.message === "cancelled") return "cancelled";
924
+ if (isUserRejection(e)) return "rejected";
925
+ return "error";
926
+ }
927
+ function attemptFor(option, outcome, startedAt, now = Date.now()) {
928
+ return {
929
+ walletType: option ? walletTypeOf(option) : null,
930
+ substrate: option ? substrateOf(option) : null,
931
+ outcome,
932
+ ms: Math.max(0, Math.round(now - startedAt))
933
+ };
934
+ }
935
+ function reportAttempt(hook, a) {
936
+ if (!hook) return;
937
+ try {
938
+ hook(Object.freeze({ walletType: a.walletType, substrate: a.substrate, outcome: a.outcome, ms: a.ms }));
939
+ } catch {
940
+ }
941
+ }
942
+
943
+ // src/connect-flow.ts
944
+ async function runConnect(o) {
945
+ const started = Date.now();
946
+ const { list } = o;
947
+ let choice = null;
948
+ let reason = "none";
949
+ try {
950
+ const d = decideWallet(list, { walletId: o.walletId, chooser: o.chooser, picker: o.picker, fastPath: o.fastPath });
951
+ choice = d.option;
952
+ reason = d.reason;
953
+ if (!choice && d.reason === "chooser") choice = await o.pick(list);
954
+ if (!choice) {
955
+ if (d.reason === "none" || !list.length) throw Object.assign(new Error("No wallet found. Get bWalletX at bwalletx.com."), { code: "NOT_FOUND" });
956
+ reportAttempt(o.onAttempt, attemptFor(null, "cancelled", started));
957
+ return null;
958
+ }
959
+ const signIn = !!(o.challengeUrl && o.verifyUrl) && choice.kind !== "handcash";
960
+ const cw = await choice.connect(signIn ? { identity: false } : void 0);
961
+ let handle = cw.handle ?? null;
962
+ let paymail = cw.paymail ?? null;
963
+ let identityKey = cw.identityKey;
964
+ let signedIn = false;
965
+ if (cw.wallet && signIn) {
966
+ const { result, verified } = await signInAndVerify({
967
+ wallet: cw.wallet,
968
+ identityKey: cw.identityKey ?? void 0,
969
+ // The key that signed in here last time: a repeat sign-in is then the signature alone.
970
+ identityKeyHint: rememberedIdentity(),
971
+ challengeNeedsIdentityKey: o.challengeNeedsIdentityKey,
972
+ challenge: (key) => o.post(o.challengeUrl, key ? { identityKey: key } : {}),
973
+ verify: (r) => o.post(o.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature })
974
+ });
975
+ rememberIdentity(result.identityKey);
976
+ identityKey = result.identityKey;
977
+ handle = verified.handle ?? handle;
978
+ paymail = verified.paymail ?? paymail;
979
+ signedIn = true;
980
+ } else if (cw.wallet && !identityKey) {
981
+ identityKey = (await cw.wallet.getPublicKey({ identityKey: true })).publicKey;
982
+ }
983
+ reportAttempt(o.onAttempt, attemptFor(choice, signedIn ? "signed-in" : "connected", started));
984
+ return { connected: cw, identityKey, handle, paymail, signedIn, walletType: walletTypeOf(choice), substrate: substrateOf(choice), reason };
985
+ } catch (e) {
986
+ reportAttempt(o.onAttempt, attemptFor(choice, outcomeOf(e), started));
987
+ throw e;
988
+ }
989
+ }
990
+
623
991
  // src/modal.ts
624
992
  var import_qrcode_generator = __toESM(require("qrcode-generator"), 1);
625
993
 
626
994
  // src/access-client.ts
627
- var import_sdk4 = require("@bsv/sdk");
995
+ var import_sdk5 = require("@bsv/sdk");
628
996
 
629
997
  // src/access.ts
630
998
  var ACCESS_PROTOCOL = [2, "bwalletx access"];
@@ -762,7 +1130,7 @@ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
762
1130
  const counterparty = ci.counterparty ?? "self";
763
1131
  for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
764
1132
  const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
765
- const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
1133
+ const h = import_sdk5.Utils.toHex(import_sdk5.Hash.hash160(import_sdk5.PublicKey.fromString(publicKey).encode(true)));
766
1134
  if (want && h !== want) continue;
767
1135
  const existing = byKey.get(publicKey);
768
1136
  if (existing) {
@@ -812,7 +1180,7 @@ async function requestAccess(o) {
812
1180
  var SIGNIN_TAG = "bwalletx-signin";
813
1181
  var GET_URL = "https://bwalletx.com/get";
814
1182
  var WEB_WALLET_URL = "https://web.bwalletx.com";
815
- var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
1183
+ var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect", get: "Get", play: "Play with" };
816
1184
  function signinText(label = "signin", brand = "bWalletX") {
817
1185
  return `${VERBS[label] ?? VERBS.signin} ${brand}`;
818
1186
  }
@@ -867,7 +1235,50 @@ button[aria-busy=true]{cursor:wait}
867
1235
  .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
868
1236
  :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
869
1237
  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
1238
+ /* pending: one plain line under the button */
1239
+ .note{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center;overflow-wrap:break-word}
1240
+ :host([theme=dark]) .note,:host-context(.dark) .note{color:#a1a1aa}
1241
+ /* "Other wallets" (0.5.6): one small link under the button */
1242
+ .ow{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
1243
+ :host([theme=dark]) .ow,:host-context(.dark) .ow{color:#a1a1aa}
1244
+ .lnk{all:unset;cursor:pointer;color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
1245
+ .lnk:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
1246
+ /* error: one line plus Retry */
1247
+ .err{margin:0 0 8px;width:0;min-width:100%;font-size:13px;font-weight:600;line-height:1.4;color:#b91c1c;text-align:center;overflow-wrap:anywhere}
1248
+ :host([theme=dark]) .err,:host-context(.dark) .err{color:#fca5a5}
1249
+ /* signed in: the badge */
1250
+ button.chip{display:inline-flex;align-items:center;justify-content:flex-start;gap:10px;width:auto;max-width:100%;padding:5px 14px 5px 5px;border-radius:999px;
1251
+ background:#0b0b0c;color:#ffd24d;border:1px solid rgba(255,210,77,.55);box-shadow:none;font-size:15px}
1252
+ button.chip:hover{background:#151517;border-color:#ffd24d}
1253
+ button.chip:active{background:#000;transform:none}
1254
+ button.chip:focus-visible{outline:2px solid #ffd24d;outline-offset:3px}
1255
+ :host([theme=light]) button.chip{background:#fff;color:#09090b;border-color:#f59e0b}
1256
+ :host([theme=light]) button.chip:hover{background:#fffbeb}
1257
+ :host([full-width]) button.chip{width:100%}
1258
+ .av{position:relative;flex:none;display:inline-grid;place-items:center;width:30px;height:30px;border-radius:50%;overflow:hidden;
1259
+ background:radial-gradient(circle at 30% 30%,#ffe27a,#f5b800 55%,#b88600);color:#000;font-weight:800;font-size:14px}
1260
+ .av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
1261
+ .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:min(52vw,260px)}
1262
+ .dot{flex:none;width:8px;height:8px;border-radius:50%;background:#ffd24d;box-shadow:0 0 0 3px rgba(255,210,77,.22)}
1263
+ :host([size=sm]) button.chip{font-size:13px;padding:3px 12px 3px 3px;gap:8px}
1264
+ :host([size=sm]) .av{width:26px;height:26px}
1265
+ :host([size=icon]) button.chip{width:48px;height:48px;padding:0;justify-content:center}
1266
+ :host([size=icon]) .name,:host([size=icon]) .dot{display:none}
1267
+ :host([size=icon]) .av{width:40px;height:40px}
870
1268
  `;
1269
+ var MENU_CSS = `
1270
+ :host{all:initial}
1271
+ .menu{position:fixed;z-index:2147483600;box-sizing:border-box;min-width:200px;padding:6px;border-radius:14px;background:#0b0b0c;color:#ffd24d;
1272
+ border:1px solid rgba(255,210,77,.55);box-shadow:0 12px 32px rgba(0,0,0,.45);font:600 14px/1.3 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
1273
+ .who{padding:8px 10px 10px;font-size:12px;font-weight:500;color:#e7d9a8;overflow-wrap:anywhere;border-bottom:1px solid rgba(255,210,77,.3);margin-bottom:4px}
1274
+ .item{all:unset;box-sizing:border-box;display:block;width:100%;padding:11px 10px;border-radius:10px;cursor:pointer;color:#ffd24d}
1275
+ .item:hover,.item:focus-visible{background:#ffd24d;color:#000;outline:none}
1276
+ .tip{position:fixed;z-index:2147483600;box-sizing:border-box;padding:8px 12px;border-radius:10px;background:#0b0b0c;color:#e7d9a8;
1277
+ border:1px solid rgba(255,210,77,.4);box-shadow:0 8px 24px rgba(0,0,0,.35);font:500 12px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
1278
+ `;
1279
+ var PENDING_NOTE = "Sign in: shares only your public identity key, moves no money";
1280
+ var PENDING_LABEL = "Approve in bWalletX\u2026";
1281
+ var PENDING_LABEL_OTHER = "Approve in your wallet\u2026";
871
1282
  function b64url2(bytes) {
872
1283
  let s = "";
873
1284
  for (const b of bytes) s += String.fromCharCode(b);
@@ -900,14 +1311,15 @@ async function buildAuthorizeUrl(p) {
900
1311
  async function postJson(url, body) {
901
1312
  const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
902
1313
  const data = await res.json().catch(() => ({}));
903
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
1314
+ if (!res.ok) throw Object.assign(new Error(data.error ?? `HTTP ${res.status}`), { status: res.status });
904
1315
  return data;
905
1316
  }
906
1317
  async function postAccess(url, body) {
907
1318
  const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
908
1319
  return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
909
1320
  }
910
- var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
1321
+ var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url", "state", "identity-key", "paymail", "handle", "avatar-url", "error", "other-wallets", "challenge-url"];
1322
+ var STATES = ["signed-out", "restoring", "pending", "signed-in", "error"];
911
1323
  function createClass() {
912
1324
  return class BwalletxSigninElement extends HTMLElement {
913
1325
  static get observedAttributes() {
@@ -916,12 +1328,23 @@ function createClass() {
916
1328
  #root;
917
1329
  #notFound = false;
918
1330
  #busy = false;
1331
+ #phase = "signed-out";
1332
+ #account = null;
1333
+ #error = "";
1334
+ #restored = false;
1335
+ #pendingLabel = PENDING_LABEL;
1336
+ #menu = null;
919
1337
  constructor() {
920
1338
  super();
921
1339
  this.#root = this.attachShadow({ mode: "open" });
922
1340
  }
923
1341
  connectedCallback() {
924
1342
  this.#render();
1343
+ void this.#restore();
1344
+ }
1345
+ disconnectedCallback() {
1346
+ this.#menu?.close(false);
1347
+ this.#hideTip();
925
1348
  }
926
1349
  attributeChangedCallback() {
927
1350
  if (this.isConnected) this.#render();
@@ -933,93 +1356,402 @@ function createClass() {
933
1356
  get notFound() {
934
1357
  return this.#notFound;
935
1358
  }
1359
+ /** The state shown now (the `state` attribute when set, else the element's own). */
1360
+ get state() {
1361
+ const a = this.#attr("state");
1362
+ return a && STATES.includes(a) ? a : this.#phase;
1363
+ }
1364
+ /** The signed-in account, or null. */
1365
+ get account() {
1366
+ if (!this.#controlled) return this.#account;
1367
+ const identityKey = this.#attr("identity-key");
1368
+ return identityKey ? { identityKey, paymail: this.#attr("paymail"), handle: this.#attr("handle"), avatarUrl: this.#attr("avatar-url") } : null;
1369
+ }
1370
+ /** True while the menu is open (tests). */
1371
+ get menuOpen() {
1372
+ return !!this.#menu;
1373
+ }
936
1374
  focus(opts) {
937
1375
  this.button?.focus(opts);
938
1376
  }
1377
+ get #controlled() {
1378
+ return this.hasAttribute("state");
1379
+ }
1380
+ get #stateful() {
1381
+ return !this.hasAttribute("stateless");
1382
+ }
939
1383
  #attr(name) {
940
1384
  return this.getAttribute(name);
941
1385
  }
1386
+ /** Restore the badge from session-url once, with no wallet call. */
1387
+ async #restore() {
1388
+ const url = this.#attr("session-url");
1389
+ if (this.#restored || !url || this.#controlled || !this.#stateful) return;
1390
+ this.#restored = true;
1391
+ this.#set("restoring");
1392
+ try {
1393
+ const r = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
1394
+ const j = await r.json().catch(() => ({}));
1395
+ if (this.#phase !== "restoring") return;
1396
+ if (j.identityKey) this.#signedIn({ identityKey: j.identityKey, handle: j.handle ?? null, paymail: j.paymail ?? null });
1397
+ else this.#set("signed-out");
1398
+ } catch {
1399
+ if (this.#phase === "restoring") this.#set("signed-out");
1400
+ }
1401
+ }
1402
+ #set(p) {
1403
+ this.#phase = p;
1404
+ if (p !== "signed-in") this.#menu?.close(false);
1405
+ this.#render();
1406
+ }
1407
+ #signedIn(a) {
1408
+ this.#account = a;
1409
+ this.#error = "";
1410
+ this.#set("signed-in");
1411
+ const lookup = this.#attr("lookup-url");
1412
+ if (!a.paymail && lookup !== "off") {
1413
+ void lookupPaymail(a.identityKey, lookup ?? void 0).then((pm) => {
1414
+ if (pm && this.#account?.identityKey === a.identityKey && !this.#account.paymail) {
1415
+ this.#account = { ...this.#account, paymail: pm };
1416
+ this.#render();
1417
+ }
1418
+ });
1419
+ }
1420
+ }
942
1421
  #render() {
943
1422
  const size = this.#attr("size") ?? "lg";
944
1423
  const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
945
- const text = signinText(this.#attr("label") ?? "signin", brand);
1424
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1425
+ const state = this.state;
1426
+ const acct = this.account;
1427
+ if (state === "signed-in" && acct) {
1428
+ this.#hideTip();
1429
+ const name = accountLabel(acct);
1430
+ const avatar = acct.avatarUrl ?? avatarUrlFor(acct.identityKey);
1431
+ const initial = (name.replace(/^\$/, "").charAt(0) || "b").toUpperCase();
1432
+ this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" class="chip" part="button chip" aria-haspopup="menu" aria-expanded="${this.#menu ? "true" : "false"}" aria-label="Signed in with bWalletX as ${esc(name)}. Account menu" title="Signed in as ${esc(name)}"><span class="av" part="avatar" aria-hidden="true">${esc(initial)}${avatar ? `<img alt="" src="${esc(avatar)}" referrerpolicy="no-referrer">` : ""}</span><span class="name" part="label">${esc(name)}</span><span class="dot" part="dot" aria-hidden="true"></span></button>`;
1433
+ const img = this.#root.querySelector("img");
1434
+ if (img) img.addEventListener("error", () => img.remove(), { once: true });
1435
+ const chip = this.button;
1436
+ chip.addEventListener("click", () => this.#menu ? this.#menu.close(true) : this.#openMenu());
1437
+ chip.addEventListener("keydown", (e) => {
1438
+ if ((e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") && !this.#menu) {
1439
+ e.preventDefault();
1440
+ this.#openMenu();
1441
+ }
1442
+ });
1443
+ return;
1444
+ }
1445
+ const pending = state === "pending";
1446
+ const restoring = state === "restoring";
1447
+ const error = state === "error";
1448
+ const pendingLabel = this.#controlled ? PENDING_LABEL : this.#pendingLabel;
1449
+ const text = pending ? pendingLabel : error ? "Retry" : signinText(this.#attr("label") ?? "signin", brand);
946
1450
  const subAttr = this.#attr("subtitle");
947
1451
  const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
948
- const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
949
- const loading = this.hasAttribute("loading") || this.#busy;
1452
+ const subtitle = pending || error ? null : subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
1453
+ const loading = this.hasAttribute("loading") || this.#busy || pending || restoring;
950
1454
  const disabled = this.hasAttribute("disabled");
951
1455
  const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
952
- const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
1456
+ const aria = pending ? pendingLabel : this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
953
1457
  const getUrl = this.#attr("get-url") ?? GET_URL;
954
1458
  const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
955
- const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1459
+ const errText = error ? (this.#controlled ? this.#attr("error") : this.#error) || "Sign-in failed" : "";
956
1460
  const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
957
1461
  const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
958
- const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.</p>` : "";
959
- this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : ""}${nf}`;
1462
+ const otherOff = this.#attr("other-wallets") === "off";
1463
+ const otherLink = '<button type="button" class="lnk" data-act="other" part="other-wallets-link">Other wallets</button>';
1464
+ const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.${otherOff ? "" : ` ${otherLink}`}</p>` : "";
1465
+ const ow = !this.#notFound && !pending && !restoring && this.#otherWalletsShown(size) ? `<p class="ow" part="other-wallets">${otherLink}</p>` : "";
1466
+ const err = error ? `<p class="err" part="error" role="alert">${esc(errText)}</p>` : "";
1467
+ const compact = size === "sm" || size === "icon";
1468
+ const note = pending && !compact ? `<p class="note" part="note">${esc(PENDING_NOTE)}</p>` : "";
1469
+ const status = pending ? `<span class="sr" role="status">${esc(pendingLabel)}</span>` : loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : "";
1470
+ this.#root.innerHTML = `<style>${SIGNIN_CSS}</style>${err}<button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${status}${note}${ow}${nf}`;
960
1471
  this.button.addEventListener("click", () => void this.#click());
1472
+ for (const l of Array.from(this.#root.querySelectorAll("[data-act=other]"))) l.addEventListener("click", () => void this.otherWallets());
961
1473
  bindPrewarm(this.button);
1474
+ if (pending && compact) this.#showTip(PENDING_NOTE);
1475
+ else this.#hideTip();
1476
+ }
1477
+ #tip = null;
1478
+ #showTip(text) {
1479
+ const b = this.button;
1480
+ if (!b || typeof document === "undefined") return;
1481
+ if (!this.#tip) {
1482
+ const host = document.createElement("div");
1483
+ host.setAttribute("data-bwalletx-tip", "");
1484
+ const root = host.attachShadow({ mode: "open" });
1485
+ root.innerHTML = `<style>${MENU_CSS}</style><div class="tip" role="note"></div>`;
1486
+ document.body.appendChild(host);
1487
+ this.#tip = host;
1488
+ }
1489
+ const tip2 = this.#tip.shadowRoot.querySelector(".tip");
1490
+ tip2.textContent = text;
1491
+ const r = b.getBoundingClientRect();
1492
+ const vw = window.innerWidth || document.documentElement.clientWidth || 360;
1493
+ const width = Math.min(280, vw - 32);
1494
+ tip2.style.width = `${width}px`;
1495
+ tip2.style.left = `${Math.max(16, Math.min(r.right - width, vw - width - 16))}px`;
1496
+ tip2.style.top = `${r.bottom + 8}px`;
1497
+ }
1498
+ #hideTip() {
1499
+ this.#tip?.remove();
1500
+ this.#tip = null;
1501
+ }
1502
+ #openMenu() {
1503
+ const acct = this.account;
1504
+ const chip = this.button;
1505
+ if (!acct || !chip || typeof document === "undefined") return;
1506
+ const name = accountLabel(acct);
1507
+ const host = document.createElement("div");
1508
+ host.setAttribute("data-bwalletx-menu", "");
1509
+ const root = host.attachShadow({ mode: "open" });
1510
+ const style = document.createElement("style");
1511
+ style.textContent = MENU_CSS;
1512
+ const menu = document.createElement("div");
1513
+ menu.className = "menu";
1514
+ menu.setAttribute("role", "menu");
1515
+ menu.setAttribute("aria-label", "bWalletX account");
1516
+ const who = document.createElement("div");
1517
+ who.className = "who";
1518
+ who.textContent = `Signed in as ${acct.paymail || name}`;
1519
+ const item = (text, run) => {
1520
+ const b = document.createElement("button");
1521
+ b.type = "button";
1522
+ b.className = "item";
1523
+ b.setAttribute("role", "menuitem");
1524
+ b.tabIndex = -1;
1525
+ b.textContent = text;
1526
+ b.addEventListener("click", () => run(b));
1527
+ return b;
1528
+ };
1529
+ const copyWhat = acct.paymail ? "paymail" : "key";
1530
+ const copy = item(`Copy ${copyWhat}`, (b) => {
1531
+ void copyText(acct.paymail || acct.identityKey).then((ok) => {
1532
+ b.textContent = ok ? "Copied" : `Copy ${copyWhat}`;
1533
+ setTimeout(() => close(true), ok ? 600 : 0);
1534
+ });
1535
+ });
1536
+ const out = item("Disconnect", () => {
1537
+ close(false);
1538
+ void this.signOut();
1539
+ });
1540
+ menu.append(who, copy, out);
1541
+ root.append(style, menu);
1542
+ document.body.appendChild(host);
1543
+ const place = () => {
1544
+ const r = chip.getBoundingClientRect();
1545
+ const vw = window.innerWidth || document.documentElement.clientWidth || 360;
1546
+ const vh = window.innerHeight || document.documentElement.clientHeight || 640;
1547
+ const width = Math.min(260, vw - 32);
1548
+ menu.style.width = `${width}px`;
1549
+ const left = Math.max(16, Math.min(r.right - width, vw - width - 16));
1550
+ menu.style.left = `${left}px`;
1551
+ const h = menu.offsetHeight || 150;
1552
+ const below = r.bottom + 8;
1553
+ menu.style.top = `${below + h > vh && r.top - 8 - h > 0 ? r.top - 8 - h : below}px`;
1554
+ };
1555
+ place();
1556
+ const items = [copy, out];
1557
+ const focusAt = (i) => items[(i + items.length) % items.length].focus();
1558
+ const onKey = (e) => {
1559
+ const i = items.indexOf(root.activeElement);
1560
+ if (e.key === "Escape") {
1561
+ e.preventDefault();
1562
+ close(true);
1563
+ } else if (e.key === "ArrowDown") {
1564
+ e.preventDefault();
1565
+ focusAt(i + 1);
1566
+ } else if (e.key === "ArrowUp") {
1567
+ e.preventDefault();
1568
+ focusAt(i - 1);
1569
+ } else if (e.key === "Home") {
1570
+ e.preventDefault();
1571
+ focusAt(0);
1572
+ } else if (e.key === "End") {
1573
+ e.preventDefault();
1574
+ focusAt(items.length - 1);
1575
+ } else if (e.key === "Tab") close(false);
1576
+ };
1577
+ const onDown = (e) => {
1578
+ const path = e.composedPath?.() ?? [];
1579
+ if (!path.includes(host) && !path.includes(this)) close(false);
1580
+ };
1581
+ const onMove = () => close(false);
1582
+ menu.addEventListener("keydown", onKey);
1583
+ document.addEventListener("pointerdown", onDown, true);
1584
+ window.addEventListener("resize", onMove);
1585
+ window.addEventListener("scroll", place, true);
1586
+ const close = (focusChip = false) => {
1587
+ if (this.#menu?.host !== host) return;
1588
+ this.#menu = null;
1589
+ menu.removeEventListener("keydown", onKey);
1590
+ document.removeEventListener("pointerdown", onDown, true);
1591
+ window.removeEventListener("resize", onMove);
1592
+ window.removeEventListener("scroll", place, true);
1593
+ host.remove();
1594
+ this.button?.setAttribute("aria-expanded", "false");
1595
+ if (focusChip) this.button?.focus();
1596
+ };
1597
+ this.#menu = { host, close };
1598
+ chip.setAttribute("aria-expanded", "true");
1599
+ focusAt(0);
1600
+ }
1601
+ /**
1602
+ * Disconnect: emits bwalletx-disconnect (cancelable). Unless a listener cancels it (BwalletXProvider signs out
1603
+ * itself) or the element is driven by `state`, posts logout-url and shows signed out.
1604
+ */
1605
+ async signOut() {
1606
+ this.#menu?.close(false);
1607
+ if (!this.#emit("bwalletx-disconnect", { element: this, account: this.account }, true)) return;
1608
+ if (this.#controlled) return;
1609
+ const url = this.#attr("logout-url");
1610
+ if (url) await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: "{}" }).catch(() => void 0);
1611
+ this.#account = null;
1612
+ this.#set("signed-out");
1613
+ this.#emit("bwalletx-signed-out");
1614
+ this.button?.focus();
962
1615
  }
963
1616
  #emit(type, detail, cancelable = false) {
964
1617
  return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
965
1618
  }
966
- #setBusy(on) {
967
- this.#busy = on;
968
- this.#render();
1619
+ /** "Other wallets" is offered under the button: other-wallets on, or auto at lg/md when this element signs in. */
1620
+ #otherWalletsShown(size) {
1621
+ const a = this.#attr("other-wallets");
1622
+ if (a === "off") return false;
1623
+ if (a === "on") return true;
1624
+ return (size === "lg" || size === "md") && (this.#controlled || !!this.#attr("challenge-url"));
969
1625
  }
970
- async #click() {
971
- if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
972
- if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
973
- const href = this.#attr("href");
974
- const authorizeUrl = this.#attr("authorize-url");
975
- if (authorizeUrl) {
976
- location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
977
- return;
1626
+ /** Open the wallet chooser (the "Other wallets" link): phone QR, other browser wallets, desktop wallets. */
1627
+ otherWallets() {
1628
+ return this.#click("chooser");
1629
+ }
1630
+ #attempt(option, outcome, started) {
1631
+ this.#emit("bwalletx-attempt", attemptFor(option, outcome, started));
1632
+ }
1633
+ /**
1634
+ * Which wallet: bWalletX by its own marker goes straight to its sheet; otherwise the chooser ("Get bWalletX" first)
1635
+ * when another wallet is here or the user asked; otherwise 'not-found'. null: the chooser was closed.
1636
+ */
1637
+ async #choose(mode, ms, pairing) {
1638
+ if (mode !== "chooser") {
1639
+ const f = await discoverBwalletX(ms);
1640
+ if (f) {
1641
+ const substrate = f.rdns === RDNS_EXTENSION ? "extension" : f.rdns === RDNS_MOBILE ? "in-app" : "xdm";
1642
+ return brc100Option(f.wallet, { id: f.rdns ?? "bwalletx-xdm", rdns: f.rdns ?? void 0, name: "bWalletX", isBwalletX: true, substrate });
1643
+ }
978
1644
  }
979
- if (href) {
980
- location.assign(href);
981
- return;
1645
+ const list = await discoverWallets({ ms: 50, pairing: () => pairing });
1646
+ if (mode !== "chooser" && !hasBwalletX(list) && !otherDetected(list).length) return "not-found";
1647
+ return chooseWallet(list, { getUrl: this.#attr("get-url") ?? void 0 });
1648
+ }
1649
+ async #click(mode = "signin") {
1650
+ if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy || this.state === "pending") return;
1651
+ if (!this.#emit("bwalletx-signin", { element: this, mode }, true)) return;
1652
+ if (mode === "signin") {
1653
+ const href = this.#attr("href");
1654
+ const authorizeUrl = this.#attr("authorize-url");
1655
+ if (authorizeUrl) {
1656
+ location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
1657
+ return;
1658
+ }
1659
+ if (href) {
1660
+ location.assign(href);
1661
+ return;
1662
+ }
982
1663
  }
983
1664
  const challengeUrl = this.#attr("challenge-url");
984
1665
  if (!challengeUrl) return;
1666
+ const own = this.#stateful && !this.#controlled;
1667
+ const prev = this.#phase === "signed-in" ? "signed-in" : "signed-out";
1668
+ const started = Date.now();
1669
+ const pairing = pairingModal();
1670
+ let option = null;
985
1671
  this.#notFound = false;
986
- this.#setBusy(true);
1672
+ this.#busy = true;
1673
+ this.#pendingLabel = PENDING_LABEL;
1674
+ if (own) this.#set("pending");
1675
+ else this.#render();
987
1676
  try {
988
1677
  const ms = Number(this.#attr("discover-ms")) || DEFAULT_DISCOVER_MS;
989
- const found = await discoverBwalletX(ms);
990
- if (!found) {
1678
+ const picked = await this.#choose(mode, ms, pairing);
1679
+ if (picked === "not-found") {
991
1680
  this.#notFound = true;
1681
+ if (own) this.#phase = "signed-out";
1682
+ this.#attempt(null, "not-found", started);
992
1683
  this.#emit("bwalletx-not-found");
993
1684
  if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
994
1685
  return;
995
1686
  }
1687
+ if (!picked) {
1688
+ if (own) this.#phase = prev;
1689
+ this.#attempt(null, "cancelled", started);
1690
+ return;
1691
+ }
1692
+ option = picked;
1693
+ if (!option.isBwalletX) {
1694
+ this.#pendingLabel = PENDING_LABEL_OTHER;
1695
+ if (own) this.#render();
1696
+ }
1697
+ const cw = await option.connect({ identity: false });
1698
+ const wallet = cw.wallet;
1699
+ if (!wallet) throw Object.assign(new Error("This wallet cannot sign in here"), { code: "NOT_SUPPORTED" });
1700
+ const sub = substrateOf(option);
1701
+ const method = sub === "extension" ? "extension" : sub === "in-app" || sub === "xdm" ? "in-app" : sub === "pairing" ? "pairing" : "wallet";
996
1702
  const requireToken = this.#attr("require-token");
997
1703
  if (requireToken) {
1704
+ if (own) this.#phase = "signed-out";
998
1705
  const verifyUrl2 = this.#attr("verify-url");
999
1706
  const min = this.#attr("min");
1000
1707
  const minBps = this.#attr("min-bps");
1001
1708
  const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
1002
1709
  try {
1003
1710
  const a = await requestAccess({
1004
- wallet: found.wallet,
1711
+ wallet,
1005
1712
  challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
1006
1713
  verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
1007
1714
  });
1715
+ this.#attempt(option, "signed-in", started);
1008
1716
  this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
1009
1717
  } catch (e) {
1010
1718
  const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1719
+ this.#attempt(option, outcomeOf(e), started);
1011
1720
  this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
1012
1721
  }
1013
1722
  return;
1014
1723
  }
1015
- const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
1016
1724
  const verifyUrl = this.#attr("verify-url");
1017
- const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
1018
- this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
1725
+ const { result: r, verified } = await signInAndVerify({
1726
+ wallet,
1727
+ identityKeyHint: verifyUrl ? rememberedIdentity() : void 0,
1728
+ challenge: (identityKey) => postJson(challengeUrl, identityKey ? { identityKey } : {}),
1729
+ verify: async (res) => verifyUrl ? await postJson(verifyUrl, { identityKey: res.identityKey, nonce: res.nonce, signature: res.signature }) : {}
1730
+ });
1731
+ if (verifyUrl) rememberIdentity(r.identityKey);
1732
+ const response = verifyUrl ? verified : null;
1733
+ if (own) this.#signedIn({ identityKey: r.identityKey, handle: response?.handle ?? null, paymail: response?.paymail ?? null });
1734
+ this.#attempt(option, "signed-in", started);
1735
+ this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method, nonce: r.nonce, signature: r.signature, response });
1019
1736
  } catch (error) {
1020
- this.#emit("bwalletx-error", { error });
1737
+ const outcome = outcomeOf(error);
1738
+ this.#attempt(option, outcome, started);
1739
+ const closedQr = outcome === "cancelled" && option?.kind === "pairing";
1740
+ if (own) {
1741
+ if (closedQr) this.#phase = prev;
1742
+ else {
1743
+ this.#error = signInErrorText(error);
1744
+ this.#phase = "error";
1745
+ }
1746
+ }
1747
+ if (!closedQr) this.#emit("bwalletx-error", { error });
1021
1748
  } finally {
1022
- this.#setBusy(false);
1749
+ this.#busy = false;
1750
+ try {
1751
+ pairing.close();
1752
+ } catch {
1753
+ }
1754
+ this.#render();
1023
1755
  }
1024
1756
  }
1025
1757
  };
@@ -1051,7 +1783,21 @@ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border
1051
1783
  .bwx-list{display:flex;flex-direction:column;gap:8px}
1052
1784
  .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
1053
1785
  .bwx-opt img{width:24px;height:24px;border-radius:6px}
1054
- .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f}
1786
+ .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f;white-space:nowrap}
1787
+ .bwx-opt{min-height:48px;box-sizing:border-box}
1788
+ a.bwx-btn.bwx-opt{display:flex;text-align:left}
1789
+ .bwx-btn:focus{outline:none}
1790
+ .bwx-btn:focus-visible{outline:2px solid #e8c766;outline-offset:2px}
1791
+ .bwx-mk{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
1792
+ .bwx-ico{display:inline-grid;place-items:center;flex:none;width:28px;height:28px;border-radius:8px;background:#2a2518;color:#e8c766;font-weight:700;font-size:13px}
1793
+ .bwx-opt img{width:28px;height:28px;border-radius:8px;flex:none}
1794
+ .bwx-txt{display:flex;flex-direction:column;min-width:0;gap:1px}
1795
+ .bwx-txt b{font-weight:600}
1796
+ .bwx-txt small{font-size:12px;color:#8d866f;font-weight:400}
1797
+ .bwx-primary .bwx-txt small{color:rgba(0,0,0,.68)}
1798
+ .bwx-primary .bwx-tag{color:rgba(0,0,0,.6)}
1799
+ .bwx-sep{font-size:12px;color:#8d866f;margin:6px 2px 0}
1800
+ .bwx-foot{font-size:12px;color:#8d866f;margin:12px 0 0}
1055
1801
  `;
1056
1802
  function mount() {
1057
1803
  const root = document.createElement("div");
@@ -1180,105 +1926,94 @@ function pickWallet(options, o = {}) {
1180
1926
  ui.box.lastChild.append(cancel);
1181
1927
  });
1182
1928
  }
1183
-
1184
- // src/wallets.ts
1185
- var import_sdk5 = require("@bsv/sdk");
1186
- async function brc100Connected(wallet, o) {
1187
- await wallet.waitForAuthentication({}).catch(() => {
1188
- });
1189
- const { publicKey } = await wallet.getPublicKey({ identityKey: true });
1190
- return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
1191
- }
1192
- function brc100Option(wallet, info) {
1193
- const opt = {
1194
- id: info.id,
1195
- name: info.name,
1196
- icon: info.icon,
1197
- rdns: info.rdns,
1198
- kind: "brc100",
1199
- isBwalletX: info.isBwalletX ?? false,
1200
- connect: () => brc100Connected(wallet, opt)
1201
- };
1202
- return opt;
1203
- }
1204
- function pairingOption(pairing) {
1205
- const opt = {
1206
- id: "bwalletx-pairing",
1207
- name: "bWalletX on your phone",
1208
- kind: "pairing",
1209
- isBwalletX: true,
1210
- async connect() {
1211
- const p = typeof pairing === "function" ? pairing() : pairing;
1212
- const w = await restorePairing(p) ?? await pairBwalletX(p);
1213
- return brc100Connected(w, opt);
1214
- }
1215
- };
1216
- return opt;
1217
- }
1218
- function handcashAdapter(o) {
1219
- const opt = {
1220
- id: "handcash",
1221
- name: "HandCash",
1222
- icon: o.icon,
1223
- kind: "handcash",
1224
- isBwalletX: false,
1225
- async connect() {
1226
- const p = await o.login();
1227
- return {
1228
- wallet: null,
1229
- payer: o.pay ? { pay: o.pay } : null,
1230
- identityKey: p.identityKey ?? null,
1231
- handle: p.handle,
1232
- paymail: p.paymail ?? `${p.handle.replace(/^\$/, "")}@handcash.io`,
1233
- optionId: opt.id,
1234
- kind: "handcash",
1235
- transitional: true
1236
- };
1237
- }
1238
- };
1239
- return opt;
1240
- }
1241
- var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
1242
- async function probe(substrate, ms) {
1243
- try {
1244
- const w = new import_sdk5.WalletClient(substrate);
1245
- const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
1246
- return ok ? w : null;
1247
- } catch {
1248
- return null;
1929
+ function optionRow(w, primary, sub, tag) {
1930
+ const b = el("button", `bwx-btn bwx-opt${primary ? " bwx-primary" : ""}`);
1931
+ b.type = "button";
1932
+ b.dataset.wallet = w.id;
1933
+ if (w.isBwalletX) {
1934
+ const mk = el("span", "bwx-mk");
1935
+ mk.innerHTML = markSvg(18);
1936
+ b.append(mk);
1937
+ } else if (w.icon) {
1938
+ const img = el("img");
1939
+ img.src = w.icon;
1940
+ img.alt = "";
1941
+ b.append(img);
1942
+ } else {
1943
+ b.append(el("span", "bwx-ico", (w.name.trim().charAt(0) || "W").toUpperCase()));
1249
1944
  }
1945
+ const t = el("span", "bwx-txt");
1946
+ t.append(el("b", "", w.name));
1947
+ if (sub) t.append(el("small", "", sub));
1948
+ b.append(t);
1949
+ if (tag) b.append(el("span", "bwx-tag", tag));
1950
+ return b;
1250
1951
  }
1251
- async function discoverWallets(o = {}) {
1252
- const out = [];
1253
- const ms = o.ms ?? 300;
1254
- if (typeof window !== "undefined") {
1255
- const seen = /* @__PURE__ */ new Map();
1256
- const onAnnounce = (e) => {
1257
- const d = e.detail;
1258
- if (!d?.wallet) return;
1259
- const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
1260
- if (seen.has(rdns)) return;
1261
- const isB = BWALLETX_RDNS.includes(rdns);
1262
- seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB }));
1952
+ function chooseWallet(options, o = {}) {
1953
+ if (typeof document === "undefined") return Promise.resolve(null);
1954
+ return new Promise((resolve) => {
1955
+ const ui = mount();
1956
+ ui.root.dataset.bwxChooser = "";
1957
+ const done = (w) => {
1958
+ document.removeEventListener("keydown", onKey, true);
1959
+ ui.close();
1960
+ resolve(w);
1263
1961
  };
1264
- window.addEventListener("brc100:announceWallet", onAnnounce);
1265
- window.dispatchEvent(new Event("brc100:requestWallet"));
1266
- const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
1267
- await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
1268
- window.removeEventListener("brc100:announceWallet", onAnnounce);
1269
- out.push(...seen.values());
1270
- const cwi = window.CWI;
1271
- if (cwi && ![...seen.values()].some((x) => x.rdns === "space.bwallet.mobile")) {
1272
- out.push(brc100Option(cwi, { id: "window.CWI", name: "This app\u2019s wallet", isBwalletX: false }));
1962
+ const onKey = (e) => {
1963
+ if (e.key === "Escape") {
1964
+ e.preventDefault();
1965
+ done(null);
1966
+ }
1967
+ };
1968
+ document.addEventListener("keydown", onKey, true);
1969
+ ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
1970
+ const list = el("div", "bwx-list");
1971
+ const here = hasBwalletX(options);
1972
+ if (!here) {
1973
+ const get = el("a", "bwx-btn bwx-opt bwx-primary");
1974
+ get.href = o.getUrl ?? GET_URL;
1975
+ get.target = "_blank";
1976
+ get.rel = "noopener";
1977
+ get.dataset.wallet = "get-bwalletx";
1978
+ const mk = el("span", "bwx-mk");
1979
+ mk.innerHTML = markSvg(18);
1980
+ const t = el("span", "bwx-txt");
1981
+ t.append(el("b", "", "Get bWalletX"), el("small", "", "Free. Every feature on this site"));
1982
+ get.append(mk, t);
1983
+ list.append(get);
1273
1984
  }
1274
- const [json, cicada] = await local;
1275
- if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)" }));
1276
- if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)" }));
1277
- }
1278
- if (o.pairing) out.push(pairingOption(o.pairing));
1279
- out.push(...o.extra ?? []);
1280
- const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
1281
- return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
1985
+ const ordered = chooserOrder(options);
1986
+ const touch = isTouchDevice();
1987
+ let sep = false;
1988
+ for (const w of ordered) {
1989
+ if (!here && !sep) {
1990
+ list.append(el("p", "bwx-sep", "Or use"));
1991
+ sep = true;
1992
+ }
1993
+ const sub = isPresentBwalletX(w) ? "Recommended" : w.kind === "pairing" ? touch ? "Open the bWalletX app" : "Scan a QR with your phone" : void 0;
1994
+ const tag = w.kind === "handcash" ? "until BRC-100" : !w.isBwalletX && w.kind === "brc100" ? "Fewer features" : void 0;
1995
+ const row2 = optionRow(w, isPresentBwalletX(w), sub, tag);
1996
+ row2.onclick = () => done(w);
1997
+ list.append(row2);
1998
+ }
1999
+ if (o.desktop !== false && !options.some((w) => w.substrate === "json-api" || w.substrate === "cicada")) {
2000
+ if (!here && !sep) list.append(el("p", "bwx-sep", "Or use"));
2001
+ const d = desktopOption();
2002
+ const row2 = optionRow(d, false, "Metanet or BSV Desktop on this computer", "Fewer features");
2003
+ row2.onclick = () => done(d);
2004
+ list.append(row2);
2005
+ }
2006
+ ui.box.append(list);
2007
+ if (otherDetected(options).length || !here) ui.box.append(el("p", "bwx-foot", "Some features need bWalletX."));
2008
+ const row = el("div", "bwx-row");
2009
+ const cancel = el("button", "bwx-btn", "Cancel");
2010
+ cancel.type = "button";
2011
+ cancel.onclick = () => done(null);
2012
+ row.append(cancel);
2013
+ ui.box.append(row);
2014
+ ui.root.onclick = (e) => e.target === ui.root && done(null);
2015
+ list.querySelector("a,button")?.focus();
2016
+ });
1282
2017
  }
1283
2018
 
1284
2019
  // src/pay.ts
@@ -1443,19 +2178,52 @@ async function buyToken(source, req) {
1443
2178
  }
1444
2179
 
1445
2180
  // src/react.ts
2181
+ function signinStateFor(v) {
2182
+ if (v.status === "restoring") return "restoring";
2183
+ if (v.status === "connecting") return "pending";
2184
+ if (v.status === "error") return "error";
2185
+ if (v.status === "connected" && v.identityKey) return "signed-in";
2186
+ return "signed-out";
2187
+ }
1446
2188
  function BwalletxSignin(props) {
1447
2189
  const ref = (0, import_react.useRef)(null);
1448
2190
  const cb = (0, import_react.useRef)(props);
1449
2191
  cb.current = props;
2192
+ const ctx = (0, import_react.useContext)(Ctx);
2193
+ const ctxRef = (0, import_react.useRef)(ctx);
2194
+ ctxRef.current = ctx;
2195
+ const driven = !!ctx && props.stateful !== false;
1450
2196
  (0, import_react.useEffect)(() => {
1451
2197
  defineBwalletxSignin();
1452
2198
  const el2 = ref.current;
1453
2199
  if (!el2) return;
1454
2200
  const on = [
1455
- ["bwalletx-signin", (e) => cb.current.onClick?.(e)],
2201
+ [
2202
+ "bwalletx-signin",
2203
+ (e) => {
2204
+ cb.current.onClick?.(e);
2205
+ const c = ctxRef.current;
2206
+ if (!e.defaultPrevented && c && cb.current.stateful !== false) {
2207
+ e.preventDefault();
2208
+ const mode = e.detail?.mode;
2209
+ c.connect(mode === "chooser" ? { chooser: true } : {}).catch(() => void 0);
2210
+ }
2211
+ }
2212
+ ],
1456
2213
  ["bwalletx-signed-in", (e) => cb.current.onSignedIn?.(e.detail)],
1457
2214
  ["bwalletx-not-found", () => cb.current.onNotFound?.()],
1458
- ["bwalletx-error", (e) => cb.current.onError?.(e.detail.error)]
2215
+ ["bwalletx-error", (e) => cb.current.onError?.(e.detail.error)],
2216
+ [
2217
+ "bwalletx-disconnect",
2218
+ (e) => {
2219
+ const c = ctxRef.current;
2220
+ if (c && cb.current.stateful !== false) {
2221
+ e.preventDefault();
2222
+ void c.disconnect().then(() => cb.current.onSignedOut?.());
2223
+ }
2224
+ }
2225
+ ],
2226
+ ["bwalletx-signed-out", () => cb.current.onSignedOut?.()]
1459
2227
  ];
1460
2228
  for (const [t, f] of on) el2.addEventListener(t, f);
1461
2229
  return () => {
@@ -1464,6 +2232,7 @@ function BwalletxSignin(props) {
1464
2232
  }, []);
1465
2233
  const p = props;
1466
2234
  const flag = (v) => v ? "" : void 0;
2235
+ const state = driven ? signinStateFor(ctx) : void 0;
1467
2236
  return (0, import_react.createElement)("bwalletx-signin", {
1468
2237
  ref,
1469
2238
  id: p.id,
@@ -1479,6 +2248,16 @@ function BwalletxSignin(props) {
1479
2248
  disabled: flag(p.disabled),
1480
2249
  "challenge-url": p.challengeUrl,
1481
2250
  "verify-url": p.verifyUrl,
2251
+ "session-url": driven ? void 0 : p.sessionUrl,
2252
+ "logout-url": driven ? void 0 : p.logoutUrl,
2253
+ "lookup-url": p.lookupUrl,
2254
+ stateless: p.stateful === false ? "" : void 0,
2255
+ state,
2256
+ "identity-key": driven ? ctx.identityKey ?? void 0 : void 0,
2257
+ paymail: driven ? ctx.paymail ?? void 0 : void 0,
2258
+ handle: driven ? ctx.handle ?? void 0 : void 0,
2259
+ "avatar-url": driven ? avatarUrlFor(ctx.identityKey) ?? void 0 : void 0,
2260
+ error: driven && ctx.status === "error" ? signInErrorText(ctx.error) : void 0,
1482
2261
  "get-url": p.getUrl,
1483
2262
  "web-url": p.webUrl,
1484
2263
  href: p.href,
@@ -1487,6 +2266,7 @@ function BwalletxSignin(props) {
1487
2266
  "redirect-uri": p.redirectUri,
1488
2267
  scope: p.scope,
1489
2268
  "discover-ms": p.discoverMs,
2269
+ "other-wallets": p.otherWallets,
1490
2270
  "aria-label": p["aria-label"]
1491
2271
  });
1492
2272
  }
@@ -1498,6 +2278,7 @@ var empty = {
1498
2278
  paymail: null,
1499
2279
  wallet: null,
1500
2280
  walletId: null,
2281
+ walletType: null,
1501
2282
  transitional: false,
1502
2283
  signedIn: false,
1503
2284
  error: null
@@ -1559,45 +2340,42 @@ function BwalletXProvider(p) {
1559
2340
  setState((s) => ({ ...s, status: "connecting", error: null }));
1560
2341
  try {
1561
2342
  const list = await listWallets();
1562
- let choice = opts.walletId ? list.find((w) => w.id === opts.walletId) : void 0;
1563
- if (!choice) {
1564
- const direct = list.filter((w) => w.kind !== "pairing");
1565
- choice = c.picker !== false && list.length > 1 ? await pickWallet(list) : direct[0] ?? list[0] ?? null;
1566
- }
1567
- if (!choice) {
1568
- if (!list.length) throw Object.assign(new Error("No wallet found. Get bWalletX at bwalletx.com."), { code: "NOT_FOUND" });
1569
- setState((s) => ({ ...s, status: s.identityKey ? "connected" : "idle" }));
2343
+ const fast = c.fastPath !== false;
2344
+ const r = await runConnect({
2345
+ list,
2346
+ walletId: opts.walletId,
2347
+ chooser: opts.chooser,
2348
+ picker: c.picker,
2349
+ fastPath: fast,
2350
+ pick: fast ? (l) => chooseWallet(l) : pickWallet,
2351
+ onAttempt: c.onAttempt,
2352
+ challengeUrl: c.challengeUrl,
2353
+ verifyUrl: c.verifyUrl,
2354
+ challengeNeedsIdentityKey: c.challengeNeedsIdentityKey,
2355
+ post: postJson2
2356
+ });
2357
+ if (!r) {
2358
+ setState((s) => ({ ...s, status: s.identityKey && s.signedIn ? "connected" : "idle" }));
1570
2359
  return null;
1571
2360
  }
1572
- const cw = await choice.connect();
1573
- let { handle = null, paymail = null } = cw;
1574
- let signedIn = false;
1575
- if (cw.wallet && c.challengeUrl && c.verifyUrl) {
1576
- const r = await signInWithBwalletX({
1577
- wallet: cw.wallet,
1578
- challenge: (identityKey) => postJson2(c.challengeUrl, { identityKey })
1579
- });
1580
- const v = await atStage("verify", () => postJson2(c.verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }));
1581
- handle = v.handle ?? handle;
1582
- paymail = v.paymail ?? paymail;
1583
- signedIn = true;
1584
- }
2361
+ const cw = r.connected;
1585
2362
  payer.current = cw.payer;
1586
2363
  setState({
1587
2364
  status: "connected",
1588
- identityKey: cw.identityKey,
1589
- handle,
1590
- paymail,
2365
+ identityKey: r.identityKey,
2366
+ handle: r.handle,
2367
+ paymail: r.paymail,
1591
2368
  wallet: cw.wallet,
1592
2369
  walletId: cw.optionId,
2370
+ walletType: r.walletType,
1593
2371
  transitional: !!cw.transitional,
1594
- signedIn,
2372
+ signedIn: r.signedIn,
1595
2373
  error: null
1596
2374
  });
1597
- return cw;
2375
+ return { ...cw, identityKey: r.identityKey, handle: r.handle ?? void 0, paymail: r.paymail ?? void 0 };
1598
2376
  } catch (e) {
1599
2377
  if (e?.message === "cancelled") {
1600
- setState((s) => ({ ...s, status: s.identityKey ? "connected" : "idle" }));
2378
+ setState((s) => ({ ...s, status: s.identityKey && s.signedIn ? "connected" : "idle" }));
1601
2379
  return null;
1602
2380
  }
1603
2381
  throw fail(e);
@@ -1613,6 +2391,17 @@ function BwalletXProvider(p) {
1613
2391
  payer.current = null;
1614
2392
  setState(empty);
1615
2393
  }, [state.walletId]);
2394
+ (0, import_react.useEffect)(() => {
2395
+ const key = state.identityKey;
2396
+ if (!key || state.paymail || p.lookupUrl === "off") return;
2397
+ let live = true;
2398
+ void lookupPaymail(key, p.lookupUrl).then((pm) => {
2399
+ if (live && pm) setState((s) => s.identityKey === key && !s.paymail ? { ...s, paymail: pm } : s);
2400
+ });
2401
+ return () => {
2402
+ live = false;
2403
+ };
2404
+ }, [state.identityKey, state.paymail, p.lookupUrl]);
1616
2405
  const need = () => {
1617
2406
  if (!payer.current) throw new PaymentError("NOT_SUPPORTED", "Connect a wallet first (connect()).");
1618
2407
  return payer.current;
@@ -1637,6 +2426,24 @@ function useBwalletX() {
1637
2426
  if (!v) throw new Error("useBwalletX() must be used inside <BwalletXProvider>");
1638
2427
  return v;
1639
2428
  }
2429
+ function accountFor(v) {
2430
+ const status = signinStateFor(v);
2431
+ const signedIn = status === "signed-in";
2432
+ return {
2433
+ status,
2434
+ identityKey: signedIn ? v.identityKey : null,
2435
+ paymail: signedIn ? v.paymail : null,
2436
+ handle: signedIn ? v.handle : null,
2437
+ label: signedIn ? accountLabel(v) : null,
2438
+ avatarUrl: signedIn ? avatarUrlFor(v.identityKey) : null,
2439
+ error: status === "error" ? signInErrorText(v.error) : null,
2440
+ login: () => v.connect().then(() => void 0, () => void 0),
2441
+ logout: () => v.disconnect()
2442
+ };
2443
+ }
2444
+ function useBwalletXAccount() {
2445
+ return accountFor(useBwalletX());
2446
+ }
1640
2447
  var BwalletxProvider = BwalletXProvider;
1641
2448
  // Annotate the CommonJS export names for ESM import in node:
1642
2449
  0 && (module.exports = {
@@ -1644,10 +2451,14 @@ var BwalletxProvider = BwalletXProvider;
1644
2451
  BwalletxProvider,
1645
2452
  BwalletxSignin,
1646
2453
  PaymentError,
2454
+ accountFor,
2455
+ autoChoice,
1647
2456
  brc100Option,
1648
2457
  discoverWallets,
1649
2458
  handcashAdapter,
1650
2459
  pairingOption,
1651
- useBwalletX
2460
+ signinStateFor,
2461
+ useBwalletX,
2462
+ useBwalletXAccount
1652
2463
  });
1653
2464
  //# sourceMappingURL=react.cjs.map