import { BarcodeScanner, BarcodeFormat, StartScanOptions, LensFacing, } from "@capacitor-mlkit/barcode-scanning"; import { QRScannerService, ScanListener, QRScannerOptions } from "./types"; import { logger } from "@/utils/logger"; export class CapacitorQRScanner implements QRScannerService { private scanListener: ScanListener | null = null; private isScanning = false; private listenerHandles: Array<() => Promise> = []; private cleanupPromise: Promise | null = null; async checkPermissions(): Promise { try { logger.debug("Checking camera permissions"); const { camera } = await BarcodeScanner.checkPermissions(); return camera === "granted"; } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error checking camera permissions:", { error: wrappedError.message, }); return false; } } async requestPermissions(): Promise { try { // First check if we already have permissions if (await this.checkPermissions()) { logger.debug("Camera permissions already granted"); return true; } logger.debug("Requesting camera permissions"); const { camera } = await BarcodeScanner.requestPermissions(); const granted = camera === "granted"; logger.debug(`Camera permissions ${granted ? "granted" : "denied"}`); return granted; } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error requesting camera permissions:", { error: wrappedError.message, }); return false; } } async isSupported(): Promise { try { logger.debug("Checking scanner support"); const { supported } = await BarcodeScanner.isSupported(); logger.debug(`Scanner support: ${supported}`); return supported; } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error checking scanner support:", { error: wrappedError.message, }); return false; } } async startScan(options?: QRScannerOptions): Promise { if (this.isScanning) { logger.debug("Scanner already running"); return; } if (this.cleanupPromise) { logger.debug("Waiting for previous cleanup to complete"); await this.cleanupPromise; } try { // Ensure we have permissions before starting if (!(await this.checkPermissions())) { logger.debug("Requesting camera permissions"); const granted = await this.requestPermissions(); if (!granted) { throw new Error("Camera permission denied"); } } // Check if scanning is supported if (!(await this.isSupported())) { throw new Error("QR scanning not supported on this device"); } logger.info("Starting MLKit scanner"); this.isScanning = true; const scanOptions: StartScanOptions = { formats: [BarcodeFormat.QrCode], lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back, }; logger.debug("Scanner options:", scanOptions); // Add listener for barcode scans const handle = await BarcodeScanner.addListener( "barcodeScanned", (result) => { if (this.scanListener && result.barcode?.rawValue) { this.scanListener.onScan(result.barcode.rawValue); } }, ); this.listenerHandles.push(handle.remove); // Start continuous scanning await BarcodeScanner.startScan(scanOptions); logger.info("MLKit scanner started successfully"); } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error during QR scan:", { error: wrappedError.message, stack: wrappedError.stack, }); this.isScanning = false; await this.cleanup(); this.scanListener?.onError?.(wrappedError); throw wrappedError; } } async stopScan(): Promise { if (!this.isScanning) { logger.debug("Scanner not running"); return; } try { logger.debug("Stopping QR scanner"); await BarcodeScanner.stopScan(); logger.info("QR scanner stopped successfully"); } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error stopping QR scan:", { error: wrappedError.message, stack: wrappedError.stack, }); this.scanListener?.onError?.(wrappedError); throw wrappedError; } finally { this.isScanning = false; } } addListener(listener: ScanListener): void { this.scanListener = listener; } async cleanup(): Promise { // Prevent multiple simultaneous cleanup attempts if (this.cleanupPromise) { return this.cleanupPromise; } this.cleanupPromise = (async () => { try { logger.debug("Starting QR scanner cleanup"); // Stop scanning if active if (this.isScanning) { await this.stopScan(); } // Remove all listeners for (const handle of this.listenerHandles) { try { await handle(); } catch (error) { logger.warn("Error removing listener:", error); } } logger.info("QR scanner cleanup completed"); } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); logger.error("Error during cleanup:", { error: wrappedError.message, stack: wrappedError.stack, }); throw wrappedError; } finally { this.listenerHandles = []; this.scanListener = null; this.cleanupPromise = null; } })(); return this.cleanupPromise; } onStream(callback: (stream: MediaStream | null) => void): void { // No-op for native scanner callback(null); } }