"use client"; import { useState, useEffect, useRef } from "react"; import { useZxing } from "react-zxing"; import { ensureScannerCompatibility } from "./scanner-polyfill"; export default function EnhancedQRScanner({ onScan, width = 300, height = 300, disableTorch = false, // Add option to disable torch }) { const [error, setError] = useState(null); const [isSecureContext, setIsSecureContext] = useState(true); const [initialized, setInitialized] = useState(false); const [cameraActive, setCameraActive] = useState(false); const [hasCameras, setHasCameras] = useState(true); const [scanSuccess, setScanSuccess] = useState(false); const [manualInputValue, setManualInputValue] = useState(""); const [cameraPermissionDenied, setCameraPermissionDenied] = useState(false); const cameraStreamRef = useRef(null); const [retryCount, setRetryCount] = useState(0); // We're keeping torchEnabled state but only using it if torch is enabled const [torchEnabled, setTorchEnabled] = useState(false); const inputRef = useRef(null); // Define video constraints explicitly const videoConstraints = { facingMode: "environment", width: { min: 640, ideal: 1280, max: 1920 }, height: { min: 480, ideal: 720, max: 1080 }, }; // Setup the zxing scanner hook with explicit video constraints const { ref, torch } = useZxing({ onDecodeResult(result) { handleDecode(result.getText()); }, onError(error) { handleError(error); }, constraints: { video: videoConstraints, }, paused: !cameraActive, }); // Function to release any existing camera streams const releaseCameraResources = () => { try { // Release any stream that might be stored in our ref if (cameraStreamRef.current) { cameraStreamRef.current.getTracks().forEach((track) => { track.stop(); }); cameraStreamRef.current = null; console.log("Released existing camera resources"); } // Also try to find and release any active video tracks that might be running navigator.mediaDevices .getUserMedia({ audio: false, video: false }) .catch(() => { // This intentionally fails but helps release resources in some browsers }); } catch (err) { console.error("Error releasing camera:", err); } }; // Check if we're in a secure context and if camera is accessible useEffect(() => { // Ensure scanner compatibility ensureScannerCompatibility(); // Check if we're in a secure context (https or localhost) const secure = window.isSecureContext; setIsSecureContext(secure); // Check if camera can be accessed async function checkCameraAccess() { try { // First check if MediaDevices API is supported if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error("MediaDevices API is not supported in this browser"); } // Release any existing camera streams first releaseCameraResources(); // Explicitly request video permission - important to avoid the error const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false, }); // If we got a stream, we have permission - store it for later release if needed if (stream) { cameraStreamRef.current = stream; // We'll keep this stream active to prevent other apps from taking the camera // It will be released when component unmounts or when we retry camera access } // Now enumerate devices to confirm we have video devices const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter( (device) => device.kind === "videoinput" ); if (videoDevices.length === 0) { setError( "No camera detected. Please make sure your device has a camera." ); setHasCameras(false); } else { setHasCameras(true); setCameraActive(true); // Activate camera if we have permission } setInitialized(true); } catch (err) { console.error("Camera access error:", err); // Handle permission denied specifically if ( err.name === "NotAllowedError" || err.name === "PermissionDeniedError" ) { setCameraPermissionDenied(true); setError( "Camera permission denied. Please allow camera access in your browser settings." ); } else { setError(`Camera error: ${err.message}`); } setInitialized(true); } } if (isSecureContext) { checkCameraAccess(); } else { setInitialized(true); // Still mark as initialized even if not secure } // Cleanup function to release camera when component unmounts return () => { releaseCameraResources(); }; }, []); // Toggle torch/flashlight - only used if torch isn't disabled const toggleTorch = () => { if (!disableTorch && torch) { setTorchEnabled(!torchEnabled); torch.toggled = !torchEnabled; } }; // Handle decode result const handleDecode = (text) => { console.log("QR scan result:", text); if (!onScan || !text) return; try { // Show success visual setScanSuccess(true); setTimeout(() => setScanSuccess(false), 1500); // Call the onScan callback with the decoded text onScan(text); } catch (err) { console.error("Error processing QR scan result:", err); } }; // Manually request camera access with robust error handling const requestCameraAccess = async () => { try { setError(null); setCameraActive(false); // First release any existing camera resources releaseCameraResources(); // Add a small delay to ensure resources are properly released await new Promise((resolve) => setTimeout(resolve, 500)); // Now try to get fresh camera access const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false, }); // If we got a stream, we have permission if (stream) { // Store the stream reference for potential future release cameraStreamRef.current = stream; // Release this stream since useZxing will request its own stream.getTracks().forEach((track) => track.stop()); setCameraActive(true); setCameraPermissionDenied(false); setRetryCount(0); // Reset retry count on success } } catch (err) { console.error("Failed to get camera permission:", err); // Increment retry count setRetryCount((prev) => prev + 1); if ( err.name === "NotAllowedError" || err.name === "PermissionDeniedError" ) { setCameraPermissionDenied(true); setError( "Camera permission denied. Please allow camera access in your browser settings." ); } else if ( err.name === "NotReadableError" || err.name === "TrackStartError" || retryCount < 2 // Try a few times before showing hardware error ) { setError( `Camera temporarily unavailable. Please try again in a few moments.` ); } else { setError( `Camera error: ${err.message}. You may need to restart your browser or device.` ); } } }; // Handle manual input submission const handleManualSubmit = () => { if (manualInputValue.trim()) { onScan(manualInputValue.trim()); setManualInputValue(""); } }; // Handle camera errors const handleError = (error) => { console.error("QR Scanner error:", error); // Don't immediately deactivate camera on all errors // Some errors can be temporary if ( error.name === "NotAllowedError" || error.name === "PermissionDeniedError" ) { setCameraActive(false); } // More descriptive error messages let errorMessage = "Camera access error. "; if (!isSecureContext) { errorMessage += "Your browser requires HTTPS for camera access. When running locally, use http://localhost instead of your IP address."; } else if ( error.name === "NotAllowedError" || error.name === "PermissionDeniedError" ) { errorMessage += "Camera permission denied. Please allow camera access in your browser settings."; setCameraPermissionDenied(true); } else if ( error.name === "NotFoundError" || error.name === "DevicesNotFoundError" ) { errorMessage += "No camera found. Please ensure your device has a working camera."; } else if ( error.name === "NotReadableError" || error.name === "TrackStartError" ) { errorMessage += "Camera is already in use by another application or encountered a hardware error."; } else { errorMessage += `${ error.message || "Please check your camera permissions and refresh the page." }`; } setError(errorMessage); }; // Handle secure context warning if (!isSecureContext) { return (

Camera access requires a secure context (HTTPS or localhost).

When running locally, use http://localhost:3000 instead of your network IP.

); } // Handle initialization state if (!initialized) { return (
Initializing camera...
); } return (
{error && (

{error}

{cameraPermissionDenied ? (

To fix this issue:

  1. Click the camera icon in your browser's address bar
  2. Select "Allow" for camera access
  3. Refresh this page
) : ( )}
)} {cameraActive && !error && (
)} {/* Text input fallback when camera fails */} {(!cameraActive || error) && (

Enter the QR code manually:

setManualInputValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleManualSubmit(); } }} ref={inputRef} />
)}
); } // Helper component for manual input function ManualInput({ onScan }) { const [value, setValue] = useState(""); const handleSubmit = () => { if (value.trim()) { onScan(value.trim()); setValue(""); } }; return (

Manual Code Entry

Enter the QR code value manually:

setValue(e.target.value)} placeholder="Enter code (e.g. QR1)" onKeyDown={(e) => { if (e.key === "Enter") { handleSubmit(); } }} />
); }