From ff75fa5349d521355d94d695ccae06c4b190caa5 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 16 Apr 2025 08:05:04 +0000 Subject: [PATCH] refactor(scanner): improve barcode scanner initialization and error handling - Fix scanner listener initialization in class component * Replace Vue ref with direct class property for scanListener * Remove unnecessary .value accesses throughout the code - Enhance error handling and cleanup * Add proper cleanup on scanner errors * Add cleanup in stopScanning method * Improve error message formatting * Add error handling in handleScanResult - Improve state management * Move state updates into try/catch blocks * Add finally block to reset state after scan processing * Better handling of processing states during scanning - Add comprehensive logging * Log each step of scanner initialization * Add detailed error logging * Format object logs to prevent [object Object] output - Code style improvements * Fix indentation throughout the file * Consistent error handling patterns * Better code organization and readability --- src/views/ContactQRScanShowView.vue | 282 +++++++++++++++++----------- 1 file changed, 176 insertions(+), 106 deletions(-) diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 07f40ff0..2152d9d7 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -137,7 +137,7 @@ import { BarcodeScanner, type ScanResult, } from "@capacitor-mlkit/barcode-scanning"; -import { ref, type Ref, reactive } from "vue"; +import { ref, reactive } from "vue"; // Declare global constants declare const __USE_QR_READER__: boolean; @@ -241,7 +241,7 @@ export default class ContactQRScanShowView extends Vue { private isCapturingPhoto = false; private appStateListener?: { remove: () => Promise }; - private scanListener: Ref = ref(null); + private scanListener: PluginListenerHandle | null = null; private state = reactive({ isProcessing: false, processingStatus: "", @@ -307,7 +307,17 @@ export default class ContactQRScanShowView extends Vue { this.appStateListener = await App.addListener( "appStateChange", (state: AppStateChangeEvent) => { - logger.log("App state changed:", state); + const stateInfo = { + isActive: state.isActive, + timestamp: new Date().toISOString(), + cameraActive: this.cameraActive, + scannerState: { + ...this.state.scannerState, + // Convert complex objects to strings to avoid [object Object] + error: this.state.scannerState.error?.toString() || null, + }, + }; + logger.log("App state changed:", JSON.stringify(stateInfo, null, 2)); if (!state.isActive) { this.cleanupCamera(); } @@ -316,15 +326,31 @@ export default class ContactQRScanShowView extends Vue { // Add pause listener await App.addListener("pause", () => { - logger.log("App paused"); + const pauseInfo = { + timestamp: new Date().toISOString(), + cameraActive: this.cameraActive, + scannerState: { + ...this.state.scannerState, + error: this.state.scannerState.error?.toString() || null, + }, + isProcessing: this.state.isProcessing, + }; + logger.log("App paused:", JSON.stringify(pauseInfo, null, 2)); this.cleanupCamera(); }); // Add resume listener await App.addListener("resume", () => { - logger.log("App resumed"); - // Don't automatically reinitialize camera on resume - // Let user explicitly request camera access again + const resumeInfo = { + timestamp: new Date().toISOString(), + cameraActive: this.cameraActive, + scannerState: { + ...this.state.scannerState, + error: this.state.scannerState.error?.toString() || null, + }, + isProcessing: this.state.isProcessing, + }; + logger.log("App resumed:", JSON.stringify(resumeInfo, null, 2)); }); logger.log("App lifecycle listeners setup complete"); @@ -340,7 +366,7 @@ export default class ContactQRScanShowView extends Vue { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); if (settings) { - this.hideRegisterPromptOnNewContact = + this.hideRegisterPromptOnNewContact = settings.hideRegisterPromptOnNewContact || false; } logger.log("Initial data loaded successfully"); @@ -436,41 +462,59 @@ export default class ContactQRScanShowView extends Vue { try { this.state.isProcessing = true; this.state.processingStatus = "Starting camera..."; + logger.log("Opening mobile camera - starting initialization"); // Check current permission status const status = await BarcodeScanner.checkPermissions(); + logger.log("Camera permission status:", JSON.stringify(status, null, 2)); if (status.camera !== "granted") { // Request permission if not granted + logger.log("Requesting camera permissions..."); const permissionStatus = await BarcodeScanner.requestPermissions(); if (permissionStatus.camera !== "granted") { throw new Error("Camera permission not granted"); } + logger.log( + "Camera permission granted:", + JSON.stringify(permissionStatus, null, 2), + ); } - // Set up the listener before starting the scan + // Remove any existing listener first try { - const listener = await BarcodeScanner.addListener( - "barcodesScanned", - async (result: ScanResult) => { - if (result.barcodes && result.barcodes.length > 0) { - this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`; - await this.handleScanResult(result.barcodes[0].rawValue); - } - }, - ); - - // Only set the listener if we successfully got one - if (listener) { - this.scanListener.value = listener; + if (this.scanListener) { + logger.log("Removing existing barcode listener"); + await this.scanListener.remove(); + this.scanListener = null; } } catch (error) { - logger.error("Error setting up barcode listener:", error); - throw new Error("Failed to initialize barcode scanner"); + logger.error("Error removing existing listener:", error); + // Continue with setup even if removal fails } + // Set up the listener before starting the scan + logger.log("Setting up new barcode listener"); + this.scanListener = await BarcodeScanner.addListener( + "barcodesScanned", + async (result: ScanResult) => { + logger.log( + "Barcode scan result received:", + JSON.stringify(result, null, 2), + ); + if (result.barcodes && result.barcodes.length > 0) { + this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`; + await this.handleScanResult(result.barcodes[0].rawValue); + } + }, + ); + logger.log("Barcode listener setup complete"); + // Start the scanner + logger.log("Starting barcode scanner"); await BarcodeScanner.startScan(); + logger.log("Barcode scanner started successfully"); + this.state.isProcessing = false; this.state.processingStatus = ""; } catch (error) { @@ -481,19 +525,37 @@ export default class ContactQRScanShowView extends Vue { this.showError( error instanceof Error ? error.message : "Failed to open camera", ); + + // Cleanup on error + try { + if (this.scanListener) { + await this.scanListener.remove(); + this.scanListener = null; + } + } catch (cleanupError) { + logger.error("Error during cleanup:", cleanupError); + } } } private async handleScanResult(rawValue: string) { - this.state.isProcessing = true; - this.state.processingStatus = "Processing QR code..."; - this.state.processingDetails = `Scanned value: ${rawValue}`; try { + this.state.isProcessing = true; + this.state.processingStatus = "Processing QR code..."; + this.state.processingDetails = `Scanned value: ${rawValue}`; + + // Stop scanning before processing await this.stopScanning(); + + // Process the scan result await this.onScanDetect({ rawValue }); } catch (error) { logger.error("Error handling scan result:", error); this.showError("Failed to process scan result"); + } finally { + this.state.isProcessing = false; + this.state.processingStatus = ""; + this.state.processingDetails = ""; } } @@ -553,11 +615,11 @@ export default class ContactQRScanShowView extends Vue { return; } - let newContact: Contact; - try { + let newContact: Contact; + try { // Extract JWT from URL - const jwt = getContactJwtFromJwtUrl(url); - if (!jwt) { + const jwt = getContactJwtFromJwtUrl(url); + if (!jwt) { this.danger( "Could not extract contact information from the QR code. Please try again.", "Invalid QR Code", @@ -572,11 +634,11 @@ export default class ContactQRScanShowView extends Vue { this.danger( "The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.", "Invalid Data", - ); - return; - } + ); + return; + } - const { payload } = decodeEndorserJwt(jwt); + const { payload } = decodeEndorserJwt(jwt); if (!payload) { this.danger( "Could not decode the contact information. Please try again.", @@ -594,7 +656,7 @@ export default class ContactQRScanShowView extends Vue { return; } - newContact = { + newContact = { did: payload.own?.did || payload.iss, name: payload.own?.name, nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash, @@ -603,15 +665,15 @@ export default class ContactQRScanShowView extends Vue { registered: payload.own?.registered, }; - if (!newContact.did) { + if (!newContact.did) { this.danger( "Missing contact identifier. Please scan a valid TimeSafari contact QR code.", "Incomplete Contact", ); - return; - } + return; + } - if (!isDid(newContact.did)) { + if (!isDid(newContact.did)) { this.danger( "Invalid contact identifier format. The identifier must begin with 'did:'.", "Invalid Identifier", @@ -619,66 +681,66 @@ export default class ContactQRScanShowView extends Vue { return; } - await db.open(); - await db.contacts.add(newContact); + await db.open(); + await db.contacts.add(newContact); - let addedMessage; - if (this.activeDid) { - await this.setVisibility(newContact, true); + let addedMessage; + if (this.activeDid) { + await this.setVisibility(newContact, true); newContact.seesMe = true; addedMessage = "They were added, and your activity is visible to them."; - } else { - addedMessage = "They were added."; - } + } else { + addedMessage = "They were added."; + } - this.$notify( - { - group: "alert", - type: "success", - title: "Contact Added", - text: addedMessage, - }, - 3000, - ); + this.$notify( + { + group: "alert", + type: "success", + title: "Contact Added", + text: addedMessage, + }, + 3000, + ); if ( this.isRegistered && !this.hideRegisterPromptOnNewContact && !newContact.registered ) { - setTimeout(() => { - this.$notify( - { - group: "modal", - type: "confirm", - title: "Register", - text: "Do you want to register them?", + setTimeout(() => { + this.$notify( + { + group: "modal", + type: "confirm", + title: "Register", + text: "Do you want to register them?", onCancel: async (stopAsking?: boolean) => { - if (stopAsking) { - await db.settings.update(MASTER_SETTINGS_KEY, { - hideRegisterPromptOnNewContact: stopAsking, - }); - this.hideRegisterPromptOnNewContact = stopAsking; - } - }, + if (stopAsking) { + await db.settings.update(MASTER_SETTINGS_KEY, { + hideRegisterPromptOnNewContact: stopAsking, + }); + this.hideRegisterPromptOnNewContact = stopAsking; + } + }, onNo: async (stopAsking?: boolean) => { - if (stopAsking) { - await db.settings.update(MASTER_SETTINGS_KEY, { - hideRegisterPromptOnNewContact: stopAsking, - }); - this.hideRegisterPromptOnNewContact = stopAsking; - } - }, - onYes: async () => { - await this.register(newContact); - }, - promptToStopAsking: true, - }, - -1, - ); - }, 500); - } - } catch (e) { + if (stopAsking) { + await db.settings.update(MASTER_SETTINGS_KEY, { + hideRegisterPromptOnNewContact: stopAsking, + }); + this.hideRegisterPromptOnNewContact = stopAsking; + } + }, + onYes: async () => { + await this.register(newContact); + }, + promptToStopAsking: true, + }, + -1, + ); + }, 500); + } + } catch (e) { logger.error("Error processing QR code:", e); this.danger( "Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.", @@ -795,15 +857,15 @@ export default class ContactQRScanShowView extends Vue { async onCopyUrlToClipboard(): Promise { try { await this.platformService.writeToClipboard(this.qrValue); - this.$notify( - { - group: "alert", - type: "toast", - title: "Copied", - text: "Contact URL was copied to clipboard.", - }, - 2000, - ); + this.$notify( + { + group: "alert", + type: "toast", + title: "Copied", + text: "Contact URL was copied to clipboard.", + }, + 2000, + ); } catch (error) { logger.error("Error copying to clipboard:", error); this.danger("Failed to copy to clipboard", "Error"); @@ -813,15 +875,15 @@ export default class ContactQRScanShowView extends Vue { async onCopyDidToClipboard(): Promise { try { await this.platformService.writeToClipboard(this.activeDid); - this.$notify( - { - group: "alert", - type: "info", - title: "Copied", - text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.", - }, - 5000, - ); + this.$notify( + { + group: "alert", + type: "info", + title: "Copied", + text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.", + }, + 5000, + ); } catch (error) { logger.error("Error copying to clipboard:", error); this.danger("Failed to copy to clipboard", "Error"); @@ -880,6 +942,13 @@ export default class ContactQRScanShowView extends Vue { async stopScanning() { try { + // Remove the listener first + if (this.scanListener) { + await this.scanListener.remove(); + this.scanListener = null; + } + + // Stop the scanner await BarcodeScanner.stopScan(); this.state.scannerState.processingStatus = "Scan stopped"; this.state.scannerState.isProcessing = false; @@ -889,6 +958,7 @@ export default class ContactQRScanShowView extends Vue { error instanceof Error ? error.message : String(error); this.state.scannerState.error = `Error stopping scan: ${errorMessage}`; this.state.scannerState.isProcessing = false; + logger.error("Error stopping scanner:", error); } }