@bwalletx/connect 0.5.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +97 -9
  3. package/dist/bwalletx-connect.global.js +97 -25
  4. package/dist/bwalletx-connect.global.js.map +1 -1
  5. package/dist/choice-B97AjqF4.d.cts +442 -0
  6. package/dist/choice-B97AjqF4.d.ts +442 -0
  7. package/dist/chunk-6EFFOIHK.js +2441 -0
  8. package/dist/chunk-6EFFOIHK.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-DIKJFKND.js +239 -0
  12. package/dist/chunk-DIKJFKND.js.map +1 -0
  13. package/dist/chunk-TNRI7ARM.js +206 -0
  14. package/dist/chunk-TNRI7ARM.js.map +1 -0
  15. package/dist/element-BXEoOdXY.d.cts +53 -0
  16. package/dist/element-BXEoOdXY.d.ts +53 -0
  17. package/dist/element.cjs +1576 -60
  18. package/dist/element.cjs.map +1 -1
  19. package/dist/element.d.cts +26 -26
  20. package/dist/element.d.ts +26 -26
  21. package/dist/element.js +35 -2
  22. package/dist/element.js.map +1 -1
  23. package/dist/index.cjs +2046 -454
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +323 -16
  26. package/dist/index.d.ts +323 -16
  27. package/dist/index.js +186 -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 +1601 -210
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +115 -9
  35. package/dist/react.d.ts +115 -9
  36. package/dist/react.js +262 -46
  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
  49. package/dist/wallets-CbzMtxrf.d.cts +0 -250
  50. package/dist/wallets-CbzMtxrf.d.ts +0 -250
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 (unreleased)
4
+
5
+ **Use Metanet Desktop or another BRC-100 wallet** (owner, 6 Nov 2026: "ship a new one now with the metanet option"), with the rules of the live bChatX door (bit-sign 73f7d644).
6
+
7
+ - **An explicit, visible choice.** "Use Metanet Desktop or another BRC-100 wallet · Metanet Desktop must be running on this computer": a link under `<bwalletx-signin>` (lg/md; `metanet="auto|on|off"`, React `metanet`), the chooser row that replaces "Desktop wallet", `useBwalletX().connectMetanet()` / `connect({ metanet: true })`, `runConnect({ metanet: true })`, `metanetOption()`. Pinned to `WalletClient('json-api')` on localhost:3321, never `'auto'`; it calls :3321 only after its own tap (getVersion first, so "nothing there" says "We couldn’t reach Metanet Desktop. Open it, then try again.").
8
+ - **bWalletX stays the default.** The button's discovery never picks Metanet, even when it is the remembered wallet (`matchRemembered` → null); a remembered Metanet shows no "Continue with your wallet" or Switch note (`tapGoesElsewhere`).
9
+ - **Remembered:** the type (`bwx:wallet = { type: 'metanet', substrate: 'json-api' }`) and the identity keys known on this origin (`bwx:known-keys`, public keys, newest 8; `knownIdentityKeys`, `isKnownIdentityKey`, `rememberKnownIdentityKey`). Every successful connect or sign-in adds its key.
10
+ - **Return visit:** `planAutoConnect` → `'silent-metanet'`; ONE silent attempt per page load, 2 s (`trySilentMetanet`, `runSilentMetanet`), only for a key already known here; a locked, slow, absent or unknown wallet stops silently (no banner, no retry, no unlock prompt). With a session it attaches only the session's key. The element and the provider both run it.
11
+ - **"Not you? Use another wallet"** under the button while Metanet is remembered (element `notYou()`, event `bwalletx-not-you`, `useBwalletX().forgetWallet()`), and Disconnect, forget the choice; a sign-in dropped mid-way lands nobody.
12
+ - **Computers only**, never in a frame (`metanetOffered()`, `inAnyFrame()`); the chooser row is hidden there too.
13
+ - **The minimum asked:** getVersion, isAuthenticated, the identity key, then the site's own call (the sign-in signature), one at a time; no protocol permission requests at connect.
14
+ - **No coins:** a `createAction` that fails for lack of funds throws `PaymentError` `INSUFFICIENT_FUNDS` (`maybeSent: false`) with "Your wallet doesn't have enough to cover this payment and its small network fee. Add a little and try again." (`NO_COINS_TEXT`, `isInsufficientFunds`).
15
+ - **Security review fixes:** no desktop wallet on localhost (Metanet on 3321, Cicada on 3301, `probeLocal`, a remembered 0.6.0 "Desktop wallet") is ever picked without its own tap: `autoChoice` and `matchRemembered` skip them (`isLocalDesktop`, `isLocalDesktopType`); a remembered `desktop` now plans `'none'`. Identity keys count as known only after a verified sign-in or a session match. `logoutUrl` / `logout-url` is required with `verifyUrl` (dev `console.warn`, `warnIfNoLogout`).
16
+ - `isTouchDevice` moved to its own module (still exported from the package root and `modal`).
17
+
18
+ ## 0.6.0 (unreleased)
19
+
20
+ Auto-connect for returning visitors (RB, 5 Nov 2026: "go on auto-connect"), within the wallet-choice rules of 0.5.6.
21
+
22
+ - **The remembered choice.** After a sign-in the user explicitly picked (bWalletX's own button, a pick in the chooser, a site's `walletId`), the wallet TYPE is remembered for this origin: `localStorage['bwx:wallet'] = { type, substrate }`, e.g. `{ "type": "bwalletx", "substrate": "extension" }` or `{ "type": "io.yours", "substrate": "injected" }`. Never a key, address or handle; a bare `window.CWI` is never remembered (whoever holds it next time may be another wallet). `rememberWalletChoice`, `rememberedWalletChoice`, `forgetWalletChoice`.
23
+ - **The next tap goes straight to it**, with no chooser, even when it isn't bWalletX (the user chose it). If it isn't here any more: bWalletX if here, else the chooser. A remembered desktop wallet probes localhost only after the tap.
24
+ - **Nothing that could prompt fires on load.** `planAutoConnect()`: with a live server session and a remembered injected wallet, the provider picks the wallet up by discovery alone (`attachRemembered`: no wallet call, no localhost, no relay), so `pay`/`tip` work after a reload without another tap. Without a session, a connect would ask the wallet for a key or a signature, so it waits for the tap.
25
+ - **"Some features need bWalletX · Switch"** under `<bwalletx-signin>` (lg/md; `switch-note="on|off"`) when the remembered wallet isn't bWalletX, and "Switch to bWalletX" in the signed-in menu. `switchToBwalletX()` on the element and in `useBwalletX()`; the `bwalletx-signin` event carries `mode: 'bwalletx'`. `useBwalletX()` adds `rememberedWallet` and `needsBwalletX`.
26
+ - **Disconnect forgets** the remembered wallet (the provider's `disconnect()` and the element's menu).
27
+ - Options: `BwalletXProvider rememberChoice` and `autoConnect` (both default true; both off with `fastPath={false}`), `runConnect({ remembered, remember, preferBwalletX })`, `decideWallet({ remembered, preferBwalletX })`, `<bwalletx-signin remember="off">`.
28
+ - `<bwalletx-button>` shares the record: a 0.5.5 `bwx:connected` entry (which held the identity key) becomes `{ type: 'bwalletx' }` on load and the key is deleted (`migrateConnectedKey`); a tap on Connect remembers the type; Disconnect forgets it. Still zero wallet calls on load.
29
+ - The one-sheet sign-in is unchanged: the identity key and the signature still go to the wallet together.
30
+
31
+ ## 0.5.6 (unreleased)
32
+
33
+ Wallet choice (RB, 5 Nov 2026: bWalletX is the default on our sites; no other wallet may take the sign-in flow). A fix: sites showed their own chooser even when only bWalletX was present, so users never saw the wallet sheet, and a wallet that held `window.CWI` was treated as bWalletX.
34
+
35
+ - **bWalletX is recognised by its own announcement only.** `discoverBwalletX()` / `findBwalletX()` / `connectBwalletX()` no longer take `window.CWI` as bWalletX "in-app": Metanet / BSV Desktop or Yours can set or overwrite it. bWalletX is the BRC-100 announcement with rdns `com.bwalletx.extension` (extension) or `space.bwallet.mobile` (in-app browser; every build since 3 Oct 2026 announces), or the bWalletX app frame over XDM. A bare `window.CWI` is listed by `discoverWallets()` as "Browser wallet" (`substrate: 'window.CWI'`) and is never bWalletX.
36
+ - **Fast path, on by default.** `decideWallet()` (new, pure) is the rule in `runConnect`, `BwalletXProvider.connect()` and `<bwalletx-signin>`: bWalletX here → straight to its sheet, no chooser, even with other wallets installed. Without it → the chooser, **Get bWalletX** first, then the detected wallets; another wallet is never connected unless picked (0.5.5 connected a lone Yours/Metanet with no picker). `picker: false` now means bWalletX, else the phone, else `NOT_FOUND`. `fastPath: false` restores the 0.5.5 rule.
37
+ - **"Other wallets"**: a small link under `<bwalletx-signin>` (lg/md by default; `other-wallets="on|off"`) and in its not-found row; `connect({ chooser: true })` in the provider; the element's `bwalletx-signin` event carries `mode: 'chooser'`.
38
+ - **`chooseWallet(list)`**: the new chooser (Get bWalletX first when absent; bWalletX the gold row when present; phone QR; other wallets tagged "Fewer features"; Desktop wallet; HandCash last; Esc / Cancel / backdrop close it). `pickWallet` is unchanged.
39
+ - **No localhost probe before a tap.** `desktopOption()` ("Desktop wallet") probes localhost:3321 / 3301 only when its `connect()` runs, after the user picked it. `probeLocal` stays off by default.
40
+ - **Telemetry hook**: `onAttempt` (provider prop / `runConnect` option) and the `bwalletx-attempt` event: `{ walletType, substrate, outcome, ms }` per attempt, nothing else.
41
+ - `WalletOption.substrate`, `walletTypeOf`, `substrateOf`, `isPresentBwalletX`, `hasBwalletX`, `otherDetected`; `discoverWallets` also finds bWalletX in its app frame (XDM). `useBwalletX().walletType`.
42
+ - Element: picking another wallet shows "Approve in your wallet…"; closing the phone QR returns to the button without an error.
43
+ - **`<bwalletx-button>`: no wallet calls on load.** It used to call `getPublicKey` on load when `bwx:connected` was set, and stored the identity key there. Now it does discovery only; `bwx:connected` is deleted on load (`dropLegacyConnectedKey`) and never written; a reload shows Connect, and one tap reconnects. New `session-url` shows the chip from your server's session with no wallet call.
44
+
45
+ ## 0.5.5 (unreleased)
46
+
47
+ - **One wallet sheet to sign in** (owner escalation, 2 Nov 2026: "FIVE PROMPTS??? FOR ONE CONNECTION?" on vexvoid.com). Measured with bWalletX 5.2.48: 0.5.4 asked for a picker plus 3 sheets on localhost, and a picker plus 5 sheets on the live site. Now:
48
+ - `signInWithBwalletX` gets the challenge first, with no key, then sends `getPublicKey({ identityKey: true })` and `createSignature` together (`Promise.all`), so the wallet can show one sheet. `identityKey` reuses a key you already have (the signature alone).
49
+ - `BwalletXProvider.connect()` (and `useBwalletX().connect()`) no longer asks for the key before signing in: the old path asked for it twice (once to connect, once to sign in). Logic in `runConnect()`.
50
+ - **Repeat sign-in is the signature alone**: the key that signed in last is remembered on the site (`rememberedIdentity()`, public data, a hint only). If another account is active, verification fails once and the sign-in is retried with the key asked for (`signInAndVerify`).
51
+ - `waitForAuthentication` is only called when the wallet says it is locked (`waitIfLocked`): bWalletX answers every call by fetching the site's manifest.json and, when that asks for permissions, showing a separate sheet each time.
52
+ - **Picker**: bWalletX is listed once (`window.CWI` is the same extension it announced; `cwiIsAnnounced`). With one wallet in the browser there is no picker at all (`autoChoice`); phone pairing is then a fallback, not a choice.
53
+ - **Server**: `createChallenge({ identityKey })` makes the key optional; a keyless challenge binds the key the browser claims at `verifySignIn`, and the signature only verifies for that key. A challenge made with a key still only verifies for that key. **Upgrade the server too** for one sheet: against a 0.5.4 server the browser falls back to the old order by itself (two sheets), or set `challengeNeedsIdentityKey`.
54
+ - **The official element is stateful.** `<bwalletx-signin>` / `<BwalletxSignin>`: signed out → pending ("Approve in bWalletX…" with one line, "Sign in: shares only your public identity key, moves no money"; a body-level tip on `sm`/`icon`) → signed in (a badge: bAvatar, $handle from the paymail lookup or a short key, gold dot) with a menu (Copy paymail/key, Disconnect) → or an error line plus Retry ("Sign-in cancelled" for a no). The menu is a fixed, body-level portal (never clipped by `overflow:hidden`), keyboard operable, and fits a phone. Inside `<BwalletXProvider>` the element follows the provider (click → `connect()`, Disconnect → `disconnect()` → `logoutUrl`, reload restores from `sessionUrl` with no wallet prompt). Standalone: `session-url`, `logout-url`, `lookup-url`; `stateless` keeps the old plain button.
55
+ - **`useBwalletXAccount()`**: `{ status, identityKey, paymail, handle, label, avatarUrl, error, login, logout }` for sites with their own UI.
56
+ - **`<bwalletx-button>`: the one nav wallet control.** It renders Get bWalletX (deep-linked by platform), Connect bWalletX, or the $handle chip, by state, with the official button look. It guards against duplicates: a second instance and legacy controls in the same header/nav are hidden, with a console warning. Use only this element; don't add your own Get or Connect links.
57
+ - **`<bwalletx-signin label="get">` and `label="play"`**: "Get bWalletX" and "Play with bWalletX", the same official button for download and play-now calls to action (with `href` link mode). First users: terrornode.com and tokenblaster.lol.
58
+
3
59
  ## 0.5.4 (unreleased)
4
60
 
5
61
  - **Connect is fast.** A wallet that is present now resolves at once: the extension answers `brc100:requestWallet` synchronously, and `window.CWI` (in-app browser) is taken straight away instead of after the wait. The wait for "nothing here" before the pairing QR drops from 1200 ms to 300 ms (`DEFAULT_DISCOVER_MS`); in a bWalletX frame the XDM probe now starts in parallel. `discoverWallets` stops waiting 50 ms after a wallet has announced. Measured in Chrome: pairing modal 1.24–1.34 s → 0.33–0.34 s after the click.
package/README.md CHANGED
@@ -62,6 +62,10 @@ The cookie (`bwx_session`) is `HttpOnly; SameSite=Lax; Secure` (Secure when the
62
62
 
63
63
  Create the nonce table once (a migration): `pgNonceTableSql()` returns the SQL.
64
64
 
65
+ **Hydration warning.** bWalletX for Chrome adds a `data-bwalletx-ext` attribute to `<html>` and `<body>` before React hydrates, so Next.js logs a harmless hydration mismatch. Put `suppressHydrationWarning` on both in `app/layout.tsx`: `<html lang="en" suppressHydrationWarning>` and `<body suppressHydrationWarning>`. It only silences attribute differences on those two elements, not in your components.
66
+
67
+ **One sheet needs the server too.** The client and the challenge route must both be 0.5.5 or newer: an older `createChallenge` refuses a challenge without an identity key, and the client then falls back to asking for the key first (two wallet sheets).
68
+
65
69
  ## React quick start
66
70
 
67
71
  ```tsx
@@ -95,7 +99,22 @@ function Account() {
95
99
 
96
100
  `useBwalletX()` gives `status` (`idle | restoring | connecting | connected | error`), `identityKey`, `handle`, `paymail`, `wallet` (the BRC-100 `WalletInterface`), `walletId`, `signedIn`, `error`, `wallets`, `connect({ walletId? })`, `disconnect()`, `listWallets()`, `pay`, `tip` and `buyToken`.
97
101
 
98
- `connect()` finds wallets, shows the built-in picker when there is more than one (bWalletX first), shows the pairing QR modal for phones, and signs in through your routes when `challengeUrl` and `verifyUrl` are set. Nothing touches `window` during render, so the provider is safe in server-rendered pages. After a reload the session comes back from `sessionUrl`, but `wallet` stays `null` until the user calls `connect()` again.
102
+ `connect()` finds wallets and signs in through your routes when `challengeUrl` and `verifyUrl` are set, with **one wallet sheet**: the challenge comes first (no key), then the identity key and the signature are asked for together, and a repeat sign-in on the same site is the signature alone. With bWalletX in the browser there is no chooser at all (0.5.6); without it the chooser opens with "Get bWalletX" first, and another wallet is used only when picked (see "Which wallet signs in"). Nothing touches `window` during render, so the provider is safe in server-rendered pages. After a reload the session comes back from `sessionUrl`, but `wallet` stays `null` until the user calls `connect()` again.
103
+
104
+ ### Drop-in: provider + one element + 3 routes
105
+
106
+ The official element follows the provider, so one element gives the whole experience: **Sign in with bWalletX** → **Approve in bWalletX…** (with one line: "Sign in: shares only your public identity key, moves no money") → a signed-in badge (bAvatar, $handle or a short key, gold dot) whose menu has **Copy paymail/key** and **Disconnect**. A reload restores the badge from `sessionUrl` with no wallet prompt. A no in the wallet reads "Sign-in cancelled" with **Retry**.
107
+
108
+ ```tsx
109
+ <BwalletXProvider challengeUrl="/api/bwalletx/challenge" verifyUrl="/api/bwalletx/verify"
110
+ logoutUrl="/api/bwalletx/logout" sessionUrl="/api/bwalletx/session">
111
+ <BwalletxSignin size="sm" /> {/* in your header; that's all */}
112
+ </BwalletXProvider>
113
+ ```
114
+
115
+ Your own UI instead: `const { status, identityKey, paymail, handle, label, avatarUrl, error, login, logout } = useBwalletXAccount();` (`status`: `restoring | signed-out | pending | signed-in | error`).
116
+
117
+ **Upgrade the server too.** The one-sheet order needs a challenge route that accepts no key: `createChallenge` / `createBwalletXAuth` from 0.5.5. Against an older server the browser sees the refusal and falls back to the old order (two sheets) by itself; `challengeNeedsIdentityKey` skips the attempt.
99
118
 
100
119
  ## Payments
101
120
 
@@ -110,7 +129,7 @@ const r = await pay(wallet, { to: identityKeyHex, sats: 100 }); // BR
110
129
  await buyToken(wallet, { token: '$BOASE', amount: 100, quote: (q) => fetch('/api/quote', { method: 'POST', body: JSON.stringify(q) }).then((x) => x.json()) });
111
130
  ```
112
131
 
113
- Each call makes one `createAction`, and nothing is ever retried. Failures throw a `PaymentError` with a `code` (`BAD_REQUEST`, `RESOLVE_FAILED`, `RATE_FAILED`, `WALLET_REJECTED`, `WALLET_ERROR`, `NOT_SUPPORTED`). When `maybeSent` is true, the wallet may already have broadcast the payment, so check before you ask the user again. Browsers often can't resolve paymails themselves because of CORS, so pass `resolvePaymail` to do it on your server.
132
+ Each call makes one `createAction`, and nothing is ever retried. Failures throw a `PaymentError` with a `code` (`BAD_REQUEST`, `RESOLVE_FAILED`, `RATE_FAILED`, `WALLET_REJECTED`, `WALLET_ERROR`, `NOT_SUPPORTED`, `INSUFFICIENT_FUNDS`). A wallet with no coins for the fee (0.7.0: the SDK's `WERR_INSUFFICIENT_FUNDS`, also over Metanet Desktop's JSON API, or a wallet's own "insufficient funds") gives `INSUFFICIENT_FUNDS` with `maybeSent: false` and one plain line you can show as is: "Your wallet doesn't have enough to cover this payment and its small network fee. Add a little and try again." (`NO_COINS_TEXT`; `isInsufficientFunds(e)` for your own calls). When `maybeSent` is true, the wallet may already have broadcast the payment, so check before you ask the user again. Browsers often can't resolve paymails themselves because of CORS, so pass `resolvePaymail` to do it on your server.
114
133
 
115
134
  **Verify payments on your server.** A txid or BEEF that comes back to the page is only what the page was told: a
116
135
  browser extension, a modified page or (before 0.5.2) a parent frame posing as the wallet can return a fake one.
@@ -133,12 +152,56 @@ Every expected output has to be matched by a different output that carries at le
133
152
  `discoverWallets()` lists everything it can find, with bWalletX first:
134
153
 
135
154
  - every wallet that announces itself through BRC-100 discovery (bWalletX, Yours, Metanet and others)
136
- - the in-app browser's `window.CWI`
137
- - desktop wallets on localhost ports 3321 and 3301 (only with `probeLocal: true`)
155
+ - bWalletX in its own app frame (XDM, only from bWalletX's origins)
156
+ - a `window.CWI` that nobody announced, under a neutral name ("Browser wallet"); it is never taken for bWalletX
157
+ - desktop wallets on localhost ports 3321 and 3301 (only with `probeLocal: true`; prefer `desktopOption()`, below)
138
158
  - phone pairing (`pairing: pairingModal()`)
139
159
  - any adapters you add in `extra`
140
160
 
141
- `pickWallet(list)` shows the picker. `pairingModal()` returns `PairingOptions` that draw the QR code and match code, so you no longer need your own QR library. On phones and tablets it shows an **Open bWalletX** button (the pairing universal link opens the app) and a web.bwalletx.com fallback instead of a QR; pass `mode: 'qr'` or `'app'` to force one.
161
+ `pairingModal()` returns `PairingOptions` that draw the QR code and match code, so you no longer need your own QR library. On phones and tablets it shows an **Open bWalletX** button (the pairing universal link opens the app) and a web.bwalletx.com fallback instead of a QR; pass `mode: 'qr'` or `'app'` to force one.
162
+
163
+ ### Which wallet signs in (0.5.6)
164
+
165
+ bWalletX is the default on our sites, and no other wallet may take the sign-in flow:
166
+
167
+ 1. **bWalletX is recognised by its own announcement** (`com.bwalletx.extension`, `space.bwallet.mobile`) or by the bWalletX app frame, never by who holds `window.CWI`. Metanet / BSV Desktop or Yours can set or overwrite `window.CWI`; `discoverBwalletX()` no longer takes it.
168
+ 2. **bWalletX here → straight to its sheet.** No site chooser. A small **Other wallets** link under the button opens the chooser (phone QR, other browser wallets, desktop wallets).
169
+ 3. **No bWalletX → the chooser, "Get bWalletX" first**, then what was detected. Another wallet is connected only when the user picks it, even when it is the only one.
170
+ 4. **No localhost probe on load.** "Use Metanet Desktop or another BRC-100 wallet" in the chooser (0.7.0; was "Desktop wallet") looks on localhost:3321 only when tapped, so Chrome's Local Network Access prompt follows a tap. `desktopOption()` (3321 / 3301) is still exported.
171
+
172
+ `decideWallet(list, { chooser, picker, walletId, fastPath })` is the rule as a pure function; `runConnect`, `BwalletXProvider` and `<bwalletx-signin>` all use it. `chooseWallet(list)` is the chooser UI (`pickWallet` is the 0.5.5 one, still exported). If your site has its own chooser, call `decideWallet` first and show yours only when it answers `{ reason: 'chooser' }`:
173
+
174
+ ```ts
175
+ import { discoverWallets, decideWallet, chooseWallet, pairingModal } from '@bwalletx/connect';
176
+
177
+ const list = await discoverWallets({ pairing: () => pairingModal() });
178
+ const { option, reason } = decideWallet(list); // bWalletX here → option, no chooser
179
+ const chosen = option ?? (reason === 'chooser' ? await chooseWallet(list) : null);
180
+ ```
181
+
182
+ `BwalletXProvider fastPath={false}` (or `runConnect({ fastPath: false })`) restores the 0.5.5 picker rule. `connect({ chooser: true })` is the "Other wallets" link. `<bwalletx-signin other-wallets="auto|on|off">` controls the link (auto: shown at lg/md when the element signs in).
183
+
184
+ ### Returning visitors (0.6.0)
185
+
186
+ After a sign-in the user explicitly picked, the wallet **type** is remembered for the origin (`localStorage['bwx:wallet']`, e.g. `{ "type": "io.yours", "substrate": "injected" }`; never a key). The next tap goes straight to that wallet, with no chooser. On load nothing that could prompt fires: with a live session the provider picks the remembered wallet up by discovery alone, so `pay`/`tip` work after a reload; without one, the next tap connects. If the remembered wallet isn't bWalletX, the element shows **Some features need bWalletX · Switch** (and "Switch to bWalletX" in its menu); `useBwalletX()` exposes `needsBwalletX` and `switchToBwalletX()` for your own UI. Disconnect forgets the choice. Turn it off with `rememberChoice={false}` / `autoConnect={false}` on the provider or `remember="off"` on the element.
187
+
188
+ ### Metanet Desktop and other BRC-100 wallets (0.7.0)
189
+
190
+ **Use Metanet Desktop or another BRC-100 wallet** · *Metanet Desktop must be running on this computer.* A small link under `<bwalletx-signin>` (lg/md; `metanet="auto|on|off"`), a row in the chooser, `useBwalletX().connectMetanet()` or `runConnect({ metanet: true })`. The same rules as the bChatX door:
191
+
192
+ 1. **bWalletX stays the default.** It is found by its own announcement only; the button's discovery never picks Metanet, remembered or not. No desktop wallet on localhost is picked without its own tap: not by `autoChoice` / `fastPath={false}`, not after `probeLocal`, and not from a remembered 0.6.0 "Desktop wallet" (`isLocalDesktop`).
193
+ 2. **Pinned, and only on its own tap.** `WalletClient('json-api')` on localhost:3321, never `'auto'` (which would let the bWalletX extension or `window.CWI` answer). A first visit makes no :3321 call on page load.
194
+ 3. **Remembered:** the wallet type (`bwx:wallet = { "type": "metanet", "substrate": "json-api" }`) and the identity keys known on this origin (`bwx:known-keys`, public keys only). A key counts as known only after your verify route accepted its sign-in (or it matches the server session); a plain connect adds nothing.
195
+ 4. **Back with Metanet remembered:** one silent attempt per page load, a 2 s window (`trySilentMetanet` / `runSilentMetanet`). It goes on only if Metanet returns an identity key already known here, since whatever answers on :3321 might be another profile or another local process. Otherwise the normal choice, with no error banner and no retry. With a server session, it only attaches the wallet when it holds the session's key.
196
+ 5. **"Not you? Use another wallet"** (shown while Metanet is remembered) and Disconnect forget the choice; `useBwalletX().forgetWallet()` for your own UI. A sign-in dropped this way lands nobody (`logoutUrl` ends a session its verify already set).
197
+ 6. **Computers only**, never inside a frame (`metanetOffered()`).
198
+ 7. **The minimum:** `getVersion`, `isAuthenticated` (never prompts), the identity key, then the sign-in signature, one request at a time. No protocol permission requests at connect.
199
+
200
+ Nothing on :3321 after a tap shows "We couldn’t reach Metanet Desktop. Open it, then try again."
201
+
202
+ **`logoutUrl` / `logout-url` is required whenever `verifyUrl` / `verify-url` is set.** "Not you? Use another wallet" and Disconnect end the session the verify route created by POSTing to it; without it a silent Metanet sign-in the person rejected would stay signed in. In development the provider and the element `console.warn` once when it is missing.
203
+
204
+ **Telemetry.** `onAttempt` (provider prop, `runConnect` option) and the element's `bwalletx-attempt` event report one record per attempt: `{ walletType, substrate, outcome, ms }`. `walletType` is `'bwalletx'` or the other wallet's rdns / option id; `substrate` is how it reached the page (`extension`, `in-app`, `xdm`, `injected`, `window.CWI`, `json-api`, `cicada`, `pairing`, `adapter`); `outcome` is `signed-in`, `connected`, `cancelled`, `rejected`, `not-found` or `error`. No keys, addresses or identity data.
142
205
 
143
206
  ## Migrating from HandCash Connect
144
207
 
@@ -156,6 +219,25 @@ Every expected output has to be matched by a different output that carries at le
156
219
  3. Replace HandCash payment calls with `pay` / `tip` / `buyToken`, and check them on the server with `verifyPayment` instead of HandCash payment webhooks.
157
220
  4. Remove the adapter when your users have moved over.
158
221
 
222
+ ## The nav wallet control: use only `<bwalletx-button>`
223
+
224
+ **Use only this element for your site's wallet control. Don't add your own "Get bWalletX" or "Connect" links or buttons next to it.** It renders exactly one thing by state:
225
+
226
+ | State | Shows |
227
+ |---|---|
228
+ | No wallet in this browser | **Get bWalletX** (a link: bwalletx.com/extension on desktop Chrome, /iphone, /android, else /get) |
229
+ | A wallet here, not connected | **Connect bWalletX** (extension, in-app browser, or a QR to the phone app) |
230
+ | Connected | The **$handle chip** (click to disconnect) |
231
+
232
+ ```html
233
+ <script src="https://cdn.jsdelivr.net/npm/@bwalletx/connect/dist/bwalletx-connect.global.js"></script>
234
+ <header>… <bwalletx-button></bwalletx-button></header>
235
+ ```
236
+
237
+ Or `import '@bwalletx/connect/element'`. Attributes: `size` (sm, md, lg), `get-url`, `discover-ms`, `lookup-url`. Events: `bwalletx-connected` `{ identityKey }` and `bwalletx-disconnected`.
238
+
239
+ **Guard:** there is one per page. A second `<bwalletx-button>` hides itself. Legacy controls inside the same `<header>`/`<nav>` (`[data-bwx-connect]`, `.nav-connect`, `.nav-acct`, links to bwalletx.com/get) are hidden too. Each one hidden logs a console warning. Links in your page content are left alone.
240
+
159
241
  ## The button
160
242
 
161
243
  `<bwalletx-signin>` is the standard "Sign in with bWalletX" button: a web component with no dependencies (Shadow DOM), plus a React wrapper. It is the gold button from bit-sign's sign-in card.
@@ -184,12 +266,18 @@ import { BwalletxSignin } from '@bwalletx/connect/react';
184
266
  | `brand` | `bWalletX` (default), `bWallet` (store edition) |
185
267
  | `subtitle` | `on`, `off`, or your own text. Default "also works with bWallet" on lg/md |
186
268
  | `full-width`, `loading`, `disabled` | flags |
187
- | `challenge-url`, `verify-url` | run the sign-in flow on click: POST `{ identityKey }`, then POST `{ identityKey, nonce, signature }` |
269
+ | `challenge-url`, `verify-url` | run the sign-in flow on click: POST `{}` (one sheet; `{ identityKey }` on a repeat sign-in), then POST `{ identityKey, nonce, signature }` |
270
+ | `session-url`, `logout-url` | restore the signed-in badge on load (no wallet prompt); POST on Disconnect |
271
+ | `lookup-url` | paymail lookup for the badge (default pay.bwallet.space; `off`) |
272
+ | `stateless` | the pre-0.5.5 plain button: no badge, no pending/error states |
273
+ | `state` (+ `identity-key`, `paymail`, `handle`, `avatar-url`, `error`) | drive the states yourself; `<BwalletxSignin>` inside `<BwalletXProvider>` does this for you |
188
274
  | `get-url`, `web-url` | where "Get bWalletX" and the web wallet link go (bwalletx.com/get, web.bwalletx.com) |
189
275
  | `href` | navigate on click instead |
190
276
  | `authorize-url`, `client-id`, `redirect-uri`, `scope` | OAuth 2.1 redirect with S256 PKCE (verifier, state and nonce in `sessionStorage["bwalletx:oauth"]`). For auth.bwalletx.com, which is planned and not live yet |
191
277
 
192
- Events (bubbling, composed): `bwalletx-signin` on every click (call `preventDefault()` to run your own flow instead), `bwalletx-signed-in` with `{ identityKey, method, nonce, signature, response }`, `bwalletx-not-found` (the button then shows "Get bWalletX" and a web wallet link), `bwalletx-error` with `{ error }`.
278
+ Events (bubbling, composed): `bwalletx-signin` on every click (call `preventDefault()` to run your own flow instead), `bwalletx-signed-in` with `{ identityKey, method, nonce, signature, response }`, `bwalletx-not-found` (the button then shows "Get bWalletX" and a web wallet link), `bwalletx-error` with `{ error }`, `bwalletx-disconnect` (cancelable; Disconnect in the badge menu) and `bwalletx-signed-out`.
279
+
280
+ States: signed out → pending ("Approve in bWalletX…" plus one plain line) → signed in (the badge; its menu is a body-level portal, so an `overflow:hidden` header never clips it, and it is keyboard operable: Enter/↓ opens, arrows move, Esc closes) or error (one line plus Retry).
193
281
 
194
282
  Without `challenge-url`, `href` or `authorize-url` the button only fires `bwalletx-signin`. The built-in flow uses the extension or in-app browser only. For phone pairing, listen for `bwalletx-signin`, call `preventDefault()`, and call `signInWithBwalletX` with `pairing` as shown above.
195
283
 
@@ -203,7 +291,7 @@ import { createChallenge, verifySignIn, originAllowed, memoryStore } from '@bwal
203
291
  const ORIGIN = 'https://example.com'; // your site, from config. Never from the request.
204
292
  const store = memoryStore(); // dev only. See "Nonce store" below.
205
293
 
206
- // POST /auth/bwalletx/challenge body: { identityKey }
294
+ // POST /auth/bwalletx/challenge body: {} (or { identityKey })
207
295
  app.post('/auth/bwalletx/challenge', async (req, res) => {
208
296
  if (!originAllowed(req.get('origin'), ORIGIN)) return res.status(403).end();
209
297
  try {
@@ -223,7 +311,7 @@ app.post('/auth/bwalletx/verify', async (req, res) => {
223
311
  });
224
312
  ```
225
313
 
226
- `verifySignIn` returns the identity key (66 lowercase hex characters) or `null`. It refuses a nonce that was already used, has expired (2 minutes), was issued for another origin or another identity key, or whose signature doesn't verify.
314
+ `verifySignIn` returns the identity key (66 lowercase hex characters) or `null`. It refuses a nonce that was already used, has expired (2 minutes), was issued for another origin or another identity key, or whose signature doesn't verify. Since 0.5.5 `identityKey` is optional in `createChallenge`: a challenge made without one binds the key the browser claims at verify, and the signature only verifies for that key. That lets the browser ask the wallet for the key and the signature in one sheet.
227
315
 
228
316
  The text the wallet signs:
229
317