import { createApp, App } from "vue"; import { QRScannerService, ScanListener, QRScannerOptions } from "./types"; import QRScannerDialog from "@/components/QRScanner/QRScannerDialog.vue"; import { logger } from "@/utils/logger"; export class WebDialogQRScanner implements QRScannerService { private dialogInstance: App | null = null; private dialogComponent: InstanceType | null = null; private scanListener: ScanListener | null = null; private isScanning = false; private container: HTMLElement | null = null; constructor(private options?: QRScannerOptions) {} async checkPermissions(): Promise { try { const permissions = await navigator.permissions.query({ name: "camera" as PermissionName, }); return permissions.state === "granted"; } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error checking camera permissions:", wrappedError); 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; // Create and mount dialog component this.container = document.createElement("div"); document.body.appendChild(this.container); this.dialogInstance = createApp(QRScannerDialog, { onScan: (result: string) => { if (this.scanListener) { this.scanListener.onScan(result); } }, onError: (error: Error) => { if (this.scanListener?.onError) { this.scanListener.onError(error); } }, options: this.options, }); this.dialogComponent = this.dialogInstance.mount(this.container).$refs .dialog as InstanceType; } 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); this.cleanupContainer(); throw wrappedError; } } async stopScan(): Promise { if (!this.isScanning) { return; } try { if (this.dialogComponent) { await this.dialogComponent.close(); } if (this.dialogInstance) { this.dialogInstance.unmount(); } } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error stopping scan:", wrappedError); throw wrappedError; } finally { this.isScanning = false; this.cleanupContainer(); } } addListener(listener: ScanListener): void { this.scanListener = listener; } private cleanupContainer(): void { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.container = null; } async cleanup(): Promise { try { await this.stopScan(); } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error during cleanup:", wrappedError); throw wrappedError; } finally { this.dialogComponent = null; this.dialogInstance = null; this.scanListener = null; this.cleanupContainer(); } } }