From 7c542410dc78711bcf017dd3d23d560ed6349669 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 22 Jun 2026 21:47:09 -0700 Subject: Fix false "payment received" on import; PWA; send-page font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Don't celebrate historical payments on import. receiveSeenIndex was baselined to 0 before the scan ran, so when the scan advanced the index every past payment looked new and flashed "payment received". Leave it null until the post-scan ack logic baselines it. - PWA: add a web manifest, ₿ icons (192/512 + maskable), theme/apple meta tags, and a service worker that precaches the shell so the installed app works offline. index.html stays self-contained; the sidecar files just 404 harmlessly from file://. build.js emits them. - Send page: drop btn-sm from the amount unit toggle so "sats" matches the "Max" button size (both 16px). Co-Authored-By: Claude Opus 4.8 (1M context) --- build.js | 95 +++++++++++++++++++++++++++++++++++++++++-- src/app.js | 13 ++++-- static/icon-192.png | Bin 0 -> 4109 bytes static/icon-512.png | Bin 0 -> 11584 bytes static/icon-maskable-512.png | Bin 0 -> 7086 bytes static/icon-maskable.svg | 4 ++ static/icon.svg | 4 ++ 7 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 static/icon-192.png create mode 100644 static/icon-512.png create mode 100644 static/icon-maskable-512.png create mode 100644 static/icon-maskable.svg create mode 100644 static/icon.svg diff --git a/build.js b/build.js index 486908e..f11a9f6 100644 --- a/build.js +++ b/build.js @@ -1,8 +1,13 @@ -// Build a single, self-contained dist/index.html. +// Build a single, self-contained dist/index.html (plus PWA sidecar files). // // Everything — app code, the @scure/@noble crypto libs, the QR generator, and // all CSS — is bundled and inlined so the result is one file you can save and // open offline straight from the filesystem (file://). No server, no network. +// +// For the hosted site we also emit a web manifest, icons, and a small service +// worker so the page can be installed as a PWA and still work offline once +// installed. index.html stays self-contained; these are optional extras that +// simply 404 (harmlessly) when the file is opened on its own from file://. import { mkdir } from 'node:fs/promises'; @@ -12,7 +17,81 @@ const FAVICON = `₿` ); -export async function buildHtml({ minify = true } = {}) { +// Static assets copied verbatim into dist/ (icons referenced by the manifest). +const STATIC = ['icon-192.png', 'icon-512.png', 'icon-maskable-512.png', 'icon.svg']; + +const MANIFEST = { + id: '/', + name: 'Bitcoin Wallet', + short_name: 'Bitcoin', + description: 'Self-custody Bitcoin wallet that runs entirely in your browser.', + start_url: './', + scope: './', + display: 'standalone', + background_color: '#eef0f3', + theme_color: '#f7931a', + icons: [ + { src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, + { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, + { src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, + ], +}; + +// Service worker: precache the app shell, network-first for the page (so a new +// deploy shows up), cache-first for the icons/manifest, and never touch the +// cross-origin explorer/Nostr requests. CACHE carries a build token so each +// deploy supersedes the previous cache. {{VERSION}} is filled in at build time. +const SW = `const CACHE = 'cold-{{VERSION}}'; +const SHELL = ['./', 'manifest.webmanifest', 'icon-192.png', 'icon-512.png']; + +self.addEventListener('install', (e) => { + self.skipWaiting(); + e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL))); +}); + +self.addEventListener('activate', (e) => { + e.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (e) => { + const req = e.request; + if (req.method !== 'GET') return; + if (new URL(req.url).origin !== self.location.origin) return; // explorer/ws → network + + if (req.mode === 'navigate') { + e.respondWith( + fetch(req) + .then((res) => { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put('./', copy)); + return res; + }) + .catch(() => caches.match('./')) + ); + return; + } + e.respondWith(caches.match(req).then((hit) => hit || fetch(req))); +}); +`; + +const PWA_HEAD = ` + + + + + + +`; + +const SW_REGISTER = + ``; + +export async function buildHtml({ minify = true, pwa = minify } = {}) { const result = await Bun.build({ entrypoints: ['./src/app.js'], target: 'browser', @@ -35,12 +114,12 @@ export async function buildHtml({ minify = true } = {}) { Bitcoin Wallet - +${pwa ? PWA_HEAD : ''}
- +${pwa ? SW_REGISTER + '\n' : ''} `; } @@ -48,6 +127,14 @@ if (import.meta.main) { await mkdir('dist', { recursive: true }); const html = await buildHtml({ minify: true }); await Bun.write('dist/index.html', html); + + // PWA sidecars. + await Bun.write('dist/manifest.webmanifest', JSON.stringify(MANIFEST, null, 2)); + const version = Bun.hash(html).toString(36); + await Bun.write('dist/sw.js', SW.replace('{{VERSION}}', version)); + for (const f of STATIC) await Bun.write('dist/' + f, Bun.file('static/' + f)); + const kb = (Buffer.byteLength(html) / 1024).toFixed(0); console.log(`✓ dist/index.html written (${kb} KB) — open it offline, no server needed`); + console.log(`✓ PWA: manifest.webmanifest, sw.js (cold-${version}), ${STATIC.length} icons`); } diff --git a/src/app.js b/src/app.js index bccb86d..8343290 100644 --- a/src/app.js +++ b/src/app.js @@ -466,6 +466,10 @@ async function enterWallet(mnemonic, passphrase) { const hadCache = wallet.restoreCache(); // show last-known balance/history instantly ui.screen = 'wallet'; ui.tab = 'receive'; + // Not baselined yet — stays null until the scan + ack logic below sets it, + // so the celebration never fires for payments that were already there at + // import (the index only looks "advanced" because the scan hadn't run yet). + ui.receiveSeenIndex = null; ui.send = blankSend(); ui.draft = null; ui.sendResult = null; @@ -760,11 +764,12 @@ function tabContent() { // ---------------------------------------------------------------- Receive function receiveTab() { - if (ui.receiveSeenIndex == null) ui.receiveSeenIndex = wallet.nextReceiveIndex; - // A payment landed on the shown address (the fresh index advanced past what // the user last saw) — celebrate, and wait for a tap before showing the next. - if (wallet.nextReceiveIndex > ui.receiveSeenIndex) { + // Until receiveSeenIndex has been baselined (post-scan, in enterWallet) it + // stays null and we never celebrate, so importing a wallet with existing + // history doesn't flash "payment received" for old payments. + if (ui.receiveSeenIndex != null && wallet.nextReceiveIndex > ui.receiveSeenIndex) { let amt = 0; for (let i = ui.receiveSeenIndex; i < wallet.nextReceiveIndex; i++) { const e = wallet._addrInfo(0, i); @@ -837,7 +842,7 @@ function recipientRow(s, r, i) { r.amount = v; }, }), - h('button', { type: 'button', class: 'btn-sm', title: t('switchUnit'), onClick: toggleUnit }, unitLabel()), + h('button', { type: 'button', title: t('switchUnit'), onClick: toggleUnit }, unitLabel()), single && h('button', { type: 'button', class: s.max ? 'btn-primary' : '', onClick: () => { s.max = !s.max; render(); } }, t('max')) ) ); diff --git a/static/icon-192.png b/static/icon-192.png new file mode 100644 index 0000000..ce7992d Binary files /dev/null and b/static/icon-192.png differ diff --git a/static/icon-512.png b/static/icon-512.png new file mode 100644 index 0000000..c0d953a Binary files /dev/null and b/static/icon-512.png differ diff --git a/static/icon-maskable-512.png b/static/icon-maskable-512.png new file mode 100644 index 0000000..65abe21 Binary files /dev/null and b/static/icon-maskable-512.png differ diff --git a/static/icon-maskable.svg b/static/icon-maskable.svg new file mode 100644 index 0000000..7075afd --- /dev/null +++ b/static/icon-maskable.svg @@ -0,0 +1,4 @@ + + + ₿ + diff --git a/static/icon.svg b/static/icon.svg new file mode 100644 index 0000000..5ef4cf0 --- /dev/null +++ b/static/icon.svg @@ -0,0 +1,4 @@ + + + ₿ + -- cgit v1.2.3