import { useEffect, useRef, useState } from 'react'; import { Html5Qrcode } from 'html5-qrcode'; interface QRCodeScannerProps { onScan: (userId: string) => void; onError?: (error: string) => void; } export function QRCodeScanner({ onScan, onError }: QRCodeScannerProps) { const [isScanning, setIsScanning] = useState(false); const [error, setError] = useState(null); const [showManualInput, setShowManualInput] = useState(false); const [manualInput, setManualInput] = useState(''); const [showTroubleshooting, setShowTroubleshooting] = useState(false); const scannerRef = useRef(null); const scannerIdRef = useRef('qr-reader'); // Detect if user is on iOS const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); useEffect(() => { return () => { // Cleanup on unmount if (scannerRef.current) { scannerRef.current.stop().catch(console.error); } }; }, []); const startScanning = async () => { try { setError(null); // First, check if we can get camera devices (this will trigger permission prompt) try { const devices = await Html5Qrcode.getCameras(); if (!devices || devices.length === 0) { throw new Error('No camera found on this device.'); } } catch (permErr: any) { // If we can't get cameras, it might be a permission issue console.error('Camera access error:', permErr); } const scanner = new Html5Qrcode(scannerIdRef.current); scannerRef.current = scanner; await scanner.start( { facingMode: 'environment' }, // Use back camera { fps: 10, qrbox: { width: 250, height: 250 }, }, (decodedText) => { try { const data = JSON.parse(decodedText); if (data.type === 'attendee-checkin' && data.userId) { onScan(data.userId); } else { const errorMsg = 'Invalid QR code format'; setError(errorMsg); onError?.(errorMsg); } } catch (err) { const errorMsg = 'Failed to parse QR code'; setError(errorMsg); onError?.(errorMsg); } }, () => { // Ignore scanning errors (happens continuously while scanning) } ); setIsScanning(true); } catch (err: any) { let errorMsg = 'Failed to start camera'; console.error('Scanner error:', err); if (err.name === 'NotAllowedError' || err.message?.includes('NotAllowedError')) { errorMsg = 'Camera permission denied. Please click "Allow" when your browser asks for camera access.'; } else if (err.name === 'NotFoundError' || err.message?.includes('NotFoundError')) { errorMsg = 'No camera found on this device.'; } else if (err.name === 'NotReadableError' || err.message?.includes('NotReadableError')) { errorMsg = 'Camera is already in use by another application.'; } else if (err.name === 'OverconstrainedError' || err.message?.includes('OverconstrainedError')) { errorMsg = 'Camera constraints not supported. Trying with default camera...'; // Try again without facingMode constraint setTimeout(() => startScanningWithoutConstraints(), 1000); return; } else if (err.message?.includes('secure') || err.message?.includes('https')) { errorMsg = 'Camera access requires HTTPS. Use manual input instead.'; } else if (err.message) { errorMsg = `Camera error: ${err.message}`; } setError(errorMsg); onError?.(errorMsg); setIsScanning(false); setShowManualInput(true); // Show manual input as fallback } }; const startScanningWithoutConstraints = async () => { try { setError(null); const scanner = new Html5Qrcode(scannerIdRef.current); scannerRef.current = scanner; // Try with any available camera const devices = await Html5Qrcode.getCameras(); if (devices && devices.length > 0 && devices[0]) { await scanner.start( devices[0].id, // Use first available camera { fps: 10, qrbox: { width: 250, height: 250 }, }, (decodedText) => { try { const data = JSON.parse(decodedText); if (data.type === 'attendee-checkin' && data.userId) { onScan(data.userId); } else { const errorMsg = 'Invalid QR code format'; setError(errorMsg); onError?.(errorMsg); } } catch (err) { const errorMsg = 'Failed to parse QR code'; setError(errorMsg); onError?.(errorMsg); } }, () => { // Ignore scanning errors } ); setIsScanning(true); } } catch (err: any) { console.error('Fallback scanner error:', err); setError('Unable to access camera. Please use manual check-in instead.'); setShowManualInput(true); } }; const stopScanning = async () => { if (scannerRef.current) { try { await scannerRef.current.stop(); scannerRef.current = null; setIsScanning(false); } catch (err) { console.error('Error stopping scanner:', err); } } }; const handleManualSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!manualInput.trim()) return; try { const data = JSON.parse(manualInput); if (data.type === 'attendee-checkin' && data.userId) { onScan(data.userId); setManualInput(''); setShowManualInput(false); } else { setError('Invalid QR code data format'); } } catch (err) { setError('Invalid JSON format. Please scan the QR code or paste valid data.'); } }; return (
{/* Info message before scanning */} {!isScanning && !error && (

Camera Permission Required

When you click "Start Camera Scan", your browser will ask for camera permission. Please click "Allow" to enable QR code scanning.

)}
{error && (

{error}

{error.includes('HTTPS') && (

💡 Tip: Camera access requires a secure connection (HTTPS) in most browsers. Use manual input below as an alternative.

)} {error.includes('permission') && (

💡 Tip: Check your browser's address bar for camera permission settings, or use manual input below.

)}
)}
{!isScanning ? ( <> ) : ( )}
{isScanning && (

Position the QR code within the frame to scan

)} {/* Manual Input Form */} {showManualInput && !isScanning && (

Manual Check-In

Paste the QR code data or enter the user's Clerk ID directly