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,
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<void> };
private scanListener: Ref<PluginListenerHandle | null> = ref(null);
private scanListener: PluginListenerHandle | null = null;
private state = reactive<AppState>({
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<void> {
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<void> {
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);
}
}

Loading…
Cancel
Save