diff --git a/neode-ui/src/components/WalletScanModal.vue b/neode-ui/src/components/WalletScanModal.vue index 8c38f345..38a3759f 100644 --- a/neode-ui/src/components/WalletScanModal.vue +++ b/neode-ui/src/components/WalletScanModal.vue @@ -363,9 +363,10 @@ async function onPhotoPicked(e: Event) { const input = e.target as HTMLInputElement const file = input.files?.[0] if (!file) return + scanStatusIsError.value = false + scanStatus.value = 'Reading photo…' try { - const result = await QrScanner.scanImage(file, { returnDetailedScanResult: true }) - handleScanned(result.data) + handleScanned(await decodePhotoRobust(file)) } catch { scanStatusIsError.value = true scanStatus.value = 'No QR code found in that photo — try again, closer and well-lit' @@ -374,6 +375,28 @@ async function onPhotoPicked(e: Event) { } } +/** Decode a QR photo with every engine we have. On the companion app the + * photo path IS the scan path (plain-http LAN = no secure context = no + * live camera), and Lightning invoices make DENSE codes that the wasm + * engine's single pass often misses (reported 2026-07-22: "camera not + * picking up the invoice"). Android WebView's native BarcodeDetector is + * far stronger on dense codes, so try it first; fall back to qr-scanner. */ +async function decodePhotoRobust(file: File): Promise { + try { + const Detector = (window as unknown as { BarcodeDetector?: new (opts: { formats: string[] }) => { detect(src: ImageBitmap): Promise> } }).BarcodeDetector + if (Detector) { + const bmp = await createImageBitmap(file) + const codes = await new Detector({ formats: ['qr_code'] }).detect(bmp) + const hit = codes.find(c => c.rawValue) + if (hit) return hit.rawValue + } + } catch { + // Native detector unavailable/failed — wasm engine below. + } + const result = await QrScanner.scanImage(file, { returnDetailedScanResult: true }) + return result.data +} + // --- Detection (ported from k484's scanner) --- const rail = ref('lightning') const action = ref('pay-invoice')