"use client"; import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import Webcam from "react-webcam"; import { useQRScanner } from "@/hooks/useQRScanner"; export type QRScannerProps = { onResult?: (text: string) => void; onError?: (error: unknown) => void; /** When true the camera stops; when it goes back to false the camera auto-resumes if it was on. */ paused?: boolean; }; export type QRScannerHandle = { /** Hard-stop the scanner (user must press "Scan Ticket" to restart). */ stop: () => void; }; // Mobile-friendly QR scanner with start/stop control using the back camera via react-webcam export const QRScanner = React.forwardRef( function QRScanner({ onResult, onError, paused = false }, ref) { const webcamRef = useRef(null); const [localError, setLocalError] = useState(null); const [showCam, setShowCam] = useState(false); // Track the underlying HTMLVideoElement from react-webcam reliably const [videoEl, setVideoEl] = useState(null); useEffect(() => { let id: any; if (showCam) { id = setInterval(() => { const anyCam = webcamRef.current as any; const v: HTMLVideoElement | null = anyCam?.video ?? null; if (v) { setVideoEl(v); clearInterval(id); } }, 100); } else { setVideoEl(null); } return () => { if (id) clearInterval(id); }; }, [showCam]); // Beep + cooldown state const lastScanRef = useRef(0); const audioCtxRef = useRef(null); const playBeep = () => { try { const AC = (window as any).AudioContext || (window as any).webkitAudioContext; if (!AC) return; if (!audioCtxRef.current) audioCtxRef.current = new AC(); const ctx = audioCtxRef.current as AudioContext; const oscillator = ctx.createOscillator(); const gainNode = ctx.createGain(); oscillator.type = "sine"; oscillator.frequency.value = 880; gainNode.gain.value = 0.05; oscillator.connect(gainNode); gainNode.connect(ctx.destination); const now = ctx.currentTime; oscillator.start(now); oscillator.stop(now + 0.12); } catch {} }; const wrappedOnResult = (text: string) => { const nowTs = Date.now(); if (nowTs - lastScanRef.current < 2000) return; lastScanRef.current = nowTs; playBeep(); onResult?.(text); }; const { active, toggle, stop, permissionError } = useQRScanner(videoEl, { onResult: wrappedOnResult, onError, }); const constraints = useMemo(() => ({ facingMode: { ideal: "environment" }, width: { ideal: 1280 }, height: { ideal: 720 }, }), []); const isSecure = typeof window !== "undefined" && window.isSecureContext; const isOn = showCam; // Stable stop function so effects and the ref handle can depend on it safely const stopScan = useCallback(() => { try { stop(); } catch {} setShowCam(false); }, [stop]); // Expose hard-stop to parent via ref (used when filters change) useImperativeHandle(ref, () => ({ stop: stopScan }), [stopScan]); // Pause / auto-resume when the paused prop changes (e.g. a modal opens/closes) const wasActiveRef = useRef(false); useEffect(() => { if (paused) { if (showCam) { wasActiveRef.current = true; stopScan(); } } else { if (wasActiveRef.current) { wasActiveRef.current = false; // Re-show the camera — active is still true so the decode loop restarts automatically // once the video element is acquired again. setShowCam(true); } } // showCam intentionally included: re-evaluate after stopScan sets it to false // eslint-disable-next-line react-hooks/exhaustive-deps }, [paused, stopScan]); async function startScan() { const host = typeof window !== "undefined" ? window.location.hostname : ""; const isLocal = host === "localhost" || host === "127.0.0.1"; if (!(navigator as any)?.mediaDevices?.getUserMedia) { setLocalError("Camera API is not available in this browser."); return; } setLocalError(null); try { const warmup = await navigator.mediaDevices.getUserMedia({ video: constraints, audio: false } as any); try { warmup.getTracks().forEach(t => t.stop()); } catch {} setShowCam(true); if (!active) toggle(); } catch (e: any) { const name = e?.name || e?.message; if (name === "NotAllowedError") { setLocalError("Camera permission denied. Please enable camera access in your browser settings."); } else if (name === "NotFoundError" || name === "OverconstrainedError") { setLocalError("No suitable camera found. Try a device with a back camera."); } else if (name === "NotReadableError") { setLocalError("Camera is in use by another app. Close other apps and try again."); } else if (!isSecure && !isLocal) { setLocalError("Camera access requires HTTPS or running on http://localhost."); } else { setLocalError("Unable to access camera."); } if (active) stop(); setShowCam(false); } } useEffect(() => { return () => { stopScan(); }; }, [stopScan]); return (

QR Scanner

{isOn ? ( setLocalError(null)} onUserMediaError={(e) => { const name = (e as any)?.name || (e as any)?.message; const protocol = typeof window !== "undefined" ? window.location.protocol : ""; const host = typeof window !== "undefined" ? window.location.hostname : ""; const isLocal = host === "localhost" || host === "127.0.0.1"; if (protocol !== "https:" && !isLocal) { setLocalError("Camera access requires HTTPS or running on http://localhost."); return; } if (name === "NotAllowedError") { setLocalError("Camera permission denied. Please enable camera access in your browser settings."); } else if (name === "NotFoundError") { setLocalError("No suitable camera found. Try a device with a back camera."); } else { setLocalError("Unable to access camera."); } }} /> ) : (
Camera is off. Tap "Scan Ticket" to start using the back camera.
)}
{(permissionError || localError) && (

{permissionError || localError}

)}
); } );