import { QRScannerService, ScanListener, QRScannerOptions } from "./types"; import { logger } from "@/utils/logger"; import { EventEmitter } from "events"; export class WebInlineQRScanner implements QRScannerService { private scanListener: ScanListener | null = null; private isScanning = false; private stream: MediaStream | null = null; private events = new EventEmitter(); constructor(private options?: QRScannerOptions) {} async checkPermissions(): Promise { try { logger.log("[QRScanner] Checking camera permissions..."); const permissions = await navigator.permissions.query({ name: "camera" as PermissionName, }); logger.log("[QRScanner] Permission state:", permissions.state); return permissions.state === "granted"; } catch (error) { logger.error("[QRScanner] Error checking camera permissions:", error); return false; } } async requestPermissions(): Promise { try { // First check if we have any video devices const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter( (device) => device.kind === "videoinput", ); if (videoDevices.length === 0) { logger.error("No video devices found"); throw new Error("No camera found on this device"); } // Try to get a stream with specific constraints const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 }, }, }); // Stop the test stream immediately stream.getTracks().forEach((track) => track.stop()); return true; } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error requesting camera permissions:", { error: wrappedError.message, stack: wrappedError.stack, name: wrappedError.name, }); // Provide more specific error messages if ( wrappedError.name === "NotFoundError" || wrappedError.name === "DevicesNotFoundError" ) { throw new Error("No camera found on this device"); } else if ( wrappedError.name === "NotAllowedError" || wrappedError.name === "PermissionDeniedError" ) { throw new Error( "Camera access denied. Please grant camera permission and try again", ); } else if ( wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError" ) { throw new Error("Camera is in use by another application"); } else { throw new Error(`Camera error: ${wrappedError.message}`); } } } async isSupported(): Promise { try { // Check for secure context first if (!window.isSecureContext) { logger.warn("Camera access requires HTTPS (secure context)"); return false; } // Check for camera API support if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { logger.warn("Camera API not supported in this browser"); return false; } // Check if we have any video devices const devices = await navigator.mediaDevices.enumerateDevices(); const hasVideoDevices = devices.some( (device) => device.kind === "videoinput", ); if (!hasVideoDevices) { logger.warn("No video devices found"); return false; } return true; } catch (error) { logger.error("Error checking camera support:", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); return false; } } async startScan(): Promise { if (this.isScanning) { return; } try { this.isScanning = true; logger.log("[WebInlineQRScanner] Starting scan"); // Get camera stream this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 }, }, }); // Emit stream to component this.events.emit("stream", this.stream); } catch (error) { this.isScanning = false; const wrappedError = error instanceof Error ? error : new Error(String(error)); if (this.scanListener?.onError) { this.scanListener.onError(wrappedError); } logger.error("Error starting scan:", wrappedError); throw wrappedError; } } async stopScan(): Promise { if (!this.isScanning) { return; } try { logger.log("[WebInlineQRScanner] Stopping scan"); // Stop all tracks in the stream if (this.stream) { this.stream.getTracks().forEach((track) => track.stop()); this.stream = null; } // Emit stream stopped event this.events.emit("stream", null); } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error stopping scan:", wrappedError); throw wrappedError; } finally { this.isScanning = false; } } addListener(listener: ScanListener): void { this.scanListener = listener; } // Add method to get stream events onStream(callback: (stream: MediaStream | null) => void): void { this.events.on("stream", callback); } async cleanup(): Promise { try { await this.stopScan(); this.events.removeAllListeners(); } catch (error) { logger.error("Error during cleanup:", error); } } }