From a65208944129f5148810878904dd7c0fd0f46cf7 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 20 Sep 2026 14:39:10 +0200 Subject: [PATCH 1/2] fix(access): drop the loaded Bolt Card after its first Complete attempt The card loaded at tap-to-enter carries one SUN p/c pair, and the boltcards server bumps the counter on the first GET. After a declined Complete (limit below amount, callback failure, payment error) the stored lnurlw can never succeed again, yet it stayed loaded with a Complete button that would keep failing. The same held on the success path when the payment never settled (cash-out invoice timeout back to selectingAmount). The tap handlers now report skipped / accepted / declined, and completeWithCard clears the card after any real attempt, appending 'tap your card to try again' to a decline. A skipped outcome (guard bounced it, no server call) keeps the card. A fresh tap on the cash screen goes through the normal tap-to-pay/receive path. Co-Authored-By: Claude Fable 5.1 --- .../src/services/access/mock-reader.ts | 53 ------------- .../src/services/access/qr-npub-reader.ts | 79 ------------------- apps/machine/src/stores/atm.ts | 60 ++++++++++---- 3 files changed, 45 insertions(+), 147 deletions(-) delete mode 100644 apps/machine/src/services/access/mock-reader.ts delete mode 100644 apps/machine/src/services/access/qr-npub-reader.ts diff --git a/apps/machine/src/services/access/mock-reader.ts b/apps/machine/src/services/access/mock-reader.ts deleted file mode 100644 index c918b18..0000000 --- a/apps/machine/src/services/access/mock-reader.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Mock access reader (ADR-003) — SCAFFOLD, no hardware. - * - * A keyboard/console fallback for when no camera or NFC reader is present - * (headless dev, CI, a batm3 with a dead camera). Emits an npub scan on - * demand via two triggers: - * - `window.__bitspireMockCard(npub?)` — from the LockedView dev button or - * the devtools console. - * - the `F9` key — a quick tap on the physical machine. - * - * Registered only when it is the sole available reader (see `index.ts`), so it - * never shadows the real camera/NFC path. - */ - -import { npubEncode } from 'nostr-tools/nip19' -import type { AccessReader, AccessReaderStartOptions, StopCapture } from './types' - -/** - * Default npub the mock emits when none is supplied — derived from a fixed - * (all-ones) hex pubkey so it carries a valid bech32 checksum and survives - * `authorize()`'s nip19 decode. Not a real key; dev-only. - */ -export const MOCK_NPUB = npubEncode('11'.repeat(32)) - -interface MockCardGlobal { - __bitspireMockCard?: (npub?: string) => void -} - -export class MockAccessReader implements AccessReader { - readonly kind = 'mock' as const - readonly label = 'Mock reader (dev)' - - async isAvailable(): Promise { - return true - } - - async start(opts: AccessReaderStartOptions): Promise { - const emit = (npub: string = MOCK_NPUB) => opts.onScan({ kind: 'npub', npub }) - - const g = globalThis as unknown as MockCardGlobal - g.__bitspireMockCard = emit - - const onKey = (e: KeyboardEvent) => { - if (e.key === 'F9') emit() - } - window.addEventListener('keydown', onKey) - - return () => { - window.removeEventListener('keydown', onKey) - if (g.__bitspireMockCard === emit) delete g.__bitspireMockCard - } - } -} diff --git a/apps/machine/src/services/access/qr-npub-reader.ts b/apps/machine/src/services/access/qr-npub-reader.ts deleted file mode 100644 index e95c507..0000000 --- a/apps/machine/src/services/access/qr-npub-reader.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * QR-npub access reader (ADR-003, PROTOTYPE). - * - * Until the NFC reader hardware exists, the batm3's camera — the same one the - * pairing wizard uses — reads a QR "badge" that encodes the user's npub. The - * decoded npub is handed to `authorize()`, which admits it (optionally behind - * a PIN). This is a thin adapter onto the same `qr/dom.js` decode loop as - * `pairing/qr-source.ts`; see that file for the capture-resolution rationale. - * - * It emits the raw decoded string as an `npub` scan and lets `authorize()` - * validate it — a stray, non-npub QR is rejected there, not here. - */ - -import { QRCanvas, frontalCamera, frameLoop } from 'qr/dom.js' -import type { AccessReader, AccessReaderStartOptions, StopCapture } from './types' - -export class QrNpubAccessReader implements AccessReader { - readonly kind = 'qr-npub' as const - readonly label = 'Camera (npub QR)' - - async isAvailable(): Promise { - return ( - typeof navigator !== 'undefined' && - !!navigator.mediaDevices && - typeof navigator.mediaDevices.getUserMedia === 'function' - ) - } - - async start(opts: AccessReaderStartOptions): Promise { - const { onScan, onError, video } = opts - if (!video) throw new Error('QrNpubAccessReader requires a