From 30e448faf84620a67c79beb94d2b23cccbb77dde Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 22 Apr 2025 11:04:56 +0000 Subject: [PATCH] refactor(qr): improve QR code scanning robustness and error handling - Enhance JWT extraction with unified path handling and validation - Add debouncing to prevent duplicate scans - Improve error handling and logging throughout QR flow - Add proper TypeScript interfaces for QR scan results - Implement mobile app lifecycle handlers (pause/resume) - Enhance logging with structured data and consistent levels - Clean up scanner resources properly on component destroy - Split contact handling into separate method for better organization - Add proper type for UserNameDialog ref This commit improves the reliability and maintainability of the QR code scanning functionality while adding better error handling and logging. --- src/components/QRScanner/QRScannerDialog.vue | 12 +- src/libs/crypto/index.ts | 58 +-- src/libs/endorserServer.ts | 6 + src/services/QRScanner/CapacitorQRScanner.ts | 17 +- src/services/QRScanner/QRScannerFactory.ts | 22 +- src/utils/logger.ts | 11 + src/views/ContactQRScanShowView.vue | 350 +++++++++++++------ 7 files changed, 322 insertions(+), 154 deletions(-) diff --git a/src/components/QRScanner/QRScannerDialog.vue b/src/components/QRScanner/QRScannerDialog.vue index 9f2efebe..981f4ca6 100644 --- a/src/components/QRScanner/QRScannerDialog.vue +++ b/src/components/QRScanner/QRScannerDialog.vue @@ -92,9 +92,9 @@ interface ScanProps { }, }) export default class QRScannerDialog extends Vue { - @Prop({ type: Function, required: true }) onScan!: ScanProps['onScan']; - @Prop({ type: Function }) onError?: ScanProps['onError']; - @Prop({ type: Object }) options?: ScanProps['options']; + @Prop({ type: Function, required: true }) onScan!: ScanProps["onScan"]; + @Prop({ type: Function }) onError?: ScanProps["onError"]; + @Prop({ type: Object }) options?: ScanProps["options"]; visible = true; error: string | null = null; @@ -132,7 +132,8 @@ export default class QRScannerDialog extends Vue { await promise; this.error = null; } catch (error) { - const wrappedError = error instanceof Error ? error : new Error(String(error)); + const wrappedError = + error instanceof Error ? error : new Error(String(error)); this.error = wrappedError.message; if (this.onError) { this.onError(wrappedError); @@ -146,7 +147,8 @@ export default class QRScannerDialog extends Vue { this.onScan(result); this.close(); } catch (error) { - const wrappedError = error instanceof Error ? error : new Error(String(error)); + const wrappedError = + error instanceof Error ? error : new Error(String(error)); this.error = wrappedError.message; if (this.onError) { this.onError(wrappedError); diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index f9770cff..378cab01 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -9,6 +9,7 @@ import { createEndorserJwtForDid, CONTACT_URL_PATH_ENDORSER_CH_OLD, CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, + CONTACT_CONFIRM_URL_PATH_TIME_SAFARI, } from "../../libs/endorserServer"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { logger } from "../../utils/logger"; @@ -104,34 +105,41 @@ export const accessToken = async (did?: string) => { }; /** - @return payload of JWT pulled out of any recognized URL path (if any) + * Extract JWT from various URL formats + * @param jwtUrlText The URL containing the JWT + * @returns The extracted JWT or null if not found */ export const getContactJwtFromJwtUrl = (jwtUrlText: string) => { - let jwtText = jwtUrlText; - const appImportConfirmUrlLoc = jwtText.indexOf( - CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, - ); - if (appImportConfirmUrlLoc > -1) { - jwtText = jwtText.substring( - appImportConfirmUrlLoc + - CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI.length, - ); - } - const appImportOneUrlLoc = jwtText.indexOf( - CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, - ); - if (appImportOneUrlLoc > -1) { - jwtText = jwtText.substring( - appImportOneUrlLoc + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI.length, - ); - } - const endorserUrlPathLoc = jwtText.indexOf(CONTACT_URL_PATH_ENDORSER_CH_OLD); - if (endorserUrlPathLoc > -1) { - jwtText = jwtText.substring( - endorserUrlPathLoc + CONTACT_URL_PATH_ENDORSER_CH_OLD.length, - ); + try { + let jwtText = jwtUrlText; + + // Try to extract JWT from URL paths + const paths = [ + CONTACT_CONFIRM_URL_PATH_TIME_SAFARI, + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, + CONTACT_URL_PATH_ENDORSER_CH_OLD, + ]; + + for (const path of paths) { + const pathIndex = jwtText.indexOf(path); + if (pathIndex > -1) { + jwtText = jwtText.substring(pathIndex + path.length); + break; + } + } + + // Validate JWT format + if (!jwtText.match(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/)) { + logger.error("Invalid JWT format in URL:", jwtUrlText); + return null; + } + + return jwtText; + } catch (error) { + logger.error("Error extracting JWT from URL:", error); + return null; } - return jwtText; }; export const nextDerivationPath = (origDerivPath: string) => { diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 15497b65..35c6bb24 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -86,6 +86,12 @@ export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt="; */ export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt="; +/** + * URL path suffix for contact confirmation + * @constant {string} + */ +export const CONTACT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact/confirm/"; + /** * The prefix for handle IDs, the permanent ID for claims on Endorser * @constant {string} diff --git a/src/services/QRScanner/CapacitorQRScanner.ts b/src/services/QRScanner/CapacitorQRScanner.ts index 4edbd1de..214a3960 100644 --- a/src/services/QRScanner/CapacitorQRScanner.ts +++ b/src/services/QRScanner/CapacitorQRScanner.ts @@ -86,15 +86,18 @@ export class CapacitorQRScanner implements QRScannerService { }; logger.log("Scanner options:", scanOptions); - + // Add listener for barcode scans - const handle = await BarcodeScanner.addListener('barcodeScanned', (result) => { - if (this.scanListener) { - this.scanListener.onScan(result.barcode.rawValue); - } - }); + const handle = await BarcodeScanner.addListener( + "barcodeScanned", + (result) => { + if (this.scanListener) { + this.scanListener.onScan(result.barcode.rawValue); + } + }, + ); this.listenerHandles.push(handle.remove); - + // Start continuous scanning await BarcodeScanner.startScan(scanOptions); } catch (error) { diff --git a/src/services/QRScanner/QRScannerFactory.ts b/src/services/QRScanner/QRScannerFactory.ts index 216eda91..2edc7602 100644 --- a/src/services/QRScanner/QRScannerFactory.ts +++ b/src/services/QRScanner/QRScannerFactory.ts @@ -13,13 +13,18 @@ export class QRScannerFactory { private static isNativePlatform(): boolean { // Debug logging for build flags logger.log("Build flags:", { - IS_MOBILE: typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : 'undefined', - USE_QR_READER: typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : 'undefined', + IS_MOBILE: + typeof __IS_MOBILE__ !== "undefined" ? __IS_MOBILE__ : "undefined", + USE_QR_READER: + typeof __USE_QR_READER__ !== "undefined" + ? __USE_QR_READER__ + : "undefined", VITE_PLATFORM: process.env.VITE_PLATFORM, }); const capacitorNative = Capacitor.isNativePlatform(); - const isMobile = typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : capacitorNative; + const isMobile = + typeof __IS_MOBILE__ !== "undefined" ? __IS_MOBILE__ : capacitorNative; const platform = Capacitor.getPlatform(); logger.log("Platform detection:", { @@ -37,7 +42,10 @@ export class QRScannerFactory { // For other platforms, use native if available const useNative = capacitorNative || isMobile; - logger.log("Platform decision:", { useNative, reason: useNative ? "capacitorNative/isMobile" : "web" }); + logger.log("Platform decision:", { + useNative, + reason: useNative ? "capacitorNative/isMobile" : "web", + }); return useNative; } @@ -55,7 +63,11 @@ export class QRScannerFactory { if (isNative) { logger.log("Using native MLKit scanner"); this.instance = new CapacitorQRScanner(); - } else if (typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : !isNative) { + } else if ( + typeof __USE_QR_READER__ !== "undefined" + ? __USE_QR_READER__ + : !isNative + ) { logger.log("Using web QR scanner"); this.instance = new WebDialogQRScanner(); } else { diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 86389c47..3908b28f 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -31,6 +31,17 @@ export const logger = { logToDb(message + argsString); } }, + info: (message: string, ...args: unknown[]) => { + if ( + process.env.NODE_ENV !== "production" || + process.env.VITE_PLATFORM === "capacitor" + ) { + // eslint-disable-next-line no-console + console.info(message, ...args); + const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; + logToDb(message + argsString); + } + }, warn: (message: string, ...args: unknown[]) => { if ( process.env.NODE_ENV !== "production" || diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 01720407..204b749f 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -78,10 +78,12 @@

Scan Contact Info

-
+
-