From d5e55c9c577feb6f8000bad3c37045a2cd7c9b4c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 23 Jun 2026 23:05:52 -0700 Subject: Add QR scanner to the send page (lazy-loaded decoder) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scan a Bitcoin address, a BIP21 URI (fills address + amount), or a signed-transaction QR (opens a broadcast confirmation, then relays it) from a viewfinder button on the Send form. Decoder strategy keeps the main bundle small: - Prefer the native BarcodeDetector (Chrome desktop/Android) — no lib. - Otherwise lazy-load jsQR from a separate dist/jsqr.js, only when the scanner is opened. jsQR stays out of index.html (~424 KB vs ~551 KB) and off the initial load. - The service worker precaches jsqr.js so offline scanning works for installed PWAs on browsers without BarcodeDetector. The scanner needs a camera + secure context, so it never shows on a standalone file:// page; offline air-gapped signing is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- build.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'build.js') diff --git a/build.js b/build.js index f11a9f6..34d3d19 100644 --- a/build.js +++ b/build.js @@ -42,7 +42,9 @@ const MANIFEST = { // 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']; +// jsqr.js is precached so the lazy QR decoder is available offline too (for +// installed PWAs on browsers without a native BarcodeDetector). +const SHELL = ['./', 'manifest.webmanifest', 'icon-192.png', 'icon-512.png', 'jsqr.js']; self.addEventListener('install', (e) => { self.skipWaiting(); @@ -91,6 +93,20 @@ const PWA_HEAD = ` const SW_REGISTER = ``; +// Bundle the standalone jsQR decoder (sets window.jsQR) for lazy loading. +export async function buildJsQr({ minify = true } = {}) { + const result = await Bun.build({ + entrypoints: ['./src/jsqr-global.js'], + target: 'browser', + minify, + }); + if (!result.success) { + for (const log of result.logs) console.error(log); + throw new Error('jsqr bundle failed'); + } + return result.outputs[0].text(); +} + export async function buildHtml({ minify = true, pwa = minify } = {}) { const result = await Bun.build({ entrypoints: ['./src/app.js'], @@ -128,6 +144,10 @@ if (import.meta.main) { const html = await buildHtml({ minify: true }); await Bun.write('dist/index.html', html); + // Lazy-loaded QR decoder — kept out of index.html, fetched only when a + // browser without BarcodeDetector opens the scanner. + await Bun.write('dist/jsqr.js', await buildJsQr()); + // PWA sidecars. await Bun.write('dist/manifest.webmanifest', JSON.stringify(MANIFEST, null, 2)); const version = Bun.hash(html).toString(36); @@ -136,5 +156,5 @@ if (import.meta.main) { 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`); + console.log(`✓ PWA: manifest.webmanifest, sw.js (cold-${version}), ${STATIC.length} icons, jsqr.js`); } -- cgit v1.2.3