@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/element.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,19 +17,39 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/define.ts
21
31
  var define_exports = {};
22
32
  __export(define_exports, {
33
+ BUTTON_TAG: () => BUTTON_TAG,
34
+ CONNECTED_KEY: () => CONNECTED_KEY,
23
35
  GET_URL: () => GET_URL,
36
+ MENU_CSS: () => MENU_CSS,
37
+ PENDING_LABEL: () => PENDING_LABEL,
38
+ PENDING_LABEL_OTHER: () => PENDING_LABEL_OTHER,
39
+ PENDING_NOTE: () => PENDING_NOTE,
24
40
  SIGNIN_CSS: () => SIGNIN_CSS,
25
41
  SIGNIN_TAG: () => SIGNIN_TAG,
26
42
  WEB_WALLET_URL: () => WEB_WALLET_URL,
43
+ _resetBwalletxButtons: () => _resetBwalletxButtons,
27
44
  buildAuthorizeUrl: () => buildAuthorizeUrl,
45
+ defineBwalletxButton: () => defineBwalletxButton,
28
46
  defineBwalletxSignin: () => defineBwalletxSignin,
47
+ dropLegacyConnectedKey: () => dropLegacyConnectedKey,
48
+ getUrlFor: () => getUrlFor,
49
+ handleLabel: () => handleLabel,
29
50
  markSvg: () => markSvg,
30
- signinText: () => signinText
51
+ signinText: () => signinText,
52
+ stateFor: () => stateFor
31
53
  });
32
54
  module.exports = __toCommonJS(define_exports);
33
55
 
@@ -56,21 +78,16 @@ function discoverBwalletX(ms = DEFAULT_DISCOVER_MS) {
56
78
  finish({ wallet: d.wallet, method: rdns === RDNS_EXTENSION ? "extension" : "in-app", rdns });
57
79
  }
58
80
  };
59
- const cwiNow = () => {
60
- const cwi = window.CWI;
61
- if (cwi) finish({ wallet: cwi, method: "in-app", rdns: null });
62
- return !!cwi;
63
- };
64
81
  window.addEventListener("brc100:announceWallet", onAnnounce);
65
82
  window.dispatchEvent(new Event("brc100:requestWallet"));
66
- if (done || cwiNow()) return;
83
+ if (done) return;
67
84
  if (isFramed()) {
68
85
  void probeXdm(Math.max(ms, 1200)).then((w) => finish(w ? { wallet: w, method: "in-app", rdns: null } : null));
69
86
  return;
70
87
  }
71
88
  timer = setTimeout(() => {
72
89
  timer = void 0;
73
- if (!cwiNow()) finish(null);
90
+ finish(null);
74
91
  }, ms);
75
92
  });
76
93
  }
@@ -135,13 +152,16 @@ function prewarmBwalletX() {
135
152
  });
136
153
  });
137
154
  }
138
- function bindPrewarm(el) {
155
+ function bindPrewarm(el2) {
139
156
  const on = () => prewarmBwalletX();
140
157
  const evs = ["pointerenter", "focus", "touchstart"];
141
- for (const e of evs) el.addEventListener(e, on, { passive: true });
142
- return () => evs.forEach((e) => el.removeEventListener(e, on));
158
+ for (const e of evs) el2.addEventListener(e, on, { passive: true });
159
+ return () => evs.forEach((e) => el2.removeEventListener(e, on));
143
160
  }
144
161
 
162
+ // src/wallets.ts
163
+ var import_sdk4 = require("@bsv/sdk");
164
+
145
165
  // src/pair.ts
146
166
  var import_sdk3 = require("@bsv/sdk");
147
167
 
@@ -519,6 +539,11 @@ async function restorePairing(opts = {}) {
519
539
  sealer.restore({ sent: p.sent, lastSeen: p.lastSeen });
520
540
  return new Link(p, sealer, storage, key, opts.socket ?? defaultSocket(), opts.callTimeoutMs ?? 12e4).wallet();
521
541
  }
542
+ async function clearPairing(opts = {}) {
543
+ const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
544
+ await Promise.resolve(storage.removeItem(opts.storageKey ?? DEFAULT_KEY)).catch(() => {
545
+ });
546
+ }
522
547
 
523
548
  // src/signin.ts
524
549
  var LOGIN_PROTOCOL = [2, "bwallet sign in"];
@@ -552,27 +577,68 @@ async function connectBwalletX(opts = {}) {
552
577
  status("pairing");
553
578
  return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
554
579
  }
580
+ async function waitIfLocked(wallet) {
581
+ let ok = false;
582
+ try {
583
+ ok = !!(await wallet.isAuthenticated({}))?.authenticated;
584
+ } catch {
585
+ }
586
+ if (!ok) await Promise.resolve().then(() => wallet.waitForAuthentication({})).catch(() => {
587
+ });
588
+ }
589
+ function challengeWantsIdentityKey(e) {
590
+ const msg = e instanceof Error ? e.message : String(e ?? "");
591
+ return /identityKey/i.test(msg);
592
+ }
555
593
  async function signInWithBwalletX(opts) {
556
594
  const { wallet, method } = await connectBwalletX(opts);
557
- if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
595
+ if (method !== "pairing" && !opts.skipWaitForAuthentication) await waitIfLocked(wallet);
596
+ const status = () => {
597
+ try {
598
+ opts.onStatus?.("signing");
599
+ } catch {
600
+ }
601
+ };
602
+ const sign = (c2) => wallet.createSignature({
603
+ protocolID: c2.protocolID ?? LOGIN_PROTOCOL,
604
+ keyID: c2.keyID,
605
+ counterparty: "anyone",
606
+ data: Array.from(new TextEncoder().encode(c2.message))
558
607
  });
559
- const { publicKey: identityKey } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
608
+ const keyFirst = async () => {
609
+ const { publicKey: identityKey2 } = await atStage("identity", () => wallet.getPublicKey({ identityKey: true }));
610
+ status();
611
+ const c2 = await atStage("challenge", () => opts.challenge(identityKey2));
612
+ const { signature: signature2 } = await atStage("signature", () => sign(c2));
613
+ return { identityKey: identityKey2, signature: Array.from(signature2), nonce: c2.nonce, method, wallet };
614
+ };
615
+ if (opts.challengeNeedsIdentityKey && !opts.identityKey) return keyFirst();
616
+ let c;
560
617
  try {
561
- opts.onStatus?.("signing");
562
- } catch {
618
+ c = await atStage("challenge", () => opts.challenge(opts.identityKey));
619
+ } catch (e) {
620
+ if (!opts.identityKey && challengeWantsIdentityKey(e)) return keyFirst();
621
+ throw e;
563
622
  }
564
- const c = await atStage("challenge", () => opts.challenge(identityKey));
565
- const { signature } = await atStage(
566
- "signature",
567
- () => wallet.createSignature({
568
- protocolID: c.protocolID ?? LOGIN_PROTOCOL,
569
- keyID: c.keyID,
570
- counterparty: "anyone",
571
- data: Array.from(new TextEncoder().encode(c.message))
572
- })
573
- );
623
+ status();
624
+ const [{ publicKey: identityKey }, { signature }] = await Promise.all([
625
+ opts.identityKey ? Promise.resolve({ publicKey: opts.identityKey }) : atStage("identity", () => wallet.getPublicKey({ identityKey: true })),
626
+ atStage("signature", () => sign(c))
627
+ ]);
574
628
  return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
575
629
  }
630
+ async function signInAndVerify(opts) {
631
+ const hinted = !opts.identityKey && !!opts.identityKeyHint;
632
+ const r = await signInWithBwalletX({ ...opts, identityKey: opts.identityKey ?? opts.identityKeyHint });
633
+ try {
634
+ return { result: r, verified: await atStage("verify", () => opts.verify(r)) };
635
+ } catch (e) {
636
+ const status = e?.status;
637
+ if (!hinted || status !== 401) throw e;
638
+ const again = await signInWithBwalletX({ ...opts, wallet: r.wallet, identityKey: void 0, skipWaitForAuthentication: true });
639
+ return { result: again, verified: await atStage("verify", () => opts.verify(again)) };
640
+ }
641
+ }
576
642
  async function atStage(stage, run) {
577
643
  try {
578
644
  return await run();
@@ -583,8 +649,449 @@ async function atStage(stage, run) {
583
649
  }
584
650
  }
585
651
 
652
+ // src/wallets.ts
653
+ async function brc100Connected(wallet, o, c = {}) {
654
+ if (c.identity === false) return { wallet, payer: wallet, identityKey: null, optionId: o.id, kind: o.kind };
655
+ await waitIfLocked(wallet);
656
+ const { publicKey } = await wallet.getPublicKey({ identityKey: true });
657
+ return { wallet, payer: wallet, identityKey: publicKey, optionId: o.id, kind: o.kind };
658
+ }
659
+ function brc100Option(wallet, info) {
660
+ const opt = {
661
+ id: info.id,
662
+ name: info.name,
663
+ icon: info.icon,
664
+ rdns: info.rdns,
665
+ kind: "brc100",
666
+ isBwalletX: info.isBwalletX ?? false,
667
+ substrate: info.substrate,
668
+ wallet,
669
+ connect: (c) => brc100Connected(wallet, opt, c)
670
+ };
671
+ return opt;
672
+ }
673
+ function pairingOption(pairing) {
674
+ const opt = {
675
+ id: "bwalletx-pairing",
676
+ name: "bWalletX on your phone",
677
+ kind: "pairing",
678
+ isBwalletX: true,
679
+ substrate: "pairing",
680
+ async connect(c) {
681
+ const p = typeof pairing === "function" ? pairing() : pairing;
682
+ const w = await restorePairing(p) ?? await pairBwalletX(p);
683
+ return brc100Connected(w, opt, c);
684
+ }
685
+ };
686
+ return opt;
687
+ }
688
+ var NAMES = { [RDNS_EXTENSION]: "bWalletX", "space.bwallet.mobile": "bWalletX" };
689
+ async function probe(substrate, ms) {
690
+ try {
691
+ const w = new import_sdk4.WalletClient(substrate);
692
+ const ok = await Promise.race([w.isAuthenticated({}).then(() => true), new Promise((r) => setTimeout(() => r(false), ms))]);
693
+ return ok ? w : null;
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ function cwiIsAnnounced(cwi, announced) {
699
+ return announced.some((o) => o.wallet === cwi || o.rdns === RDNS_MOBILE);
700
+ }
701
+ async function discoverWallets(o = {}) {
702
+ const out = [];
703
+ const ms = o.ms ?? 300;
704
+ if (typeof window !== "undefined") {
705
+ const seen = /* @__PURE__ */ new Map();
706
+ const onAnnounce = (e) => {
707
+ const d = e.detail;
708
+ if (!d?.wallet) return;
709
+ const rdns = d.info?.rdns ?? d.info?.uuid ?? `wallet-${seen.size}`;
710
+ if (seen.has(rdns)) return;
711
+ const isB = BWALLETX_RDNS.includes(rdns);
712
+ const substrate = rdns === RDNS_EXTENSION ? "extension" : rdns === RDNS_MOBILE ? "in-app" : "injected";
713
+ seen.set(rdns, brc100Option(d.wallet, { id: rdns, rdns, name: NAMES[rdns] ?? d.info?.name ?? rdns, icon: d.info?.icon, isBwalletX: isB, substrate }));
714
+ };
715
+ window.addEventListener("brc100:announceWallet", onAnnounce);
716
+ window.dispatchEvent(new Event("brc100:requestWallet"));
717
+ const local = o.probeLocal ? Promise.all([probe("json-api", ms), probe("Cicada", ms)]) : Promise.resolve([null, null]);
718
+ await new Promise((r) => setTimeout(r, seen.size > 0 || window.CWI ? Math.min(ms, 50) : ms));
719
+ window.removeEventListener("brc100:announceWallet", onAnnounce);
720
+ out.push(...seen.values());
721
+ if (![...seen.values()].some((w) => w.isBwalletX) && isFramed()) {
722
+ const x = await probeXdm(Math.max(ms, 1200));
723
+ if (x) out.push(brc100Option(x, { id: "bwalletx-xdm", name: "bWalletX", isBwalletX: true, substrate: "xdm" }));
724
+ }
725
+ const cwi = window.CWI;
726
+ if (cwi && !cwiIsAnnounced(cwi, [...seen.values()])) {
727
+ out.push(brc100Option(cwi, { id: "window.CWI", name: seen.size ? "Another browser wallet" : "Browser wallet", isBwalletX: false, substrate: "window.CWI" }));
728
+ }
729
+ const [json, cicada] = await local;
730
+ if (json) out.push(brc100Option(json, { id: "localhost:3321", name: "Desktop wallet (localhost:3321)", substrate: "json-api" }));
731
+ if (cicada) out.push(brc100Option(cicada, { id: "localhost:3301", name: "Desktop wallet (localhost:3301)", substrate: "cicada" }));
732
+ }
733
+ if (o.pairing) out.push(pairingOption(o.pairing));
734
+ out.push(...o.extra ?? []);
735
+ const rank = (w) => w.isBwalletX ? 0 : w.kind === "handcash" ? 2 : 1;
736
+ return out.map((w, i) => ({ w, i })).sort((a, b) => rank(a.w) - rank(b.w) || a.i - b.i).map((x) => x.w);
737
+ }
738
+ var DESKTOP_OPTION_ID = "desktop";
739
+ function desktopOption(ms = 1500) {
740
+ const opt = {
741
+ id: DESKTOP_OPTION_ID,
742
+ name: "Desktop wallet",
743
+ kind: "brc100",
744
+ isBwalletX: false,
745
+ substrate: "json-api",
746
+ async connect(c) {
747
+ const [json, cicada] = await Promise.all([probe("json-api", ms), probe("Cicada", ms)]);
748
+ const w = json ?? cicada;
749
+ if (!w) throw Object.assign(new Error("No desktop wallet answered on this computer"), { code: "NOT_FOUND" });
750
+ opt.wallet = w;
751
+ opt.substrate = json ? "json-api" : "cicada";
752
+ return brc100Connected(w, opt, c);
753
+ }
754
+ };
755
+ return opt;
756
+ }
757
+
758
+ // src/account.ts
759
+ var PAYMAIL_LOOKUP_URL = "https://pay.bwallet.space/api/paymail/lookup?key=";
760
+ var BAVATAR_URL = "https://chat.bwalletx.com/api/bavatar/";
761
+ function shortKey(key) {
762
+ return key.length > 12 ? `${key.slice(0, 6)}\u2026${key.slice(-4)}` : key;
763
+ }
764
+ function accountLabel(a) {
765
+ if (a.paymail && /@bwalletx\.com$/i.test(a.paymail)) return "$" + a.paymail.split("@")[0];
766
+ if (a.handle) return a.handle;
767
+ if (a.paymail) return a.paymail;
768
+ return a.identityKey ? shortKey(a.identityKey) : "";
769
+ }
770
+ function avatarUrlFor(identityKey) {
771
+ return identityKey && /^0[23][0-9a-fA-F]{64}$/.test(identityKey) ? `${BAVATAR_URL}${identityKey.toLowerCase()}.png` : null;
772
+ }
773
+ var cache = /* @__PURE__ */ new Map();
774
+ function lookupPaymail(identityKey, url = PAYMAIL_LOOKUP_URL) {
775
+ if (url === "off" || typeof fetch === "undefined") return Promise.resolve(null);
776
+ const k = `${url}|${identityKey}`;
777
+ let p = cache.get(k);
778
+ if (!p) {
779
+ p = fetch(url + encodeURIComponent(identityKey)).then((r) => r.ok ? r.json() : null).then((j) => j && typeof j.paymail === "string" ? j.paymail : null).catch(() => null);
780
+ cache.set(k, p);
781
+ }
782
+ return p;
783
+ }
784
+ function isUserRejection(e) {
785
+ const code = e?.code;
786
+ if (code === "ERR_PERMISSION_DENIED" || code === 4001) return true;
787
+ const msg = e instanceof Error ? e.message : String(e ?? "");
788
+ return /denied|reject|cancel|declin|refused|not now|user closed/i.test(msg);
789
+ }
790
+ function signInErrorText(e) {
791
+ if (isUserRejection(e)) return "Sign-in cancelled";
792
+ const code = e?.code;
793
+ if (code === "NOT_FOUND") return "bWalletX was not found in this browser";
794
+ const msg = e instanceof Error ? e.message : String(e ?? "");
795
+ if (/lock/i.test(msg)) return "Your wallet is locked. Unlock it and try again";
796
+ if (/timeout|timed out|expired/i.test(msg)) return "The wallet did not answer in time";
797
+ if (/origin/i.test(msg)) return "This page is not on the site\u2019s configured origin";
798
+ return "Sign-in failed";
799
+ }
800
+ async function copyText(text) {
801
+ try {
802
+ await navigator.clipboard.writeText(text);
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ var IDENTITY_HINT_KEY = "bwx:identityKey";
809
+ function rememberedIdentity() {
810
+ try {
811
+ const k = globalThis.localStorage?.getItem(IDENTITY_HINT_KEY) ?? "";
812
+ return /^0[23][0-9a-f]{64}$/i.test(k) ? k : void 0;
813
+ } catch {
814
+ return void 0;
815
+ }
816
+ }
817
+ function rememberIdentity(key) {
818
+ try {
819
+ if (key) globalThis.localStorage?.setItem(IDENTITY_HINT_KEY, key);
820
+ else globalThis.localStorage?.removeItem(IDENTITY_HINT_KEY);
821
+ } catch {
822
+ }
823
+ }
824
+
825
+ // src/choice.ts
826
+ var BWALLETX_TYPE = "bwalletx";
827
+ function substrateOf(o) {
828
+ if (o.substrate) return o.substrate;
829
+ if (o.kind === "pairing") return "pairing";
830
+ if (o.isBwalletX && o.kind === "brc100") return o.rdns === RDNS_MOBILE ? "in-app" : o.id === "bwalletx-xdm" ? "xdm" : "extension";
831
+ if (o.kind === "handcash" || o.kind === "custom") return "adapter";
832
+ if (o.rdns === RDNS_EXTENSION) return "extension";
833
+ if (o.rdns === RDNS_MOBILE) return "in-app";
834
+ if (o.id === "window.CWI") return "window.CWI";
835
+ if (o.id === "localhost:3321" || o.id === DESKTOP_OPTION_ID) return "json-api";
836
+ if (o.id === "localhost:3301") return "cicada";
837
+ return "injected";
838
+ }
839
+ function walletTypeOf(o) {
840
+ return o.isBwalletX ? BWALLETX_TYPE : o.rdns ?? o.id;
841
+ }
842
+ var OWN_MARKERS = ["extension", "in-app", "xdm"];
843
+ function isPresentBwalletX(o) {
844
+ return o.isBwalletX && o.kind === "brc100" && !!o.wallet && OWN_MARKERS.includes(substrateOf(o));
845
+ }
846
+ function hasBwalletX(list) {
847
+ return list.some(isPresentBwalletX);
848
+ }
849
+ function otherDetected(list) {
850
+ return list.filter((o) => !o.isBwalletX && o.kind === "brc100" && !!o.wallet);
851
+ }
852
+ function chooserOrder(list) {
853
+ const rank = (o) => isPresentBwalletX(o) ? 0 : o.isBwalletX ? 1 : o.kind === "brc100" ? 2 : o.kind === "custom" ? 3 : 4;
854
+ return list.map((o, i) => ({ o, i })).sort((a, b) => rank(a.o) - rank(b.o) || a.i - b.i).map((x) => x.o);
855
+ }
856
+ function outcomeOf(e) {
857
+ const err = e;
858
+ if (err?.code === "NOT_FOUND") return "not-found";
859
+ if (err?.name === "AbortError" || err?.message === "cancelled") return "cancelled";
860
+ if (isUserRejection(e)) return "rejected";
861
+ return "error";
862
+ }
863
+ function attemptFor(option, outcome, startedAt, now = Date.now()) {
864
+ return {
865
+ walletType: option ? walletTypeOf(option) : null,
866
+ substrate: option ? substrateOf(option) : null,
867
+ outcome,
868
+ ms: Math.max(0, Math.round(now - startedAt))
869
+ };
870
+ }
871
+
872
+ // src/modal.ts
873
+ var import_qrcode_generator = __toESM(require("qrcode-generator"), 1);
874
+ var CSS = `
875
+ .bwx-ov{position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;font:15px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
876
+ .bwx-box{background:#0b0b0b;color:#f4f1e8;border:1px solid #3a3220;border-radius:16px;width:min(360px,100%);padding:20px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
877
+ .bwx-box h2{margin:0 0 6px;font-size:18px;color:#e8c766}
878
+ .bwx-box p{margin:0 0 14px;color:#bdb6a3;font-size:14px}
879
+ .bwx-qr{background:#fff;border-radius:12px;padding:12px;display:flex;justify-content:center}
880
+ .bwx-qr svg{width:100%;height:auto;max-width:260px}
881
+ .bwx-code{margin:14px 0 0;text-align:center;font-size:28px;letter-spacing:.3em;color:#e8c766;font-variant-numeric:tabular-nums}
882
+ .bwx-row{display:flex;gap:8px;margin-top:14px}
883
+ .bwx-btn{flex:1;border:1px solid #3a3220;background:#15130e;color:#f4f1e8;border-radius:10px;padding:10px 12px;font:inherit;cursor:pointer}
884
+ .bwx-btn:hover{border-color:#e8c766}
885
+ a.bwx-btn{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
886
+ .bwx-primary{background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;border:0;font-weight:700;min-height:48px;padding:13px 12px}
887
+ .bwx-stack{display:flex;flex-direction:column;gap:8px}
888
+ .bwx-list{display:flex;flex-direction:column;gap:8px}
889
+ .bwx-opt{display:flex;align-items:center;gap:10px;text-align:left}
890
+ .bwx-opt img{width:24px;height:24px;border-radius:6px}
891
+ .bwx-tag{margin-left:auto;font-size:11px;color:#8d866f;white-space:nowrap}
892
+ .bwx-opt{min-height:48px;box-sizing:border-box}
893
+ a.bwx-btn.bwx-opt{display:flex;text-align:left}
894
+ .bwx-btn:focus{outline:none}
895
+ .bwx-btn:focus-visible{outline:2px solid #e8c766;outline-offset:2px}
896
+ .bwx-mk{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
897
+ .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}
898
+ .bwx-opt img{width:28px;height:28px;border-radius:8px;flex:none}
899
+ .bwx-txt{display:flex;flex-direction:column;min-width:0;gap:1px}
900
+ .bwx-txt b{font-weight:600}
901
+ .bwx-txt small{font-size:12px;color:#8d866f;font-weight:400}
902
+ .bwx-primary .bwx-txt small{color:rgba(0,0,0,.68)}
903
+ .bwx-primary .bwx-tag{color:rgba(0,0,0,.6)}
904
+ .bwx-sep{font-size:12px;color:#8d866f;margin:6px 2px 0}
905
+ .bwx-foot{font-size:12px;color:#8d866f;margin:12px 0 0}
906
+ `;
907
+ function mount() {
908
+ const root = document.createElement("div");
909
+ root.className = "bwx-ov";
910
+ root.setAttribute("role", "dialog");
911
+ root.setAttribute("aria-modal", "true");
912
+ const style = document.createElement("style");
913
+ style.textContent = CSS;
914
+ const box = document.createElement("div");
915
+ box.className = "bwx-box";
916
+ root.append(style, box);
917
+ document.body.appendChild(root);
918
+ return { root, box, close: () => root.remove() };
919
+ }
920
+ function el(tag, cls, text) {
921
+ const e = document.createElement(tag);
922
+ if (cls) e.className = cls;
923
+ if (text != null) e.textContent = text;
924
+ return e;
925
+ }
926
+ function qrSvg(text) {
927
+ const q = (0, import_qrcode_generator.default)(0, "M");
928
+ q.addData(text);
929
+ q.make();
930
+ return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true });
931
+ }
932
+ function isTouchDevice(env = globalThis) {
933
+ const nav = env.navigator;
934
+ if (!nav) return false;
935
+ const ua = nav.userAgent ?? "";
936
+ if (/Android|iPhone|iPad|iPod|Mobile|Silk|Kindle/i.test(ua)) return true;
937
+ if (/Macintosh/.test(ua) && (nav.maxTouchPoints ?? 0) > 1) return true;
938
+ try {
939
+ return !!env.matchMedia?.("(pointer: coarse)").matches && !env.matchMedia?.("(hover: hover)").matches;
940
+ } catch {
941
+ return false;
942
+ }
943
+ }
944
+ function pairingModal(o = {}) {
945
+ const ac = new AbortController();
946
+ let ui = null;
947
+ let codeEl = null;
948
+ const close = () => {
949
+ ui?.close();
950
+ ui = null;
951
+ };
952
+ ac.signal.addEventListener("abort", close);
953
+ return {
954
+ signal: ac.signal,
955
+ close,
956
+ onLink(link) {
957
+ if (typeof document === "undefined") return;
958
+ close();
959
+ ui = mount();
960
+ const app = o.mode === "app" || (o.mode ?? "auto") === "auto" && isTouchDevice();
961
+ const h = el("h2", "", o.title ?? "Connect bWalletX");
962
+ const cancel = el("button", "bwx-btn", "Cancel");
963
+ cancel.onclick = () => {
964
+ o.onCancel?.();
965
+ ac.abort();
966
+ };
967
+ codeEl = el("div", "bwx-code", "");
968
+ codeEl.setAttribute("aria-live", "polite");
969
+ if (app) {
970
+ const p2 = el("p", "", "Open bWalletX, approve, then come back to this page.");
971
+ const stack = el("div", "bwx-stack");
972
+ const open = el("a", "bwx-btn bwx-primary", "Open bWalletX");
973
+ open.href = link;
974
+ open.rel = "noopener";
975
+ const web = el("button", "bwx-btn", "Use web.bwalletx.com instead");
976
+ web.onclick = () => {
977
+ void navigator.clipboard?.writeText(link).catch(() => void 0);
978
+ window.open(WEB_WALLET_URL, "_blank", "noopener");
979
+ p2.textContent = "Link copied. In web.bwalletx.com tap Connect and paste it, then come back here.";
980
+ };
981
+ stack.append(open, web, cancel);
982
+ ui.box.append(h, p2, codeEl, stack);
983
+ return;
984
+ }
985
+ const p = el("p", "", "Scan with bWalletX on your phone, or paste the link into web.bwalletx.com.");
986
+ const qr = el("div", "bwx-qr");
987
+ qr.innerHTML = qrSvg(link);
988
+ const row = el("div", "bwx-row");
989
+ const copy = el("button", "bwx-btn", "Copy link");
990
+ copy.onclick = () => void navigator.clipboard?.writeText(link).then(() => copy.textContent = "Copied");
991
+ row.append(copy, cancel);
992
+ ui.box.append(h, p, qr, codeEl, row);
993
+ },
994
+ onCode(code) {
995
+ if (codeEl) {
996
+ codeEl.textContent = code;
997
+ const p = ui?.box.querySelector("p");
998
+ if (p) p.textContent = "Check bWalletX shows the same code, then tap Connect.";
999
+ }
1000
+ }
1001
+ };
1002
+ }
1003
+ function optionRow(w, primary, sub, tag) {
1004
+ const b = el("button", `bwx-btn bwx-opt${primary ? " bwx-primary" : ""}`);
1005
+ b.type = "button";
1006
+ b.dataset.wallet = w.id;
1007
+ if (w.isBwalletX) {
1008
+ const mk = el("span", "bwx-mk");
1009
+ mk.innerHTML = markSvg(18);
1010
+ b.append(mk);
1011
+ } else if (w.icon) {
1012
+ const img = el("img");
1013
+ img.src = w.icon;
1014
+ img.alt = "";
1015
+ b.append(img);
1016
+ } else {
1017
+ b.append(el("span", "bwx-ico", (w.name.trim().charAt(0) || "W").toUpperCase()));
1018
+ }
1019
+ const t = el("span", "bwx-txt");
1020
+ t.append(el("b", "", w.name));
1021
+ if (sub) t.append(el("small", "", sub));
1022
+ b.append(t);
1023
+ if (tag) b.append(el("span", "bwx-tag", tag));
1024
+ return b;
1025
+ }
1026
+ function chooseWallet(options, o = {}) {
1027
+ if (typeof document === "undefined") return Promise.resolve(null);
1028
+ return new Promise((resolve) => {
1029
+ const ui = mount();
1030
+ ui.root.dataset.bwxChooser = "";
1031
+ const done = (w) => {
1032
+ document.removeEventListener("keydown", onKey, true);
1033
+ ui.close();
1034
+ resolve(w);
1035
+ };
1036
+ const onKey = (e) => {
1037
+ if (e.key === "Escape") {
1038
+ e.preventDefault();
1039
+ done(null);
1040
+ }
1041
+ };
1042
+ document.addEventListener("keydown", onKey, true);
1043
+ ui.box.append(el("h2", "", o.title ?? "Choose a wallet"));
1044
+ const list = el("div", "bwx-list");
1045
+ const here = hasBwalletX(options);
1046
+ if (!here) {
1047
+ const get = el("a", "bwx-btn bwx-opt bwx-primary");
1048
+ get.href = o.getUrl ?? GET_URL;
1049
+ get.target = "_blank";
1050
+ get.rel = "noopener";
1051
+ get.dataset.wallet = "get-bwalletx";
1052
+ const mk = el("span", "bwx-mk");
1053
+ mk.innerHTML = markSvg(18);
1054
+ const t = el("span", "bwx-txt");
1055
+ t.append(el("b", "", "Get bWalletX"), el("small", "", "Free. Every feature on this site"));
1056
+ get.append(mk, t);
1057
+ list.append(get);
1058
+ }
1059
+ const ordered = chooserOrder(options);
1060
+ const touch = isTouchDevice();
1061
+ let sep = false;
1062
+ for (const w of ordered) {
1063
+ if (!here && !sep) {
1064
+ list.append(el("p", "bwx-sep", "Or use"));
1065
+ sep = true;
1066
+ }
1067
+ const sub = isPresentBwalletX(w) ? "Recommended" : w.kind === "pairing" ? touch ? "Open the bWalletX app" : "Scan a QR with your phone" : void 0;
1068
+ const tag = w.kind === "handcash" ? "until BRC-100" : !w.isBwalletX && w.kind === "brc100" ? "Fewer features" : void 0;
1069
+ const row2 = optionRow(w, isPresentBwalletX(w), sub, tag);
1070
+ row2.onclick = () => done(w);
1071
+ list.append(row2);
1072
+ }
1073
+ if (o.desktop !== false && !options.some((w) => w.substrate === "json-api" || w.substrate === "cicada")) {
1074
+ if (!here && !sep) list.append(el("p", "bwx-sep", "Or use"));
1075
+ const d = desktopOption();
1076
+ const row2 = optionRow(d, false, "Metanet or BSV Desktop on this computer", "Fewer features");
1077
+ row2.onclick = () => done(d);
1078
+ list.append(row2);
1079
+ }
1080
+ ui.box.append(list);
1081
+ if (otherDetected(options).length || !here) ui.box.append(el("p", "bwx-foot", "Some features need bWalletX."));
1082
+ const row = el("div", "bwx-row");
1083
+ const cancel = el("button", "bwx-btn", "Cancel");
1084
+ cancel.type = "button";
1085
+ cancel.onclick = () => done(null);
1086
+ row.append(cancel);
1087
+ ui.box.append(row);
1088
+ ui.root.onclick = (e) => e.target === ui.root && done(null);
1089
+ list.querySelector("a,button")?.focus();
1090
+ });
1091
+ }
1092
+
586
1093
  // src/access-client.ts
587
- var import_sdk4 = require("@bsv/sdk");
1094
+ var import_sdk5 = require("@bsv/sdk");
588
1095
 
589
1096
  // src/access.ts
590
1097
  var ACCESS_PROTOCOL = [2, "bwalletx access"];
@@ -722,7 +1229,7 @@ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
722
1229
  const counterparty = ci.counterparty ?? "self";
723
1230
  for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
724
1231
  const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
725
- const h = import_sdk4.Utils.toHex(import_sdk4.Hash.hash160(import_sdk4.PublicKey.fromString(publicKey).encode(true)));
1232
+ const h = import_sdk5.Utils.toHex(import_sdk5.Hash.hash160(import_sdk5.PublicKey.fromString(publicKey).encode(true)));
726
1233
  if (want && h !== want) continue;
727
1234
  const existing = byKey.get(publicKey);
728
1235
  if (existing) {
@@ -772,7 +1279,7 @@ async function requestAccess(o) {
772
1279
  var SIGNIN_TAG = "bwalletx-signin";
773
1280
  var GET_URL = "https://bwalletx.com/get";
774
1281
  var WEB_WALLET_URL = "https://web.bwalletx.com";
775
- var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
1282
+ var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect", get: "Get", play: "Play with" };
776
1283
  function signinText(label = "signin", brand = "bWalletX") {
777
1284
  return `${VERBS[label] ?? VERBS.signin} ${brand}`;
778
1285
  }
@@ -827,7 +1334,50 @@ button[aria-busy=true]{cursor:wait}
827
1334
  .nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
828
1335
  :host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
829
1336
  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
1337
+ /* pending: one plain line under the button */
1338
+ .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}
1339
+ :host([theme=dark]) .note,:host-context(.dark) .note{color:#a1a1aa}
1340
+ /* "Other wallets" (0.5.6): one small link under the button */
1341
+ .ow{margin:8px 0 0;width:0;min-width:100%;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
1342
+ :host([theme=dark]) .ow,:host-context(.dark) .ow{color:#a1a1aa}
1343
+ .lnk{all:unset;cursor:pointer;color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
1344
+ .lnk:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
1345
+ /* error: one line plus Retry */
1346
+ .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}
1347
+ :host([theme=dark]) .err,:host-context(.dark) .err{color:#fca5a5}
1348
+ /* signed in: the badge */
1349
+ 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;
1350
+ background:#0b0b0c;color:#ffd24d;border:1px solid rgba(255,210,77,.55);box-shadow:none;font-size:15px}
1351
+ button.chip:hover{background:#151517;border-color:#ffd24d}
1352
+ button.chip:active{background:#000;transform:none}
1353
+ button.chip:focus-visible{outline:2px solid #ffd24d;outline-offset:3px}
1354
+ :host([theme=light]) button.chip{background:#fff;color:#09090b;border-color:#f59e0b}
1355
+ :host([theme=light]) button.chip:hover{background:#fffbeb}
1356
+ :host([full-width]) button.chip{width:100%}
1357
+ .av{position:relative;flex:none;display:inline-grid;place-items:center;width:30px;height:30px;border-radius:50%;overflow:hidden;
1358
+ background:radial-gradient(circle at 30% 30%,#ffe27a,#f5b800 55%,#b88600);color:#000;font-weight:800;font-size:14px}
1359
+ .av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
1360
+ .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:min(52vw,260px)}
1361
+ .dot{flex:none;width:8px;height:8px;border-radius:50%;background:#ffd24d;box-shadow:0 0 0 3px rgba(255,210,77,.22)}
1362
+ :host([size=sm]) button.chip{font-size:13px;padding:3px 12px 3px 3px;gap:8px}
1363
+ :host([size=sm]) .av{width:26px;height:26px}
1364
+ :host([size=icon]) button.chip{width:48px;height:48px;padding:0;justify-content:center}
1365
+ :host([size=icon]) .name,:host([size=icon]) .dot{display:none}
1366
+ :host([size=icon]) .av{width:40px;height:40px}
830
1367
  `;
1368
+ var MENU_CSS = `
1369
+ :host{all:initial}
1370
+ .menu{position:fixed;z-index:2147483600;box-sizing:border-box;min-width:200px;padding:6px;border-radius:14px;background:#0b0b0c;color:#ffd24d;
1371
+ 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}
1372
+ .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}
1373
+ .item{all:unset;box-sizing:border-box;display:block;width:100%;padding:11px 10px;border-radius:10px;cursor:pointer;color:#ffd24d}
1374
+ .item:hover,.item:focus-visible{background:#ffd24d;color:#000;outline:none}
1375
+ .tip{position:fixed;z-index:2147483600;box-sizing:border-box;padding:8px 12px;border-radius:10px;background:#0b0b0c;color:#e7d9a8;
1376
+ 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}
1377
+ `;
1378
+ var PENDING_NOTE = "Sign in: shares only your public identity key, moves no money";
1379
+ var PENDING_LABEL = "Approve in bWalletX\u2026";
1380
+ var PENDING_LABEL_OTHER = "Approve in your wallet\u2026";
831
1381
  function b64url2(bytes) {
832
1382
  let s = "";
833
1383
  for (const b of bytes) s += String.fromCharCode(b);
@@ -860,14 +1410,15 @@ async function buildAuthorizeUrl(p) {
860
1410
  async function postJson(url, body) {
861
1411
  const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
862
1412
  const data = await res.json().catch(() => ({}));
863
- if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
1413
+ if (!res.ok) throw Object.assign(new Error(data.error ?? `HTTP ${res.status}`), { status: res.status });
864
1414
  return data;
865
1415
  }
866
1416
  async function postAccess(url, body) {
867
1417
  const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
868
1418
  return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
869
1419
  }
870
- var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
1420
+ 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"];
1421
+ var STATES = ["signed-out", "restoring", "pending", "signed-in", "error"];
871
1422
  function createClass() {
872
1423
  return class BwalletxSigninElement extends HTMLElement {
873
1424
  static get observedAttributes() {
@@ -876,12 +1427,23 @@ function createClass() {
876
1427
  #root;
877
1428
  #notFound = false;
878
1429
  #busy = false;
1430
+ #phase = "signed-out";
1431
+ #account = null;
1432
+ #error = "";
1433
+ #restored = false;
1434
+ #pendingLabel = PENDING_LABEL;
1435
+ #menu = null;
879
1436
  constructor() {
880
1437
  super();
881
1438
  this.#root = this.attachShadow({ mode: "open" });
882
1439
  }
883
1440
  connectedCallback() {
884
1441
  this.#render();
1442
+ void this.#restore();
1443
+ }
1444
+ disconnectedCallback() {
1445
+ this.#menu?.close(false);
1446
+ this.#hideTip();
885
1447
  }
886
1448
  attributeChangedCallback() {
887
1449
  if (this.isConnected) this.#render();
@@ -893,93 +1455,402 @@ function createClass() {
893
1455
  get notFound() {
894
1456
  return this.#notFound;
895
1457
  }
1458
+ /** The state shown now (the `state` attribute when set, else the element's own). */
1459
+ get state() {
1460
+ const a = this.#attr("state");
1461
+ return a && STATES.includes(a) ? a : this.#phase;
1462
+ }
1463
+ /** The signed-in account, or null. */
1464
+ get account() {
1465
+ if (!this.#controlled) return this.#account;
1466
+ const identityKey = this.#attr("identity-key");
1467
+ return identityKey ? { identityKey, paymail: this.#attr("paymail"), handle: this.#attr("handle"), avatarUrl: this.#attr("avatar-url") } : null;
1468
+ }
1469
+ /** True while the menu is open (tests). */
1470
+ get menuOpen() {
1471
+ return !!this.#menu;
1472
+ }
896
1473
  focus(opts) {
897
1474
  this.button?.focus(opts);
898
1475
  }
1476
+ get #controlled() {
1477
+ return this.hasAttribute("state");
1478
+ }
1479
+ get #stateful() {
1480
+ return !this.hasAttribute("stateless");
1481
+ }
899
1482
  #attr(name) {
900
1483
  return this.getAttribute(name);
901
1484
  }
1485
+ /** Restore the badge from session-url once, with no wallet call. */
1486
+ async #restore() {
1487
+ const url = this.#attr("session-url");
1488
+ if (this.#restored || !url || this.#controlled || !this.#stateful) return;
1489
+ this.#restored = true;
1490
+ this.#set("restoring");
1491
+ try {
1492
+ const r = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
1493
+ const j = await r.json().catch(() => ({}));
1494
+ if (this.#phase !== "restoring") return;
1495
+ if (j.identityKey) this.#signedIn({ identityKey: j.identityKey, handle: j.handle ?? null, paymail: j.paymail ?? null });
1496
+ else this.#set("signed-out");
1497
+ } catch {
1498
+ if (this.#phase === "restoring") this.#set("signed-out");
1499
+ }
1500
+ }
1501
+ #set(p) {
1502
+ this.#phase = p;
1503
+ if (p !== "signed-in") this.#menu?.close(false);
1504
+ this.#render();
1505
+ }
1506
+ #signedIn(a) {
1507
+ this.#account = a;
1508
+ this.#error = "";
1509
+ this.#set("signed-in");
1510
+ const lookup = this.#attr("lookup-url");
1511
+ if (!a.paymail && lookup !== "off") {
1512
+ void lookupPaymail(a.identityKey, lookup ?? void 0).then((pm) => {
1513
+ if (pm && this.#account?.identityKey === a.identityKey && !this.#account.paymail) {
1514
+ this.#account = { ...this.#account, paymail: pm };
1515
+ this.#render();
1516
+ }
1517
+ });
1518
+ }
1519
+ }
902
1520
  #render() {
903
1521
  const size = this.#attr("size") ?? "lg";
904
1522
  const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
905
- const text = signinText(this.#attr("label") ?? "signin", brand);
1523
+ const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1524
+ const state = this.state;
1525
+ const acct = this.account;
1526
+ if (state === "signed-in" && acct) {
1527
+ this.#hideTip();
1528
+ const name = accountLabel(acct);
1529
+ const avatar = acct.avatarUrl ?? avatarUrlFor(acct.identityKey);
1530
+ const initial = (name.replace(/^\$/, "").charAt(0) || "b").toUpperCase();
1531
+ 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>`;
1532
+ const img = this.#root.querySelector("img");
1533
+ if (img) img.addEventListener("error", () => img.remove(), { once: true });
1534
+ const chip = this.button;
1535
+ chip.addEventListener("click", () => this.#menu ? this.#menu.close(true) : this.#openMenu());
1536
+ chip.addEventListener("keydown", (e) => {
1537
+ if ((e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") && !this.#menu) {
1538
+ e.preventDefault();
1539
+ this.#openMenu();
1540
+ }
1541
+ });
1542
+ return;
1543
+ }
1544
+ const pending = state === "pending";
1545
+ const restoring = state === "restoring";
1546
+ const error = state === "error";
1547
+ const pendingLabel = this.#controlled ? PENDING_LABEL : this.#pendingLabel;
1548
+ const text = pending ? pendingLabel : error ? "Retry" : signinText(this.#attr("label") ?? "signin", brand);
906
1549
  const subAttr = this.#attr("subtitle");
907
1550
  const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
908
- const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
909
- const loading = this.hasAttribute("loading") || this.#busy;
1551
+ const subtitle = pending || error ? null : subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
1552
+ const loading = this.hasAttribute("loading") || this.#busy || pending || restoring;
910
1553
  const disabled = this.hasAttribute("disabled");
911
1554
  const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
912
- const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
1555
+ const aria = pending ? pendingLabel : this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
913
1556
  const getUrl = this.#attr("get-url") ?? GET_URL;
914
1557
  const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
915
- const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1558
+ const errText = error ? (this.#controlled ? this.#attr("error") : this.#error) || "Sign-in failed" : "";
916
1559
  const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
917
1560
  const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
918
- 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>` : "";
919
- 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}`;
1561
+ const otherOff = this.#attr("other-wallets") === "off";
1562
+ const otherLink = '<button type="button" class="lnk" data-act="other" part="other-wallets-link">Other wallets</button>';
1563
+ 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>` : "";
1564
+ const ow = !this.#notFound && !pending && !restoring && this.#otherWalletsShown(size) ? `<p class="ow" part="other-wallets">${otherLink}</p>` : "";
1565
+ const err = error ? `<p class="err" part="error" role="alert">${esc(errText)}</p>` : "";
1566
+ const compact = size === "sm" || size === "icon";
1567
+ const note = pending && !compact ? `<p class="note" part="note">${esc(PENDING_NOTE)}</p>` : "";
1568
+ const status = pending ? `<span class="sr" role="status">${esc(pendingLabel)}</span>` : loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : "";
1569
+ 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}`;
920
1570
  this.button.addEventListener("click", () => void this.#click());
1571
+ for (const l of Array.from(this.#root.querySelectorAll("[data-act=other]"))) l.addEventListener("click", () => void this.otherWallets());
921
1572
  bindPrewarm(this.button);
1573
+ if (pending && compact) this.#showTip(PENDING_NOTE);
1574
+ else this.#hideTip();
1575
+ }
1576
+ #tip = null;
1577
+ #showTip(text) {
1578
+ const b = this.button;
1579
+ if (!b || typeof document === "undefined") return;
1580
+ if (!this.#tip) {
1581
+ const host = document.createElement("div");
1582
+ host.setAttribute("data-bwalletx-tip", "");
1583
+ const root = host.attachShadow({ mode: "open" });
1584
+ root.innerHTML = `<style>${MENU_CSS}</style><div class="tip" role="note"></div>`;
1585
+ document.body.appendChild(host);
1586
+ this.#tip = host;
1587
+ }
1588
+ const tip = this.#tip.shadowRoot.querySelector(".tip");
1589
+ tip.textContent = text;
1590
+ const r = b.getBoundingClientRect();
1591
+ const vw = window.innerWidth || document.documentElement.clientWidth || 360;
1592
+ const width = Math.min(280, vw - 32);
1593
+ tip.style.width = `${width}px`;
1594
+ tip.style.left = `${Math.max(16, Math.min(r.right - width, vw - width - 16))}px`;
1595
+ tip.style.top = `${r.bottom + 8}px`;
1596
+ }
1597
+ #hideTip() {
1598
+ this.#tip?.remove();
1599
+ this.#tip = null;
1600
+ }
1601
+ #openMenu() {
1602
+ const acct = this.account;
1603
+ const chip = this.button;
1604
+ if (!acct || !chip || typeof document === "undefined") return;
1605
+ const name = accountLabel(acct);
1606
+ const host = document.createElement("div");
1607
+ host.setAttribute("data-bwalletx-menu", "");
1608
+ const root = host.attachShadow({ mode: "open" });
1609
+ const style = document.createElement("style");
1610
+ style.textContent = MENU_CSS;
1611
+ const menu = document.createElement("div");
1612
+ menu.className = "menu";
1613
+ menu.setAttribute("role", "menu");
1614
+ menu.setAttribute("aria-label", "bWalletX account");
1615
+ const who = document.createElement("div");
1616
+ who.className = "who";
1617
+ who.textContent = `Signed in as ${acct.paymail || name}`;
1618
+ const item = (text, run) => {
1619
+ const b = document.createElement("button");
1620
+ b.type = "button";
1621
+ b.className = "item";
1622
+ b.setAttribute("role", "menuitem");
1623
+ b.tabIndex = -1;
1624
+ b.textContent = text;
1625
+ b.addEventListener("click", () => run(b));
1626
+ return b;
1627
+ };
1628
+ const copyWhat = acct.paymail ? "paymail" : "key";
1629
+ const copy = item(`Copy ${copyWhat}`, (b) => {
1630
+ void copyText(acct.paymail || acct.identityKey).then((ok) => {
1631
+ b.textContent = ok ? "Copied" : `Copy ${copyWhat}`;
1632
+ setTimeout(() => close(true), ok ? 600 : 0);
1633
+ });
1634
+ });
1635
+ const out = item("Disconnect", () => {
1636
+ close(false);
1637
+ void this.signOut();
1638
+ });
1639
+ menu.append(who, copy, out);
1640
+ root.append(style, menu);
1641
+ document.body.appendChild(host);
1642
+ const place = () => {
1643
+ const r = chip.getBoundingClientRect();
1644
+ const vw = window.innerWidth || document.documentElement.clientWidth || 360;
1645
+ const vh = window.innerHeight || document.documentElement.clientHeight || 640;
1646
+ const width = Math.min(260, vw - 32);
1647
+ menu.style.width = `${width}px`;
1648
+ const left = Math.max(16, Math.min(r.right - width, vw - width - 16));
1649
+ menu.style.left = `${left}px`;
1650
+ const h = menu.offsetHeight || 150;
1651
+ const below = r.bottom + 8;
1652
+ menu.style.top = `${below + h > vh && r.top - 8 - h > 0 ? r.top - 8 - h : below}px`;
1653
+ };
1654
+ place();
1655
+ const items = [copy, out];
1656
+ const focusAt = (i) => items[(i + items.length) % items.length].focus();
1657
+ const onKey = (e) => {
1658
+ const i = items.indexOf(root.activeElement);
1659
+ if (e.key === "Escape") {
1660
+ e.preventDefault();
1661
+ close(true);
1662
+ } else if (e.key === "ArrowDown") {
1663
+ e.preventDefault();
1664
+ focusAt(i + 1);
1665
+ } else if (e.key === "ArrowUp") {
1666
+ e.preventDefault();
1667
+ focusAt(i - 1);
1668
+ } else if (e.key === "Home") {
1669
+ e.preventDefault();
1670
+ focusAt(0);
1671
+ } else if (e.key === "End") {
1672
+ e.preventDefault();
1673
+ focusAt(items.length - 1);
1674
+ } else if (e.key === "Tab") close(false);
1675
+ };
1676
+ const onDown = (e) => {
1677
+ const path = e.composedPath?.() ?? [];
1678
+ if (!path.includes(host) && !path.includes(this)) close(false);
1679
+ };
1680
+ const onMove = () => close(false);
1681
+ menu.addEventListener("keydown", onKey);
1682
+ document.addEventListener("pointerdown", onDown, true);
1683
+ window.addEventListener("resize", onMove);
1684
+ window.addEventListener("scroll", place, true);
1685
+ const close = (focusChip = false) => {
1686
+ if (this.#menu?.host !== host) return;
1687
+ this.#menu = null;
1688
+ menu.removeEventListener("keydown", onKey);
1689
+ document.removeEventListener("pointerdown", onDown, true);
1690
+ window.removeEventListener("resize", onMove);
1691
+ window.removeEventListener("scroll", place, true);
1692
+ host.remove();
1693
+ this.button?.setAttribute("aria-expanded", "false");
1694
+ if (focusChip) this.button?.focus();
1695
+ };
1696
+ this.#menu = { host, close };
1697
+ chip.setAttribute("aria-expanded", "true");
1698
+ focusAt(0);
1699
+ }
1700
+ /**
1701
+ * Disconnect: emits bwalletx-disconnect (cancelable). Unless a listener cancels it (BwalletXProvider signs out
1702
+ * itself) or the element is driven by `state`, posts logout-url and shows signed out.
1703
+ */
1704
+ async signOut() {
1705
+ this.#menu?.close(false);
1706
+ if (!this.#emit("bwalletx-disconnect", { element: this, account: this.account }, true)) return;
1707
+ if (this.#controlled) return;
1708
+ const url = this.#attr("logout-url");
1709
+ if (url) await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: "{}" }).catch(() => void 0);
1710
+ this.#account = null;
1711
+ this.#set("signed-out");
1712
+ this.#emit("bwalletx-signed-out");
1713
+ this.button?.focus();
922
1714
  }
923
1715
  #emit(type, detail, cancelable = false) {
924
1716
  return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
925
1717
  }
926
- #setBusy(on) {
927
- this.#busy = on;
928
- this.#render();
1718
+ /** "Other wallets" is offered under the button: other-wallets on, or auto at lg/md when this element signs in. */
1719
+ #otherWalletsShown(size) {
1720
+ const a = this.#attr("other-wallets");
1721
+ if (a === "off") return false;
1722
+ if (a === "on") return true;
1723
+ return (size === "lg" || size === "md") && (this.#controlled || !!this.#attr("challenge-url"));
929
1724
  }
930
- async #click() {
931
- if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
932
- if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
933
- const href = this.#attr("href");
934
- const authorizeUrl = this.#attr("authorize-url");
935
- if (authorizeUrl) {
936
- location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
937
- return;
1725
+ /** Open the wallet chooser (the "Other wallets" link): phone QR, other browser wallets, desktop wallets. */
1726
+ otherWallets() {
1727
+ return this.#click("chooser");
1728
+ }
1729
+ #attempt(option, outcome, started) {
1730
+ this.#emit("bwalletx-attempt", attemptFor(option, outcome, started));
1731
+ }
1732
+ /**
1733
+ * Which wallet: bWalletX by its own marker goes straight to its sheet; otherwise the chooser ("Get bWalletX" first)
1734
+ * when another wallet is here or the user asked; otherwise 'not-found'. null: the chooser was closed.
1735
+ */
1736
+ async #choose(mode, ms, pairing) {
1737
+ if (mode !== "chooser") {
1738
+ const f = await discoverBwalletX(ms);
1739
+ if (f) {
1740
+ const substrate = f.rdns === RDNS_EXTENSION ? "extension" : f.rdns === RDNS_MOBILE ? "in-app" : "xdm";
1741
+ return brc100Option(f.wallet, { id: f.rdns ?? "bwalletx-xdm", rdns: f.rdns ?? void 0, name: "bWalletX", isBwalletX: true, substrate });
1742
+ }
938
1743
  }
939
- if (href) {
940
- location.assign(href);
941
- return;
1744
+ const list = await discoverWallets({ ms: 50, pairing: () => pairing });
1745
+ if (mode !== "chooser" && !hasBwalletX(list) && !otherDetected(list).length) return "not-found";
1746
+ return chooseWallet(list, { getUrl: this.#attr("get-url") ?? void 0 });
1747
+ }
1748
+ async #click(mode = "signin") {
1749
+ if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy || this.state === "pending") return;
1750
+ if (!this.#emit("bwalletx-signin", { element: this, mode }, true)) return;
1751
+ if (mode === "signin") {
1752
+ const href = this.#attr("href");
1753
+ const authorizeUrl = this.#attr("authorize-url");
1754
+ if (authorizeUrl) {
1755
+ location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
1756
+ return;
1757
+ }
1758
+ if (href) {
1759
+ location.assign(href);
1760
+ return;
1761
+ }
942
1762
  }
943
1763
  const challengeUrl = this.#attr("challenge-url");
944
1764
  if (!challengeUrl) return;
1765
+ const own = this.#stateful && !this.#controlled;
1766
+ const prev = this.#phase === "signed-in" ? "signed-in" : "signed-out";
1767
+ const started = Date.now();
1768
+ const pairing = pairingModal();
1769
+ let option = null;
945
1770
  this.#notFound = false;
946
- this.#setBusy(true);
1771
+ this.#busy = true;
1772
+ this.#pendingLabel = PENDING_LABEL;
1773
+ if (own) this.#set("pending");
1774
+ else this.#render();
947
1775
  try {
948
1776
  const ms = Number(this.#attr("discover-ms")) || DEFAULT_DISCOVER_MS;
949
- const found = await discoverBwalletX(ms);
950
- if (!found) {
1777
+ const picked = await this.#choose(mode, ms, pairing);
1778
+ if (picked === "not-found") {
951
1779
  this.#notFound = true;
1780
+ if (own) this.#phase = "signed-out";
1781
+ this.#attempt(null, "not-found", started);
952
1782
  this.#emit("bwalletx-not-found");
953
1783
  if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
954
1784
  return;
955
1785
  }
1786
+ if (!picked) {
1787
+ if (own) this.#phase = prev;
1788
+ this.#attempt(null, "cancelled", started);
1789
+ return;
1790
+ }
1791
+ option = picked;
1792
+ if (!option.isBwalletX) {
1793
+ this.#pendingLabel = PENDING_LABEL_OTHER;
1794
+ if (own) this.#render();
1795
+ }
1796
+ const cw = await option.connect({ identity: false });
1797
+ const wallet = cw.wallet;
1798
+ if (!wallet) throw Object.assign(new Error("This wallet cannot sign in here"), { code: "NOT_SUPPORTED" });
1799
+ const sub = substrateOf(option);
1800
+ const method = sub === "extension" ? "extension" : sub === "in-app" || sub === "xdm" ? "in-app" : sub === "pairing" ? "pairing" : "wallet";
956
1801
  const requireToken = this.#attr("require-token");
957
1802
  if (requireToken) {
1803
+ if (own) this.#phase = "signed-out";
958
1804
  const verifyUrl2 = this.#attr("verify-url");
959
1805
  const min = this.#attr("min");
960
1806
  const minBps = this.#attr("min-bps");
961
1807
  const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
962
1808
  try {
963
1809
  const a = await requestAccess({
964
- wallet: found.wallet,
1810
+ wallet,
965
1811
  challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
966
1812
  verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
967
1813
  });
1814
+ this.#attempt(option, "signed-in", started);
968
1815
  this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
969
1816
  } catch (e) {
970
1817
  const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1818
+ this.#attempt(option, outcomeOf(e), started);
971
1819
  this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
972
1820
  }
973
1821
  return;
974
1822
  }
975
- const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
976
1823
  const verifyUrl = this.#attr("verify-url");
977
- const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
978
- this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
1824
+ const { result: r, verified } = await signInAndVerify({
1825
+ wallet,
1826
+ identityKeyHint: verifyUrl ? rememberedIdentity() : void 0,
1827
+ challenge: (identityKey) => postJson(challengeUrl, identityKey ? { identityKey } : {}),
1828
+ verify: async (res) => verifyUrl ? await postJson(verifyUrl, { identityKey: res.identityKey, nonce: res.nonce, signature: res.signature }) : {}
1829
+ });
1830
+ if (verifyUrl) rememberIdentity(r.identityKey);
1831
+ const response = verifyUrl ? verified : null;
1832
+ if (own) this.#signedIn({ identityKey: r.identityKey, handle: response?.handle ?? null, paymail: response?.paymail ?? null });
1833
+ this.#attempt(option, "signed-in", started);
1834
+ this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method, nonce: r.nonce, signature: r.signature, response });
979
1835
  } catch (error) {
980
- this.#emit("bwalletx-error", { error });
1836
+ const outcome = outcomeOf(error);
1837
+ this.#attempt(option, outcome, started);
1838
+ const closedQr = outcome === "cancelled" && option?.kind === "pairing";
1839
+ if (own) {
1840
+ if (closedQr) this.#phase = prev;
1841
+ else {
1842
+ this.#error = signInErrorText(error);
1843
+ this.#phase = "error";
1844
+ }
1845
+ }
1846
+ if (!closedQr) this.#emit("bwalletx-error", { error });
981
1847
  } finally {
982
- this.#setBusy(false);
1848
+ this.#busy = false;
1849
+ try {
1850
+ pairing.close();
1851
+ } catch {
1852
+ }
1853
+ this.#render();
983
1854
  }
984
1855
  }
985
1856
  };
@@ -993,17 +1864,232 @@ function defineBwalletxSignin(tag = SIGNIN_TAG) {
993
1864
  return C;
994
1865
  }
995
1866
 
1867
+ // src/wallet-button.ts
1868
+ var BUTTON_TAG = "bwalletx-button";
1869
+ var CONNECTED_KEY = "bwx:connected";
1870
+ function dropLegacyConnectedKey() {
1871
+ try {
1872
+ const had = globalThis.localStorage?.getItem(CONNECTED_KEY) != null;
1873
+ if (had) globalThis.localStorage?.removeItem(CONNECTED_KEY);
1874
+ return had;
1875
+ } catch {
1876
+ return false;
1877
+ }
1878
+ }
1879
+ var LOOKUP = "https://pay.bwallet.space/api/paymail/lookup?key=";
1880
+ var LEGACY = '[data-bwx-connect], .nav-connect, .nav-acct, a[href^="https://bwalletx.com/get"], a[href^="https://www.bwalletx.com/get"]';
1881
+ function getUrlFor(ua, touchMac = false) {
1882
+ if (/iPhone|iPad|iPod/i.test(ua) || touchMac && /Macintosh/.test(ua)) return "https://bwalletx.com/iphone";
1883
+ if (/Android/i.test(ua)) return "https://bwalletx.com/android";
1884
+ if (/Chrome\//.test(ua) && !/Edg\/|OPR\//.test(ua)) return "https://bwalletx.com/extension";
1885
+ return "https://bwalletx.com/get";
1886
+ }
1887
+ function stateFor(o) {
1888
+ if (o.connected) return "connected";
1889
+ if (o.found === null) return o.phone ? "available" : "checking";
1890
+ return o.found || o.phone ? "available" : "none";
1891
+ }
1892
+ function handleLabel(key, paymail) {
1893
+ if (paymail && /@bwalletx\.com$/i.test(paymail)) return "$" + paymail.split("@")[0];
1894
+ if (paymail) return paymail;
1895
+ return key.slice(0, 6) + "\u2026" + key.slice(-4);
1896
+ }
1897
+ var CHIP_CSS = `
1898
+ a.b,button.b{all:unset;box-sizing:border-box}
1899
+ .chip{background:#0b0b0c;color:#ffd24d;border:1px solid rgba(255,210,77,.45);box-shadow:none}
1900
+ .chip:hover{background:#151517;border-color:#ffd24d}
1901
+ .chip .dot{width:8px;height:8px;border-radius:50%;background:#34d399;flex:none}
1902
+ .txt b{overflow:hidden;text-overflow:ellipsis;max-width:min(40vw,220px)}
1903
+ :host(:not([size])) button,:host(:not([size])) a.btn,:host([size=sm]) a.btn{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
1904
+ :host(:not([size])) .mark,:host([size=sm]) a.btn .mark{width:22px;height:22px}
1905
+ a.btn{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;cursor:pointer;
1906
+ padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
1907
+ box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2)}
1908
+ a.btn:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
1909
+ a.btn:focus-visible{outline:2px solid #b45309;outline-offset:3px}
1910
+ :host([size=md]) a.btn{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
1911
+ `;
1912
+ var live = /* @__PURE__ */ new Set();
1913
+ function createClass2() {
1914
+ return class BwalletxButtonElement extends HTMLElement {
1915
+ #root;
1916
+ #state = "checking";
1917
+ #key = null;
1918
+ #paymail = null;
1919
+ #duplicate = false;
1920
+ constructor() {
1921
+ super();
1922
+ this.#root = this.attachShadow({ mode: "open" });
1923
+ }
1924
+ get state() {
1925
+ return this.#state;
1926
+ }
1927
+ get identityKey() {
1928
+ return this.#key;
1929
+ }
1930
+ connectedCallback() {
1931
+ if (live.size && ![...live].includes(this)) {
1932
+ this.#duplicate = true;
1933
+ this.hidden = true;
1934
+ console.warn("[bwalletx] A second <bwalletx-button> is on this page; it is hidden. Use one per page.");
1935
+ return;
1936
+ }
1937
+ live.add(this);
1938
+ this.#hideLegacy();
1939
+ dropLegacyConnectedKey();
1940
+ const phone = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
1941
+ this.#set(stateFor({ connected: false, found: null, phone }));
1942
+ const ms = Number(this.getAttribute("discover-ms")) || 800;
1943
+ void discoverBwalletX(ms).then((f) => {
1944
+ if (!this.isConnected) return;
1945
+ if (this.#state !== "connected" && this.#state !== "connecting") this.#set(stateFor({ connected: false, found: !!f, phone }));
1946
+ }).catch(() => {
1947
+ if (this.#state === "checking") this.#set("none");
1948
+ });
1949
+ void this.#restoreSession();
1950
+ }
1951
+ /** A live session your server reports (session-url): the chip, with no wallet call. */
1952
+ async #restoreSession() {
1953
+ const url = this.getAttribute("session-url");
1954
+ if (!url) return;
1955
+ try {
1956
+ const r = await fetch(url, { credentials: "include", headers: { accept: "application/json" } });
1957
+ const j = await r.json().catch(() => ({}));
1958
+ if (j.identityKey && this.isConnected && this.#state !== "connected" && this.#state !== "connecting") {
1959
+ this.#connected(j.identityKey);
1960
+ if (j.paymail) {
1961
+ this.#paymail = j.paymail;
1962
+ this.#render();
1963
+ }
1964
+ }
1965
+ } catch {
1966
+ }
1967
+ }
1968
+ disconnectedCallback() {
1969
+ if (!this.#duplicate) live.delete(this);
1970
+ }
1971
+ #hideLegacy() {
1972
+ const scope = this.closest("header, nav") ?? document;
1973
+ for (const el2 of Array.from(scope.querySelectorAll(LEGACY))) {
1974
+ if (el2 === this || el2.contains(this)) continue;
1975
+ el2.hidden = true;
1976
+ el2.style.setProperty("display", "none", "important");
1977
+ console.warn("[bwalletx] Hid a second wallet control next to <bwalletx-button>:", el2, ". Use only <bwalletx-button>; don't add your own Get or Connect links.");
1978
+ }
1979
+ }
1980
+ #emit(type, detail) {
1981
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
1982
+ }
1983
+ #set(s) {
1984
+ this.#state = s;
1985
+ this.setAttribute("state", s);
1986
+ this.#render();
1987
+ }
1988
+ #connected(key) {
1989
+ this.#key = key;
1990
+ this.#set("connected");
1991
+ this.#emit("bwalletx-connected", { identityKey: key });
1992
+ const lookup = this.getAttribute("lookup-url");
1993
+ if (lookup === "off") return;
1994
+ void fetch((lookup ?? LOOKUP) + encodeURIComponent(key)).then((r) => r.ok ? r.json() : null).then((j) => {
1995
+ if (j && typeof j.paymail === "string" && this.#key === key) {
1996
+ this.#paymail = j.paymail;
1997
+ this.#render();
1998
+ }
1999
+ }).catch(() => void 0);
2000
+ }
2001
+ async connect() {
2002
+ if (this.#state === "connecting" || this.#state === "connected") return;
2003
+ const modal = pairingModal({ title: "Connect bWalletX" });
2004
+ this.#set("connecting");
2005
+ try {
2006
+ const { wallet } = await connectBwalletX({ pairing: modal });
2007
+ const { publicKey } = await wallet.getPublicKey({ identityKey: true });
2008
+ this.#connected(publicKey);
2009
+ } catch (e) {
2010
+ if (e?.code === "NOT_FOUND") this.#set("none");
2011
+ else {
2012
+ this.#set("available");
2013
+ if (e?.name !== "AbortError") console.warn("[bwalletx] connect:", e);
2014
+ }
2015
+ } finally {
2016
+ try {
2017
+ modal.close();
2018
+ } catch {
2019
+ }
2020
+ }
2021
+ }
2022
+ disconnect() {
2023
+ try {
2024
+ clearPairing();
2025
+ } catch {
2026
+ }
2027
+ try {
2028
+ window.postMessage({ type: "bwallet:disconnect" }, location.origin);
2029
+ } catch {
2030
+ }
2031
+ this.#key = null;
2032
+ this.#paymail = null;
2033
+ dropLegacyConnectedKey();
2034
+ this.#set("available");
2035
+ this.#emit("bwalletx-disconnected");
2036
+ }
2037
+ #render() {
2038
+ const esc = (s2) => s2.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2039
+ const size = this.getAttribute("size") ?? "sm";
2040
+ const px = size === "lg" ? 20 : size === "md" ? 17 : 15;
2041
+ const mark = `<span class="mark" part="mark">${markSvg(px)}</span>`;
2042
+ const css = `<style>${SIGNIN_CSS}${CHIP_CSS}</style>`;
2043
+ const s = this.#state;
2044
+ if (s === "none") {
2045
+ const href = this.getAttribute("get-url") ?? getUrlFor(navigator.userAgent, (navigator.maxTouchPoints ?? 0) > 1);
2046
+ this.#root.innerHTML = `${css}<a class="btn" part="button" href="${esc(href)}" target="_blank" rel="noopener">${mark}<span class="txt" part="text"><b part="label">Get bWalletX</b></span></a>`;
2047
+ return;
2048
+ }
2049
+ if (s === "connected" && this.#key) {
2050
+ const name = handleLabel(this.#key, this.#paymail);
2051
+ this.#root.innerHTML = `${css}<button type="button" class="chip" part="button chip" title="Connected to bWalletX. Click to disconnect." aria-label="Connected as ${esc(name)}. Disconnect">${mark}<span class="txt" part="text"><b part="label">${esc(name)}</b></span><span class="dot" aria-hidden="true"></span></button>`;
2052
+ this.#root.querySelector("button").onclick = () => this.disconnect();
2053
+ return;
2054
+ }
2055
+ const busy = s === "connecting" || s === "checking";
2056
+ this.#root.innerHTML = `${css}<button type="button" part="button"${busy ? ' aria-busy="true"' : ""}>${busy ? '<span class="mark" part="mark"><span class="spin" aria-hidden="true"></span></span>' : mark}<span class="txt" part="text"><b part="label">${s === "connecting" ? "Connecting\u2026" : "Connect bWalletX"}</b></span></button>`;
2057
+ this.#root.querySelector("button").onclick = () => void this.connect();
2058
+ }
2059
+ };
2060
+ }
2061
+ function defineBwalletxButton(tag = BUTTON_TAG) {
2062
+ if (typeof customElements === "undefined" || customElements.get(tag)) return;
2063
+ customElements.define(tag, createClass2());
2064
+ }
2065
+ function _resetBwalletxButtons() {
2066
+ live.clear();
2067
+ }
2068
+
996
2069
  // src/define.ts
997
2070
  defineBwalletxSignin();
2071
+ defineBwalletxButton();
998
2072
  // Annotate the CommonJS export names for ESM import in node:
999
2073
  0 && (module.exports = {
2074
+ BUTTON_TAG,
2075
+ CONNECTED_KEY,
1000
2076
  GET_URL,
2077
+ MENU_CSS,
2078
+ PENDING_LABEL,
2079
+ PENDING_LABEL_OTHER,
2080
+ PENDING_NOTE,
1001
2081
  SIGNIN_CSS,
1002
2082
  SIGNIN_TAG,
1003
2083
  WEB_WALLET_URL,
2084
+ _resetBwalletxButtons,
1004
2085
  buildAuthorizeUrl,
2086
+ defineBwalletxButton,
1005
2087
  defineBwalletxSignin,
2088
+ dropLegacyConnectedKey,
2089
+ getUrlFor,
2090
+ handleLabel,
1006
2091
  markSvg,
1007
- signinText
2092
+ signinText,
2093
+ stateFor
1008
2094
  });
1009
2095
  //# sourceMappingURL=element.cjs.map