diff --git a/src/services/QRScanner/CapacitorQRScanner.ts b/src/services/QRScanner/CapacitorQRScanner.ts index 214a3960..0161bd4c 100644 --- a/src/services/QRScanner/CapacitorQRScanner.ts +++ b/src/services/QRScanner/CapacitorQRScanner.ts @@ -11,15 +11,16 @@ export class CapacitorQRScanner implements QRScannerService { private scanListener: ScanListener | null = null; private isScanning = false; private listenerHandles: Array<() => Promise<void>> = []; + private cleanupPromise: Promise<void> | null = null; async checkPermissions(): Promise<boolean> { 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:", wrappedError); + const wrappedError = error instanceof Error ? error : new Error(String(error)); + logger.error("Error checking camera permissions:", { error: wrappedError.message }); return false; } } @@ -28,42 +29,50 @@ export class CapacitorQRScanner implements QRScannerService { try { // First check if we already have permissions if (await this.checkPermissions()) { + logger.debug("Camera permissions already granted"); return true; } - // Request permissions if we don't have them + logger.debug("Requesting camera permissions"); const { camera } = await BarcodeScanner.requestPermissions(); - return camera === "granted"; + 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:", wrappedError); + const wrappedError = error instanceof Error ? error : new Error(String(error)); + logger.error("Error requesting camera permissions:", { error: wrappedError.message }); return false; } } async isSupported(): Promise<boolean> { 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:", wrappedError); + 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<void> { 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 - logger.log("Checking camera permissions..."); if (!(await this.checkPermissions())) { - logger.log("Requesting camera permissions..."); + logger.debug("Requesting camera permissions"); const granted = await this.requestPermissions(); if (!granted) { throw new Error("Camera permission denied"); @@ -71,39 +80,39 @@ export class CapacitorQRScanner implements QRScannerService { } // Check if scanning is supported - logger.log("Checking scanner support..."); if (!(await this.isSupported())) { throw new Error("QR scanning not supported on this device"); } - logger.log("Starting MLKit scanner..."); + logger.info("Starting MLKit scanner"); this.isScanning = true; const scanOptions: StartScanOptions = { formats: [BarcodeFormat.QrCode], - lensFacing: - options?.camera === "front" ? LensFacing.Front : LensFacing.Back, + lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back, }; - logger.log("Scanner options:", scanOptions); + logger.debug("Scanner options:", scanOptions); // Add listener for barcode scans - const handle = await BarcodeScanner.addListener( - "barcodeScanned", - (result) => { - if (this.scanListener) { - this.scanListener.onScan(result.barcode.rawValue); - } - }, - ); + 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:", wrappedError); + 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; } @@ -111,15 +120,20 @@ export class CapacitorQRScanner implements QRScannerService { async stopScan(): Promise<void> { 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:", wrappedError); + 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 { @@ -132,19 +146,44 @@ export class CapacitorQRScanner implements QRScannerService { } async cleanup(): Promise<void> { - try { - await this.stopScan(); - for (const handle of this.listenerHandles) { - await handle(); - } - } catch (error) { - const wrappedError = - error instanceof Error ? error : new Error(String(error)); - logger.error("Error during cleanup:", wrappedError); - throw wrappedError; - } finally { - this.listenerHandles = []; - this.scanListener = null; + // 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; } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 3908b28f..95e3b480 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -20,6 +20,14 @@ function safeStringify(obj: unknown) { } export const logger = { + debug: (message: string, ...args: unknown[]) => { + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.debug(message, ...args); + const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; + logToDb(message + argsString); + } + }, log: (message: string, ...args: unknown[]) => { if ( process.env.NODE_ENV !== "production" || diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 204b749f..ffe2d767 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -160,32 +160,50 @@ export default class ContactQRScanShow extends Vue { private lastScanTime: number = 0; private readonly SCAN_DEBOUNCE_MS = 2000; // Prevent duplicate scans within 2 seconds + // Add cleanup tracking + private isCleaningUp = false; + private isMounted = false; + async created() { - const settings = await retrieveSettingsForActiveAccount(); - this.activeDid = settings.activeDid || ""; - this.apiServer = settings.apiServer || ""; - this.givenName = settings.firstName || ""; - this.hideRegisterPromptOnNewContact = - !!settings.hideRegisterPromptOnNewContact; - this.isRegistered = !!settings.isRegistered; - - const account = await retrieveAccountMetadata(this.activeDid); - if (account) { - const name = - (settings.firstName || "") + - (settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3 - - this.qrValue = await generateEndorserJwtUrlForAccount( - account, - !!settings.isRegistered, - name, - settings.profileImageUrl || "", - false, - ); + try { + const settings = await retrieveSettingsForActiveAccount(); + this.activeDid = settings.activeDid || ""; + this.apiServer = settings.apiServer || ""; + this.givenName = settings.firstName || ""; + this.hideRegisterPromptOnNewContact = !!settings.hideRegisterPromptOnNewContact; + this.isRegistered = !!settings.isRegistered; + + const account = await retrieveAccountMetadata(this.activeDid); + if (account) { + const name = (settings.firstName || "") + (settings.lastName ? ` ${settings.lastName}` : ""); + this.qrValue = await generateEndorserJwtUrlForAccount( + account, + !!settings.isRegistered, + name, + settings.profileImageUrl || "", + false, + ); + } + } catch (error) { + logger.error("Error initializing component:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + this.$notify({ + group: "alert", + type: "danger", + title: "Initialization Error", + text: "Failed to initialize QR scanner. Please try again.", + }); } } async startScanning() { + if (this.isCleaningUp) { + logger.debug("Cannot start scanning during cleanup"); + return; + } + try { this.error = null; this.isScanning = true; @@ -215,7 +233,10 @@ export default class ContactQRScanShow extends Vue { } catch (error) { this.error = error instanceof Error ? error.message : String(error); this.isScanning = false; - logger.error("Error starting scan:", error); + logger.error("Error starting scan:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); } } @@ -223,11 +244,35 @@ export default class ContactQRScanShow extends Vue { try { const scanner = QRScannerFactory.getInstance(); await scanner.stopScan(); + } catch (error) { + logger.error("Error stopping scan:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + } finally { this.isScanning = false; this.lastScannedValue = ""; this.lastScanTime = 0; + } + } + + async cleanupScanner() { + if (this.isCleaningUp) { + return; + } + + this.isCleaningUp = true; + try { + logger.info("Cleaning up QR scanner resources"); + await this.stopScanning(); + await QRScannerFactory.cleanup(); } catch (error) { - logger.error("Error stopping scan:", error); + logger.error("Error during scanner cleanup:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + } finally { + this.isCleaningUp = false; } } @@ -497,10 +542,32 @@ export default class ContactQRScanShow extends Vue { }); } + // Lifecycle hooks + mounted() { + this.isMounted = true; + document.addEventListener("pause", this.handleAppPause); + document.addEventListener("resume", this.handleAppResume); + } + beforeDestroy() { - logger.info("Cleaning up QR scanner resources"); - this.stopScanning(); // Ensure scanner is stopped - QRScannerFactory.cleanup(); + this.isMounted = false; + document.removeEventListener("pause", this.handleAppPause); + document.removeEventListener("resume", this.handleAppResume); + this.cleanupScanner(); + } + + async handleAppPause() { + if (!this.isMounted) return; + + logger.info("App paused, stopping scanner"); + await this.stopScanning(); + } + + handleAppResume() { + if (!this.isMounted) return; + + logger.info("App resumed, scanner can be restarted by user"); + this.isScanning = false; } async addNewContact(contact: Contact) { @@ -581,28 +648,6 @@ export default class ContactQRScanShow extends Vue { ); } } - - // Add pause/resume handlers for mobile - mounted() { - document.addEventListener("pause", this.handleAppPause); - document.addEventListener("resume", this.handleAppResume); - } - - beforeUnmount() { - document.removeEventListener("pause", this.handleAppPause); - document.removeEventListener("resume", this.handleAppResume); - } - - handleAppPause() { - logger.info("App paused, stopping scanner"); - this.stopScanning(); - } - - handleAppResume() { - logger.info("App resumed, scanner can be restarted by user"); - // Don't auto-restart scanning - let user initiate it - this.isScanning = false; - } } </script> @@ -611,3 +656,4 @@ export default class ContactQRScanShow extends Vue { aspect-ratio: 1 / 1; } </style> +