import { useState, useRef, useEffect } from 'react'; import { Camera, X, CheckCircle2, AlertCircle } from 'lucide-react'; import jsQR from 'jsqr'; interface QRScannerProps { onScanSuccess: (data: string | Uint8Array) => void; onClose: () => void; onError?: (error: string) => void; } export default function QRScanner({ onScanSuccess, onClose, onError }: QRScannerProps) { const [internalError, setInternalError] = useState(''); const [success, setSuccess] = useState(false); const [hasPermission, setHasPermission] = useState(null); const videoRef = useRef(null); const canvasRef = useRef(null); useEffect(() => { let stream: MediaStream | null = null; let scanInterval: number | null = null; let isCancelled = false; const stopScanning = () => { if (scanInterval) clearInterval(scanInterval); if (stream) stream.getTracks().forEach(track => track.stop()); }; const startScanning = async () => { try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); if (isCancelled) return stopScanning(); setHasPermission(true); const video = videoRef.current; const canvas = canvasRef.current; if (video && canvas) { video.srcObject = stream; await video.play(); if (isCancelled) return stopScanning(); const ctx = canvas.getContext('2d'); if (!ctx) { setInternalError('Canvas context not available'); return stopScanning(); } scanInterval = window.setInterval(() => { if (isCancelled || !video || video.paused || video.ended) return; if (!video.videoWidth || !video.videoHeight) return; canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' }); if (code) { isCancelled = true; stopScanning(); setSuccess(true); // jsQR gives us raw bytes! const rawBytes = code.binaryData; // Detect binary (16 or 32 bytes with non-printable chars) const isBinary = (rawBytes.length === 16 || rawBytes.length === 32) && /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(String.fromCharCode(...Array.from(rawBytes))); if (isBinary) { onScanSuccess(new Uint8Array(rawBytes)); } else { // Text QR - use the text property onScanSuccess(code.data); } setTimeout(() => onClose(), 1000); } }, 300); } } catch (err: any) { // IMPORTANT: Check for null/undefined err object first. if (!err) { console.error('Caught a null or undefined error inside QRScanner.'); return; // Exit if error is falsy } if (isCancelled || err.name === 'AbortError') { console.log('Camera operation was cancelled or aborted, which is expected on unmount.'); return; // Ignore abort errors, they are expected on cleanup } console.error('Camera error:', err); setHasPermission(false); let errorMsg = 'An unknown camera error occurred.'; if (err.name === 'NotAllowedError') { errorMsg = 'Camera access was denied. Please grant permission in your browser settings.'; } else if (err.name === 'NotFoundError') { errorMsg = 'No camera found on this device.'; } else if (err.name === 'NotReadableError') { errorMsg = 'Cannot access the camera. It may be in use by another application or browser tab.'; } else if (err.name === 'OverconstrainedError') { errorMsg = 'The camera does not meet the required constraints.'; } else if (err instanceof Error) { errorMsg = `Camera error: ${err.message}`; } setInternalError(errorMsg); onError?.(errorMsg); } }; startScanning(); return () => { isCancelled = true; stopScanning(); }; }, [onScanSuccess, onClose, onError]); return (

Scan QR Code

{internalError && (
{internalError}
)} {success && (
QR Code detected!
)}
{!hasPermission && !internalError && (

Requesting camera access...

)}
); }