@@ -251,7 +305,23 @@ import { useBodyScrollLock } from '@/composables/useBodyScrollLock'
type Rail = 'onchain' | 'lightning' | 'cashu' | 'fedimint'
type Action = 'pay-invoice' | 'send-onchain' | 'redeem-token' | 'fedimint-join'
-type Pane = 'scan' | 'amount' | 'success'
+type Pane = 'choose' | 'scan' | 'amount' | 'success'
+
+// JS bridge the Android companion app injects: when present, live scanning is
+// delegated to a native camera modal (styled like this one) β the WebView's
+// getUserMedia preview lags badly on phones. Decodes come back through the
+// window.__archyQr* callbacks; we mirror status lines out to the native modal.
+interface ArchipelagoQrBridge {
+ open(): void
+ setStatus(message: string, isError: boolean): void
+ close(): void
+}
+interface NativeWindow extends Window {
+ ArchipelagoQr?: ArchipelagoQrBridge
+ __archyQrResult?: (text: string) => void
+ __archyQrCancelled?: () => void
+}
+const nativeWin = window as NativeWindow
const PRESETS = [21, 2100, 21000, 100000]
@@ -263,9 +333,10 @@ useModalKeyboard(modalRef, computed(() => props.show), close)
useBodyScrollLock(computed(() => props.show))
// --- Pane state ---
-const pane = ref
('scan')
+const pane = ref('choose')
const direction = ref<'forward' | 'back'>('forward')
const paneTitle = computed(() => {
+ if (pane.value === 'choose') return 'Send'
if (pane.value === 'scan') return 'Scan to send'
if (pane.value === 'success') return 'Success'
if (action.value === 'fedimint-join') return 'Join federation'
@@ -279,15 +350,26 @@ function goTo(p: Pane, dir: 'forward' | 'back' = 'forward') {
}
function goBack() {
- if (pane.value === 'amount') {
+ if (pane.value === 'scan') {
+ stopScanning()
+ goTo('choose', 'back')
+ } else if (pane.value === 'amount') {
error.value = ''
- goTo('scan', 'back')
- nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
+ goTo('choose', 'back')
} else if (pane.value === 'success') {
close()
}
}
+// --- Chooser ---
+function chooseCamera() {
+ scanStatus.value = ''
+ scanStatusIsError.value = false
+ if (startNativeScan()) return
+ goTo('scan')
+ nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
+}
+
// --- Scanner ---
const videoElement = ref(null)
const isScanning = ref(false)
@@ -302,6 +384,27 @@ const pasteInput = ref('')
const qrScanner = ref(null)
const animatedDecoder = useAnimatedQRDecoder()
+// --- Native scanner (companion app) ---
+const nativeScanActive = ref(false)
+
+function startNativeScan(): boolean {
+ const bridge = nativeWin.ArchipelagoQr
+ if (!bridge) return false
+ nativeWin.__archyQrResult = (text: string) => handleScanned(text)
+ nativeWin.__archyQrCancelled = () => { nativeScanActive.value = false }
+ nativeScanActive.value = true
+ bridge.open()
+ return true
+}
+
+// Mirror scan status (animated-QR progress, "not recognised" errors) onto the
+// native modal's status strip while it's up.
+watch([scanStatus, scanStatusIsError], () => {
+ if (nativeScanActive.value) {
+ nativeWin.ArchipelagoQr?.setStatus(scanStatus.value, scanStatusIsError.value)
+ }
+})
+
async function startScanning() {
cameraError.value = false
scanStatusIsError.value = false
@@ -344,6 +447,10 @@ function stopScanning() {
qrScanner.value?.destroy()
qrScanner.value = null
isScanning.value = false
+ if (nativeScanActive.value) {
+ nativeScanActive.value = false
+ nativeWin.ArchipelagoQr?.close()
+ }
}
function submitPaste() {
@@ -357,6 +464,9 @@ function submitPaste() {
// undefined), but opens the native camera in any browser,
// PWA or WebView; the shot is decoded locally by qr-scanner's scanImage.
const photoInput = ref(null)
+// Chooser-pane picker: same decode path, but no capture attribute β the user
+// picks an existing image (screenshot, saved QR) rather than taking a photo.
+const uploadInput = ref(null)
const liveCameraUnavailable = computed(() => !navigator.mediaDevices?.getUserMedia)
async function onPhotoPicked(e: Event) {
@@ -663,7 +773,7 @@ async function confirmSend() {
function resetAll() {
stopScanning()
animatedDecoder.reset()
- pane.value = 'scan'
+ pane.value = 'choose'
direction.value = 'forward'
scanStatus.value = ''
scanStatusIsError.value = false
@@ -683,10 +793,10 @@ function close() {
emit('close')
}
+// Every open lands on the chooser β the camera never auto-starts.
watch(() => props.show, (open) => {
if (open) {
resetAll()
- nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
} else {
stopScanning()
}