Files
archy/neode-ui/src/composables/useAnimatedQRDecoder.ts
T
archipelagoandClaude Fable 5 8968d41ca9 feat(wallet): QR scan modal — camera + animated QR, wired into Home/Send/Receive
New WalletScanModal scans QR codes with the device camera (BarcodeDetector
with jsQR fallback) including animated/multi-frame QRs via qrloop, then
routes what it saw: BOLT11 invoices -> lightning pay (amount locked from
the invoice when present), bitcoin:/BIP21 URIs -> on-chain send, Cashu
tokens -> redeem, Fedimint invites -> join. Entry points: camera button on
the wallet card (far right), and a Scan button centered between the
Close/Send and Close/Receive buttons of both modals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:14:31 -04:00

61 lines
1.8 KiB
TypeScript

import { ref } from 'vue'
import {
parseFramesReducer,
areFramesComplete,
framesToData,
totalNumberOfFrames,
currentNumberOfFrames,
} from 'qrloop'
/**
* Collects animated-QR frames (qrloop format, as used by k484 for large
* Fedimint tokens) and reassembles them into the original token string.
*/
export function useAnimatedQRDecoder() {
const framesState = ref<ReturnType<typeof parseFramesReducer> | null>(null)
const isComplete = ref(false)
const decodedData = ref<string | null>(null)
const uniqueFrames = ref<Set<string>>(new Set())
/** Feed one scanned frame; returns true once the full payload is decoded. */
function addFrame(frame: string): boolean {
if (isComplete.value) return true
if (uniqueFrames.value.has(frame)) return false
uniqueFrames.value.add(frame)
try {
framesState.value = parseFramesReducer(framesState.value, frame)
if (areFramesComplete(framesState.value)) {
const dataBuffer = framesToData(framesState.value)
// Tokens travel as URL-safe base64
decodedData.value = dataBuffer
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
isComplete.value = true
return true
}
return false
} catch {
// A frame that qrloop rejects may just be a different QR format
return false
}
}
function progressText(): string {
if (!framesState.value) return ''
const total = totalNumberOfFrames(framesState.value)
const current = currentNumberOfFrames(framesState.value)
return `${current}/${total} frames`
}
function reset() {
framesState.value = null
uniqueFrames.value.clear()
isComplete.value = false
decodedData.value = null
}
return { isComplete, decodedData, addFrame, reset, progressText }
}