| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
| |
scanSilentPayments() now recomputes the balance so received SP value shows in the
wallet total (not just the Receive card), and gains a silent mode. It's triggered
automatically after each full scan and on the poll loop (incremental, cheap), so
payments appear without tapping anything. The Receive card now shows a status
line ('Watching for payments' / found amount) with a small manual Rescan instead
of a primary Scan button.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
wallet.js: scanSilentPayments() pulls the indexer's /scan range, matches outputs
on-device, and verifies each is unspent via the chain backend; spUtxos + balance
are tracked in their own bucket (one-time taproot keys), persisted in the cache.
silentPaymentsAvailable()/spBalance() expose state.
app.js: the Receive tab shows the reusable sp address (QR + copy), a 'Scan for
silent payments' button, and the found amount; Settings gains a per-network SP
indexer picker (under Data source) with custom/self-host support.
sp-indexer.js: added /scan/{from} so a client fetches a whole range in one
request instead of one fetch per block.
Receiving verified end-to-end on regtest (scan finds the output, 49,999,800 sat
confirmed). Spending SP outputs is the remaining piece.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
On a fresh open, receiveSeenIndex was baselined to the pre-scan frontier (line
691), so the moment the socket's reconcile credited a deposit that predated the
open, nextReceiveIndex advanced past it and the celebration flashed for ~2s until
the post-scan baseline caught up. Leave receiveSeenIndex null until that final
baseline for fresh opens (matching the documented intent); the != null guard then
suppresses the flash. Same-session refreshes still use the persisted ack.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
| |
The mempool parenthetical is jargon for non-technical users; the status line now
reuses the plain `pending` string. Drop the now-unused pendingInMempool key.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
The recovery-phrase section of the per-wallet settings page now shows an Import
seed action for watch-only wallets, with the load-seed card inline. The load-seed
flow takes an account id (defaulting to the active wallet), so it can upgrade a
specific watch-only wallet — validating the entered seed against that wallet's
xpub before making it spendable.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
The global Settings tab no longer shows the recovery phrase / public key — those
live only on each wallet's settings page now (the watch-only 'load seed' upgrade
stays in the global tab since it targets the active wallet). On the per-wallet
page, recovery phrase moves to the top and auto-logout to the bottom. Removes the
now-dead pubkeyCard/autoLockCard. Lowercases 'THIS' in the auto-logout copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
The accounts list now shows a settings cog (instead of a rename pencil) that
opens a per-wallet settings page: edit name, set auto-logout (moved here from the
global settings tab), reveal the recovery phrase, and show the public key (zpub)
— all read from the account itself, so it works for any wallet, not just the
active one. Room for more per-wallet settings later.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The network card is now just the network picker. Each network's endpoints
(Electrum server + block explorer) live solely in the Data source card, whose
options change per network and are stored per network (so switching networks
remembers each one's choice). Removes the duplicate regtest Esplora/WS fields.
Adds mutinynet (signet — testnet address format, explorer at mutinynet.com).
Per-network defaults: mainnet → coinos Electrum, testnet/mutinynet → explorer,
regtest → local Fulcrum. Fixes tx-explorer links to use each network's web base
(no double /testnet).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
The data-source picker is now just Electrum vs Block explorer; coinos is the
first/default entry in the Electrum server list (our own Fulcrum), with public
servers as failover. Drops the standalone 'coinos' mode and the retired watcher
shim. Old 'coinos' selections migrate to electrum.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
On regtest there's no realtime push (coinos watcher is mainnet-only), so the UI
only updated on the 5–10s poll — a funded tx took a while to appear and a
just-mined block stayed "pending" until the next reconcile. electrs speaks the
Electrum protocol the watcher already understands, but only over TCP; route it
through a small WS<->TCP bridge (see coinos-server/electrum-ws) so the wallet
subscribes scripthashes and reconciles instantly on each notification. Data
still comes from esplora; this only adds the push channel. Configurable in
Settings (default ws://localhost:50003); falls back to polling if unreachable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
New `regtest` network (bech32 `bcrt`, testnet version bytes) selectable in
Settings, pointed at a configurable local Esplora (default http://localhost:3000).
Adds getNetwork/setNetwork + getRegtestEsplora persistence, a regtest branch in
resolveHosts, mainnet-only gating for the realtime watcher + Electrum backend,
and a Network selector card. Account loading reads the active network.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
On the Electrum backend the post-send rescan is many round-trips, so the balance
lagged a few seconds. Now a successful broadcast immediately applies the tx
locally (applySentTx): drop the spent coins, credit change as pending, add a
pending history row, recompute the balance, and emit — so the UI reflects the
send instantly. The realtime watcher / backstop poll reconciles and fills in the
confirmation once the explorer has indexed it (no immediate rescan, so a
not-yet-indexed tx can't briefly revert the optimistic state). Verified the
balance/utxo/history math.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The Esplora broadcast cleared its abort timer right after fetch() resolved, so
the subsequent res.text() body read had no timeout — a stalled response body
hung the whole broadcast (and the send spinner) indefinitely, with the tx never
reaching the network. Keep the abort timer active through the body read, and add
a hard 30s outer timeout in the send handler so a wedged broadcast always surfaces
as 'timed out — try again' instead of spinning forever. (The silent-payment tx
itself is valid — verified structure + BIP143 signature over the real outputs.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
broadcast() set the success result but kept ui.busy=true and didn't render until
after awaiting wallet.scan(), so the 'Sign & broadcast' button spun for the whole
post-send rescan (slow under rate-limiting / on the Electrum backend) — looking
stuck even though the tx had already broadcast. Now it shows the success screen
the instant the broadcast resolves and refreshes balances in the background.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
The ✕ on a wallet now opens a confirmation screen warning it's removed from this
device (keep its recovery phrase; other wallets unaffected) instead of deleting
immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
- goHome: when a wallet is open, the logo goes to its Receive page (its tabs)
instead of the create/import screen — so it's always a way back to your wallet.
Only with no wallet open does it show create/import.
- Paste buttons: a clipboard 'Paste' icon on the send address field and the
import (seed/key/xpub) and 'load seed to spend' fields, filling them from the
clipboard. Hidden where the Clipboard read API isn't available.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
A vault exists → Accounts screen shows Change password: enter the current
password, then a new one (with confirm; leave blank to remove it). It decrypts
the actual vault and re-encrypts it with the new password, so wallets not loaded
in the current session are never dropped. Keeps the in-memory password in sync
when the vault is already unlocked. Verified the re-encrypt round-trip
(old rejected, new works, blank works).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The logout timer is now an account setting (stored per account in the session and
the durable directory), not a global one, and it removes only that wallet from
the device — its session entry, directory entry, cached state (xpub + seed keys),
and its vault entry — leaving other wallets untouched.
- wipeAccount(id) surgically removes one wallet; the active one falls back to the
next account (or the unlock screen).
- Background-away timestamp drives per-account evaluation on resume/boot; an armed
timer fires for the soonest-due wallet so a blurred-but-visible window still
signs out on time. Overdue saved wallets are removed from the vault when it's
unlocked (the only point we can re-encrypt it).
- New cacheKeyFor() export lets app code target one wallet's cache keys exactly
(verified against the wallet's seed/xpub/watch keys).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
With a logout timer set, signing out now removes everything from the device
including the password-protected vault — nothing survives, so the recovery
phrase is required to return. Updated the setting copy to say so.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
A vault saved with no password auto-unlocks on open, which would undo the
sign-out. When a logout timer fires (or a boot is overdue), remove it too —
only a password-protected vault survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Auto log-out: a Settings option (Never default, 1m/5m/1h/1d) that, after the app
has been backgrounded/unfocused that long, signs out completely — clears the
session, the watch-only directory, and cached balance/history from the device
(only password-saved vault wallets remain). The countdown only runs while the
app is hidden/unfocused, never while it's open and focused; a background
timestamp covers throttled/discarded-tab cases on resume/boot.
Optional vault passwords: drop the 8-character minimum and allow a blank
password. A blank-password vault auto-unlocks on open with no prompt (Save to
device with no password = just persist). Verified blank round-trip + wrong-
password rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Closing the browser without 'Save to device' used to wipe the wallet entirely.
Now every wallet's account xpub is mirrored to a durable localStorage directory
(never the seed), so it survives a session wipe as a watch-only entry — you keep
seeing balance and history, and re-enter the seed to spend again.
- Durable account directory: saveDirectory() mirrors {id,label,xpub} for every
account; restore falls back to it as watch-only when there's no session/vault.
- Instant view: a full wallet also caches under its xpub key, so the downgraded
watch-only shows balance/history immediately (no rescan). Verified round-trip.
- Re-enter seed to spend: the Send tab (now shown for watch-only) and Settings
prompt for the recovery phrase / xprv; it must derive to this wallet's xpub
(verified via accountXpubFor — wrong seed/passphrase rejected), with an option
to Save to device at the same time. Upgrade is in place (id/label/cache kept).
- Vault merge now dedupes by xpub so saved + non-saved wallets coexist without
duplicates.
Privacy note: the xpub now persists by default (someone with device access can
see balance/history without the seed); 'Remove' clears it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Replace the Esplora|Electrum backend toggle plus the separate 'Instant
notifications' on/off with one 'Data source' selector:
• coinos (instant) — default; explorer data + coinos push
• Block explorer — REST + polling, nothing sent to coinos (the opt-out)
• Electrum server — one WS server for data + push (your node / public)
Picking a non-coinos source IS the privacy opt-out, so the standalone toggle is
gone. A single 'mode' setting drives getBackend()/getRealtimeEnabled(), so the
wallet/wsUrl logic is unchanged; existing per-feature settings migrate in
(realtime=off → explorer, backend=electrum → electrum). coinos⇄explorer just
flips the watcher (no rescan); to/from electrum rebuilds the backend.
Sets up cleanly for a future coinos Fulcrum: 'coinos' mode upgrades from
REST+watcher to a full Electrum backend transparently.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Settings → Data source now offers 'Electrum' alongside the Esplora REST explorer.
One Electrum WebSocket serves BOTH the wallet's data queries and scripthash push
notifications, so pointing Hal at your own Fulcrum/electrs gives a fully
self-hosted setup — no coinos, no public REST, nothing shared off your node.
- src/electrum.js: ElectrumApi maps the Esplora shapes the wallet consumes onto
Electrum methods (scripthash get_balance/listunspent/get_history, estimatefee,
transaction.broadcast). electrs has no verbose tx lookup, so it parses raw txs
and resolves input values from parent txs to compute fees (cached, in-flight
deduped, bounded-parallel). Validated field-for-field against mempool.space:
addressTxs (fee/vin/vout/status), getTx, outspend, balance, utxos.
- wallet.js: the realtime socket already spoke Electrum's notify protocol; added
request/response (_rpcCall) so the same connection answers data calls. Real
Electrum notifications carry no tx data, so they reconcile via the backend.
Replaced the fragile newline parser with a string-aware brace-depth scanner
that handles every framing (newline, one-per-frame, concatenated, split) —
fixes a buffer-poisoning hang under concurrent calls; also hardens the coinos
watcher path. Esplora stays the default; nothing changes for existing users.
Verified end-to-end against blockstream's electrs: concurrent transport, a full
fresh-wallet scan (~2s), and the parser unit-tested on all framings.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Pay sp1… addresses: the wallet derives the one-time taproot output from the
recipient's address + the transaction's own input keys (ECDH), so payments are
unlinkable on-chain with no interaction. New src/silentpay.js implements the
sender derivation + bech32m address decode, validated against the official
BIP-352 test vectors (all P2WPKH-input / single-recipient cases pass; the rest
exercise input types Hal never spends).
buildTx detects sp1… recipients, selects coins against a same-size placeholder
taproot output, then swaps in the script derived from the final input set
(updateOutput). Works for normal sends, multi-recipient, and send-max. The
review screen shows the sp1… address with a 'Silent payment' label (the on-chain
output is a derived one-time taproot address). End-to-end signing verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The WS watcher is the one place the app shares addresses off-device (it
subscribes your scripthashes to coinos to push instant payment updates). Add an
'Instant notifications' toggle in the explorer settings: off means the app never
connects to coinos and polls your chosen explorer instead — a bit slower, more
requests, but nothing is told to coinos. wsUrl returns null when disabled, so
startRealtime cleanly skips the socket and relies on the polling fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
Reorder the already-claimed actions (Create a wallet anyway above the explorer
link) and force explorer links to open in a new window/tab via window.open —
inside an installed PWA, <a target=_blank> can navigate the app away instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Opening a gift link generates a wallet only to claim into. Mark it provisional
so bailing from an already-claimed gift (Go to home / logo) discards it instead
of leaving an empty account in the session list. It's committed — kept for good —
the moment the user claims, chooses 'Create a wallet anyway', or otherwise enters
the wallet (incl. a refresh). Critically, commit on claim *success* so the backup
step can't discard a wallet that just received funds.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
The claim flow already generated a fresh wallet, so the dead-end screen now lets
the opener keep it (→ back up the recovery phrase → into the wallet) or head to
the home screen. The backup step's copy adapts (Claimed! vs Your new wallet).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Save the wallet-screen nav (tab and open tx detail) to sessionStorage on each
render and restore it on load, so refreshing keeps your place instead of jumping
back to Receive. A restored tx detail re-fetches its fee/details if missing.
Gift links still open the claim screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
A watcher-credited receive arrives with no fee (the push can't compute it from
the raw tx, and the reduced reconcile doesn't revisit the now-used address). On
opening the tx detail, fetch the full tx from the explorer to fill in the fee
(and confirmations) — works for pending mempool txs too.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
Persist the receive-ack the moment the 'Payment received!' screen is shown
(not only on tap), so refreshing or navigating away without tapping won't bring
it back. It still stays visible in the current session until dismissed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Gift links now carry a compact binary encoding of just the claim-essential data
(presigned inputs + the committed change output) instead of a full PSBT (~25%
fewer bytes), base32-encoded and served at an uppercase /g/<code> path. The QR
renders the uppercase URL in the QR alphanumeric mode (5.5 bits/char vs byte
mode's 8), so the grid drops from ~73 to ~53 modules — much easier to scan.
- wallet: _encodeCompactGift/_decodeCompactGift + _giftTx dispatcher; legacy
base64url-PSBT links still claim (auto-detected by a magic byte). Verified
round-trip + claim-byte-identical across 1/2/3-input and whole-balance gifts,
against the real exported preview/claim/outpoint functions.
- app: giftUrl uses /g/<code>; opener parses /g/ (and legacy #gift=); QR uses
uppercase + Alphanumeric mode (qrSvg gains a mode option w/ byte fallback).
- nginx already SPA-routes /g/*; the SW serves the shell for the new path.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
Avoids flashing 'You've been sent Bitcoin' before flipping to 'Already claimed'
for a spent gift. Offline fallback clears the checking flag so the loader can't
hang.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Opening a gift link now checks whether its funding coin is already spent — via
the Esplora outspend endpoint, which reports a spend even by an unconfirmed
claim. The Claim button is gated until that check passes; if the coin is spent,
show an 'Already claimed' screen with a link to the claim tx instead of a
claimable amount. doClaim also re-checks on broadcast failure (the race window),
and refuses to run while unchecked/taken — so a second opener can't attempt a
doomed double-spend.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The WS badge reads Live but mempool.space's free address tracking doesn't
reliably push incoming mempool txs, so a deposit could take ~18s to appear (the
25s safety poll catching it). While the user is on the Receive tab (online), now
poll just the fresh receive address every ~3s regardless of Live state — one
cheap request that guarantees near-instant arrival. Toggled idempotently from
render() based on the active tab; stops when they navigate away.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The refresh request storm (and the 429s) was a full API re-scan running on every
load even when the local cache or Nostr already held the complete state. Now the
full scan runs ONLY when there's neither cache nor Nostr (a seed on a fresh
device); otherwise the socket + frontier poll keep things current and reconcile
held coins — a handful of requests instead of re-querying every historical
address plus its utxos and txs.
Also reordered: startRealtime() now runs BEFORE syncFromNostr (which blocks up to
6s waiting on the relay) and before any discovery scan, so the wallet goes Live
immediately instead of sitting in Polling. retrack() after sync/scan re-subscribes
to the final frontier; a fresh open re-baselines its celebration against it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pending tx is judged 'stuck' once it has waited ~10 blocks (≈100 min) and
its fee rate (fee/vsize) is below the mempool's current floor (minimumFee) —
i.e. it's been purged and won't confirm without a bump. _txSummary now records
vsize and a firstSeen timestamp (persisted in cache) to support this.
Stuck txs are dropped from active monitoring: excluded from the watched-address
set and from the fast-poll trigger, so they don't hold a socket slot or keep us
in the 6s lane. The slow ~25s reconcile still catches one if the floor later
drops and it confirms. History flags them (⚠ stuck) and the detail view explains
it — pointing outgoing ones at the existing fee bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
- watchedAddresses() now includes any address holding an unconfirmed coin (not
just the frontier), so a pending deposit's confirmation pushes over the socket
instantly. Capped at the socket's 10-address limit. refreshLive/rescanAddress
retrack when a new pending coin changes the set.
- While any coin is pending, the frontier poll runs every ~6s even when live, so
confirmations show fast as a belt-and-suspenders to the WS push.
- Per-address rescans no longer disable each other: clicking many queues them,
and the api.js global scheduler (1 concurrent, 500ms spacing, 429 backoff)
serializes and staggers the underlying requests within the rate limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Remove the periodic full re-scan entirely — the WS and the frontier poll keep
the balance current, and refreshLive's _reconcileHeld catches spends of held
coins. The only gap (a deposit to a reused old address) is now handled
explicitly: Settings → Rescan opens a paginated list of every known address,
each with its own Rescan button (wallet.rescanAddress re-queries just that one).
Replaces the full 'Rescan wallet' button.
This sharply cuts recurring REST volume (the main rate-limit driver). Trade-off:
when the socket is down, a pending tx's confirmed badge in History may lag until
the WS reconnects or you rescan — the balance itself stays correct via reconcile.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
preventDefault() without a following prompt() makes Chrome log a 'Banner not
shown' warning. Modern Chrome shows no auto-banner to suppress, so we just
stash the event for our own install link without preventing the default.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|