refactor(qr): improve QR scanner robustness and lifecycle management

- Add cleanup promise to prevent concurrent cleanup operations
- Add proper component lifecycle tracking with isMounted flag
- Add isCleaningUp flag to prevent operations during cleanup
- Add debug level logging for better diagnostics
- Add structured error logging with stack traces
- Add proper error handling in component initialization
- Add proper cleanup of event listeners and camera resources
- Add proper handling of app pause/resume events
- Add proper error boundaries around camera operations
- Improve error message formatting and consistency

The QR scanner now properly handles lifecycle events, cleans up resources,
and provides better error diagnostics. This improves reliability on mobile
devices and prevents potential memory leaks.
This commit is contained in:
Matthew Raymer
2025-04-22 11:26:27 +00:00
parent a4d184b1c6
commit 67cf77a6fb
3 changed files with 185 additions and 92 deletions

View File

@@ -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;
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}` : ""); // lastName is deprecated, pre v 0.1.3
this.qrValue = await generateEndorserJwtUrlForAccount(
account,
!!settings.isRegistered,
name,
settings.profileImageUrl || "",
false,
);
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>