Browse Source

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
qrcode-capacitor
Matthew Raymer 3 months ago
parent
commit
ff75fa5349
  1. 282
      src/views/ContactQRScanShowView.vue

282
src/views/ContactQRScanShowView.vue

@ -137,7 +137,7 @@ import {
BarcodeScanner, BarcodeScanner,
type ScanResult, type ScanResult,
} from "@capacitor-mlkit/barcode-scanning"; } from "@capacitor-mlkit/barcode-scanning";
import { ref, type Ref, reactive } from "vue"; import { ref, reactive } from "vue";
// Declare global constants // Declare global constants
declare const __USE_QR_READER__: boolean; declare const __USE_QR_READER__: boolean;
@ -241,7 +241,7 @@ export default class ContactQRScanShowView extends Vue {
private isCapturingPhoto = false; private isCapturingPhoto = false;
private appStateListener?: { remove: () => Promise<void> }; private appStateListener?: { remove: () => Promise<void> };
private scanListener: Ref<PluginListenerHandle | null> = ref(null); private scanListener: PluginListenerHandle | null = null;
private state = reactive<AppState>({ private state = reactive<AppState>({
isProcessing: false, isProcessing: false,
processingStatus: "", processingStatus: "",
@ -307,7 +307,17 @@ export default class ContactQRScanShowView extends Vue {
this.appStateListener = await App.addListener( this.appStateListener = await App.addListener(
"appStateChange", "appStateChange",
(state: AppStateChangeEvent) => { (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) { if (!state.isActive) {
this.cleanupCamera(); this.cleanupCamera();
} }
@ -316,15 +326,31 @@ export default class ContactQRScanShowView extends Vue {
// Add pause listener // Add pause listener
await App.addListener("pause", () => { 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(); this.cleanupCamera();
}); });
// Add resume listener // Add resume listener
await App.addListener("resume", () => { await App.addListener("resume", () => {
logger.log("App resumed"); const resumeInfo = {
// Don't automatically reinitialize camera on resume timestamp: new Date().toISOString(),
// Let user explicitly request camera access again 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"); logger.log("App lifecycle listeners setup complete");
@ -340,7 +366,7 @@ export default class ContactQRScanShowView extends Vue {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (settings) { if (settings) {
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
settings.hideRegisterPromptOnNewContact || false; settings.hideRegisterPromptOnNewContact || false;
} }
logger.log("Initial data loaded successfully"); logger.log("Initial data loaded successfully");
@ -436,41 +462,59 @@ export default class ContactQRScanShowView extends Vue {
try { try {
this.state.isProcessing = true; this.state.isProcessing = true;
this.state.processingStatus = "Starting camera..."; this.state.processingStatus = "Starting camera...";
logger.log("Opening mobile camera - starting initialization");
// Check current permission status // Check current permission status
const status = await BarcodeScanner.checkPermissions(); const status = await BarcodeScanner.checkPermissions();
logger.log("Camera permission status:", JSON.stringify(status, null, 2));
if (status.camera !== "granted") { if (status.camera !== "granted") {
// Request permission if not granted // Request permission if not granted
logger.log("Requesting camera permissions...");
const permissionStatus = await BarcodeScanner.requestPermissions(); const permissionStatus = await BarcodeScanner.requestPermissions();
if (permissionStatus.camera !== "granted") { if (permissionStatus.camera !== "granted") {
throw new Error("Camera permission not 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 { try {
const listener = await BarcodeScanner.addListener( if (this.scanListener) {
"barcodesScanned", logger.log("Removing existing barcode listener");
async (result: ScanResult) => { await this.scanListener.remove();
if (result.barcodes && result.barcodes.length > 0) { this.scanListener = null;
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;
} }
} catch (error) { } catch (error) {
logger.error("Error setting up barcode listener:", error); logger.error("Error removing existing listener:", error);
throw new Error("Failed to initialize barcode scanner"); // 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 // Start the scanner
logger.log("Starting barcode scanner");
await BarcodeScanner.startScan(); await BarcodeScanner.startScan();
logger.log("Barcode scanner started successfully");
this.state.isProcessing = false; this.state.isProcessing = false;
this.state.processingStatus = ""; this.state.processingStatus = "";
} catch (error) { } catch (error) {
@ -481,19 +525,37 @@ export default class ContactQRScanShowView extends Vue {
this.showError( this.showError(
error instanceof Error ? error.message : "Failed to open camera", 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) { private async handleScanResult(rawValue: string) {
this.state.isProcessing = true;
this.state.processingStatus = "Processing QR code...";
this.state.processingDetails = `Scanned value: ${rawValue}`;
try { try {
this.state.isProcessing = true;
this.state.processingStatus = "Processing QR code...";
this.state.processingDetails = `Scanned value: ${rawValue}`;
// Stop scanning before processing
await this.stopScanning(); await this.stopScanning();
// Process the scan result
await this.onScanDetect({ rawValue }); await this.onScanDetect({ rawValue });
} catch (error) { } catch (error) {
logger.error("Error handling scan result:", error); logger.error("Error handling scan result:", error);
this.showError("Failed to process scan result"); 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; return;
} }
let newContact: Contact; let newContact: Contact;
try { try {
// Extract JWT from URL // Extract JWT from URL
const jwt = getContactJwtFromJwtUrl(url); const jwt = getContactJwtFromJwtUrl(url);
if (!jwt) { if (!jwt) {
this.danger( this.danger(
"Could not extract contact information from the QR code. Please try again.", "Could not extract contact information from the QR code. Please try again.",
"Invalid QR Code", "Invalid QR Code",
@ -572,11 +634,11 @@ export default class ContactQRScanShowView extends Vue {
this.danger( this.danger(
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.", "The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
"Invalid Data", "Invalid Data",
); );
return; return;
} }
const { payload } = decodeEndorserJwt(jwt); const { payload } = decodeEndorserJwt(jwt);
if (!payload) { if (!payload) {
this.danger( this.danger(
"Could not decode the contact information. Please try again.", "Could not decode the contact information. Please try again.",
@ -594,7 +656,7 @@ export default class ContactQRScanShowView extends Vue {
return; return;
} }
newContact = { newContact = {
did: payload.own?.did || payload.iss, did: payload.own?.did || payload.iss,
name: payload.own?.name, name: payload.own?.name,
nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash, nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
@ -603,15 +665,15 @@ export default class ContactQRScanShowView extends Vue {
registered: payload.own?.registered, registered: payload.own?.registered,
}; };
if (!newContact.did) { if (!newContact.did) {
this.danger( this.danger(
"Missing contact identifier. Please scan a valid TimeSafari contact QR code.", "Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
"Incomplete Contact", "Incomplete Contact",
); );
return; return;
} }
if (!isDid(newContact.did)) { if (!isDid(newContact.did)) {
this.danger( this.danger(
"Invalid contact identifier format. The identifier must begin with 'did:'.", "Invalid contact identifier format. The identifier must begin with 'did:'.",
"Invalid Identifier", "Invalid Identifier",
@ -619,66 +681,66 @@ export default class ContactQRScanShowView extends Vue {
return; return;
} }
await db.open(); await db.open();
await db.contacts.add(newContact); await db.contacts.add(newContact);
let addedMessage; let addedMessage;
if (this.activeDid) { if (this.activeDid) {
await this.setVisibility(newContact, true); await this.setVisibility(newContact, true);
newContact.seesMe = true; newContact.seesMe = true;
addedMessage = "They were added, and your activity is visible to them."; addedMessage = "They were added, and your activity is visible to them.";
} else { } else {
addedMessage = "They were added."; addedMessage = "They were added.";
} }
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Contact Added", title: "Contact Added",
text: addedMessage, text: addedMessage,
}, },
3000, 3000,
); );
if ( if (
this.isRegistered && this.isRegistered &&
!this.hideRegisterPromptOnNewContact && !this.hideRegisterPromptOnNewContact &&
!newContact.registered !newContact.registered
) { ) {
setTimeout(() => { setTimeout(() => {
this.$notify( this.$notify(
{ {
group: "modal", group: "modal",
type: "confirm", type: "confirm",
title: "Register", title: "Register",
text: "Do you want to register them?", text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => { onCancel: async (stopAsking?: boolean) => {
if (stopAsking) { if (stopAsking) {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking, hideRegisterPromptOnNewContact: stopAsking,
}); });
this.hideRegisterPromptOnNewContact = stopAsking; this.hideRegisterPromptOnNewContact = stopAsking;
} }
}, },
onNo: async (stopAsking?: boolean) => { onNo: async (stopAsking?: boolean) => {
if (stopAsking) { if (stopAsking) {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking, hideRegisterPromptOnNewContact: stopAsking,
}); });
this.hideRegisterPromptOnNewContact = stopAsking; this.hideRegisterPromptOnNewContact = stopAsking;
} }
}, },
onYes: async () => { onYes: async () => {
await this.register(newContact); await this.register(newContact);
}, },
promptToStopAsking: true, promptToStopAsking: true,
}, },
-1, -1,
); );
}, 500); }, 500);
} }
} catch (e) { } catch (e) {
logger.error("Error processing QR code:", e); logger.error("Error processing QR code:", e);
this.danger( this.danger(
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.", "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<void> { async onCopyUrlToClipboard(): Promise<void> {
try { try {
await this.platformService.writeToClipboard(this.qrValue); await this.platformService.writeToClipboard(this.qrValue);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "toast", type: "toast",
title: "Copied", title: "Copied",
text: "Contact URL was copied to clipboard.", text: "Contact URL was copied to clipboard.",
}, },
2000, 2000,
); );
} catch (error) { } catch (error) {
logger.error("Error copying to clipboard:", error); logger.error("Error copying to clipboard:", error);
this.danger("Failed to copy to clipboard", "Error"); this.danger("Failed to copy to clipboard", "Error");
@ -813,15 +875,15 @@ export default class ContactQRScanShowView extends Vue {
async onCopyDidToClipboard(): Promise<void> { async onCopyDidToClipboard(): Promise<void> {
try { try {
await this.platformService.writeToClipboard(this.activeDid); await this.platformService.writeToClipboard(this.activeDid);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Copied", title: "Copied",
text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.", text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
}, },
5000, 5000,
); );
} catch (error) { } catch (error) {
logger.error("Error copying to clipboard:", error); logger.error("Error copying to clipboard:", error);
this.danger("Failed to copy to clipboard", "Error"); this.danger("Failed to copy to clipboard", "Error");
@ -880,6 +942,13 @@ export default class ContactQRScanShowView extends Vue {
async stopScanning() { async stopScanning() {
try { try {
// Remove the listener first
if (this.scanListener) {
await this.scanListener.remove();
this.scanListener = null;
}
// Stop the scanner
await BarcodeScanner.stopScan(); await BarcodeScanner.stopScan();
this.state.scannerState.processingStatus = "Scan stopped"; this.state.scannerState.processingStatus = "Scan stopped";
this.state.scannerState.isProcessing = false; this.state.scannerState.isProcessing = false;
@ -889,6 +958,7 @@ export default class ContactQRScanShowView extends Vue {
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
this.state.scannerState.error = `Error stopping scan: ${errorMessage}`; this.state.scannerState.error = `Error stopping scan: ${errorMessage}`;
this.state.scannerState.isProcessing = false; this.state.scannerState.isProcessing = false;
logger.error("Error stopping scanner:", error);
} }
} }

Loading…
Cancel
Save