import { useEffect, useRef, useState } from 'react'; import { pushOverlayBack } from './platform.js'; import type { ScanResult } from './scanner.js'; import { Icon } from './ui.js'; type Formats = 'card' | 'qr'; /** Fullscreen camera view that resolves with the first detected code. */ export function ScannerView({ formats, hint, onResult, onClose }: { formats: Formats; hint: string; onResult: (result: ScanResult) => void; onClose: () => void }) { const videoRef = useRef(null); const [error, setError] = useState(); const onResultRef = useRef(onResult); onResultRef.current = onResult; // Android back button closes the scanner, not the screen underneath. const closeRef = useRef(onClose); closeRef.current = onClose; useEffect(() => pushOverlayBack(closeRef), []); useEffect(() => { let stopped = false; let stream: MediaStream | undefined; let timer: ReturnType | undefined; (async () => { try { const { createDetector, CARD_FORMATS, QR_FORMATS } = await import('./scanner.js'); const detect = createDetector(formats === 'qr' ? QR_FORMATS : CARD_FORMATS); stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false, }); if (stopped) return; const video = videoRef.current!; video.srcObject = stream; await video.play(); const tick = async () => { if (stopped) return; try { if (video.readyState >= 2) { const found = await detect(video); if (found && !stopped) { stopped = true; onResultRef.current(found); return; } } } catch { // a frame that could not be decoded } timer = setTimeout(tick, 200); }; void tick(); } catch (e) { const name = (e as Error).name; setError( name === 'NotAllowedError' ? 'Camera access was denied. Allow it in the app or browser settings.' : name === 'NotFoundError' ? 'No camera found.' : `Cannot start the camera: ${(e as Error).message}`, ); } })(); return () => { stopped = true; if (timer) clearTimeout(timer); stream?.getTracks().forEach((t) => t.stop()); }; }, [formats]); return (