import { BarcodeScanner, BarcodeFormat, LensFacing, ScanResult, } from "@capacitor-mlkit/barcode-scanning"; import type { QRScannerService, ScanListener } from "./types"; import { logger } from "../../utils/logger"; export class CapacitorQRScanner implements QRScannerService { private scanListener: ScanListener | null = null; private isScanning = false; private listenerHandles: Array<() => Promise> = []; async checkPermissions() { try { const { camera } = await BarcodeScanner.checkPermissions(); return camera === "granted"; } catch (error) { logger.error("Error checking camera permissions:", error); return false; } } async requestPermissions() { try { const { camera } = await BarcodeScanner.requestPermissions(); return camera === "granted"; } catch (error) { logger.error("Error requesting camera permissions:", error); return false; } } async isSupported() { try { const { supported } = await BarcodeScanner.isSupported(); return supported; } catch (error) { logger.error("Error checking barcode scanner support:", error); return false; } } async startScan() { if (this.isScanning) { logger.warn("Scanner is already active"); return; } try { // First register listeners before starting scan await this.registerListeners(); this.isScanning = true; await BarcodeScanner.startScan({ formats: [BarcodeFormat.QrCode], lensFacing: LensFacing.Back, }); } catch (error) { // Ensure cleanup on error this.isScanning = false; await this.removeListeners(); logger.error("Error starting barcode scan:", error); throw error; } } async stopScan() { if (!this.isScanning) { return; } try { // First stop the scan await BarcodeScanner.stopScan(); } catch (error) { logger.error("Error stopping barcode scan:", error); } finally { // Always cleanup state even if stop fails this.isScanning = false; await this.removeListeners(); } } private async registerListeners() { // Clear any existing listeners first await this.removeListeners(); const scanHandle = await BarcodeScanner.addListener( "barcodesScanned", (result: ScanResult) => { if (result.barcodes.length > 0) { const barcode = result.barcodes[0]; if (barcode.rawValue && this.scanListener) { this.scanListener.onScan(barcode.rawValue); } } }, ); const errorHandle = await BarcodeScanner.addListener( "scanError", (error) => { logger.error("Scan error:", error); if (this.scanListener?.onError) { this.scanListener.onError( new Error(error.message || "Unknown scan error"), ); } }, ); this.listenerHandles.push( async () => await scanHandle.remove(), async () => await errorHandle.remove(), ); } private async removeListeners() { try { // Remove all registered listener handles await Promise.all(this.listenerHandles.map((handle) => handle())); this.listenerHandles = []; } catch (error) { logger.error("Error removing listeners:", error); } } addListener(listener: ScanListener) { if (this.scanListener) { logger.warn("Scanner listener already exists, removing old listener"); this.cleanup(); } this.scanListener = listener; } async cleanup() { try { // Stop scan first if active if (this.isScanning) { await this.stopScan(); } // Remove listeners await this.removeListeners(); // Clear state this.scanListener = null; this.isScanning = false; } catch (error) { logger.error("Error during cleanup:", error); } } }