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
This commit is contained in:
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any existing listener first
|
||||||
|
try {
|
||||||
|
if (this.scanListener) {
|
||||||
|
logger.log("Removing existing barcode listener");
|
||||||
|
await this.scanListener.remove();
|
||||||
|
this.scanListener = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error removing existing listener:", error);
|
||||||
|
// Continue with setup even if removal fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the listener before starting the scan
|
// Set up the listener before starting the scan
|
||||||
try {
|
logger.log("Setting up new barcode listener");
|
||||||
const listener = await BarcodeScanner.addListener(
|
this.scanListener = await BarcodeScanner.addListener(
|
||||||
"barcodesScanned",
|
"barcodesScanned",
|
||||||
async (result: ScanResult) => {
|
async (result: ScanResult) => {
|
||||||
if (result.barcodes && result.barcodes.length > 0) {
|
logger.log(
|
||||||
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
|
"Barcode scan result received:",
|
||||||
await this.handleScanResult(result.barcodes[0].rawValue);
|
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);
|
||||||
// Only set the listener if we successfully got one
|
}
|
||||||
if (listener) {
|
},
|
||||||
this.scanListener.value = listener;
|
);
|
||||||
}
|
logger.log("Barcode listener setup complete");
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error setting up barcode listener:", error);
|
|
||||||
throw new Error("Failed to initialize barcode scanner");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user