fix(qr): improve QR scanner implementation and error handling
- Implement robust QR scanner factory with platform detection - Add proper camera permissions to Android manifest - Improve error handling and logging across scanner implementations - Add continuous scanning mode for Capacitor/MLKit scanner - Enhance UI feedback during scanning process - Fix build configuration for proper platform detection - Clean up resources properly in scanner components - Add TypeScript improvements and error wrapping The changes include: - Adding CAMERA permission to AndroidManifest.xml - Setting proper build flags (__IS_MOBILE__, __USE_QR_READER__) - Implementing continuous scanning mode for better UX - Adding proper cleanup of scanner resources - Improving error handling and type safety - Enhancing UI with loading states and error messages
This commit is contained in:
@@ -10,13 +10,16 @@ import { logger } from "@/utils/logger";
|
||||
export class CapacitorQRScanner implements QRScannerService {
|
||||
private scanListener: ScanListener | null = null;
|
||||
private isScanning = false;
|
||||
private listenerHandles: Array<() => Promise<void>> = [];
|
||||
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
try {
|
||||
const { camera } = await BarcodeScanner.checkPermissions();
|
||||
return camera === "granted";
|
||||
} catch (error) {
|
||||
logger.error("Error checking camera permissions:", error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error checking camera permissions:", wrappedError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +35,9 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
const { camera } = await BarcodeScanner.requestPermissions();
|
||||
return camera === "granted";
|
||||
} catch (error) {
|
||||
logger.error("Error requesting camera permissions:", error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error requesting camera permissions:", wrappedError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +47,9 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
const { supported } = await BarcodeScanner.isSupported();
|
||||
return supported;
|
||||
} catch (error) {
|
||||
logger.error("Error checking scanner support:", error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error checking scanner support:", wrappedError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -79,17 +86,23 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
};
|
||||
|
||||
logger.log("Scanner options:", scanOptions);
|
||||
const result = await BarcodeScanner.scan(scanOptions);
|
||||
logger.log("Scan result:", result);
|
||||
|
||||
if (result.barcodes.length > 0) {
|
||||
this.scanListener?.onScan(result.barcodes[0].rawValue);
|
||||
}
|
||||
|
||||
// Add listener for barcode scans
|
||||
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) {
|
||||
logger.error("Error during QR scan:", error);
|
||||
this.scanListener?.onError?.(error as Error);
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error during QR scan:", wrappedError);
|
||||
this.scanListener?.onError?.(wrappedError);
|
||||
throw wrappedError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,10 +113,14 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
|
||||
try {
|
||||
await BarcodeScanner.stopScan();
|
||||
this.isScanning = false;
|
||||
} catch (error) {
|
||||
logger.error("Error stopping QR scan:", error);
|
||||
this.scanListener?.onError?.(error as Error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error stopping QR scan:", wrappedError);
|
||||
this.scanListener?.onError?.(wrappedError);
|
||||
throw wrappedError;
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +129,19 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await this.stopScan();
|
||||
this.scanListener = null;
|
||||
try {
|
||||
await this.stopScan();
|
||||
for (const handle of this.listenerHandles) {
|
||||
await handle();
|
||||
}
|
||||
} catch (error) {
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error during cleanup:", wrappedError);
|
||||
throw wrappedError;
|
||||
} finally {
|
||||
this.listenerHandles = [];
|
||||
this.scanListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,15 @@ export class QRScannerFactory {
|
||||
private static instance: QRScannerService | null = null;
|
||||
|
||||
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',
|
||||
VITE_PLATFORM: process.env.VITE_PLATFORM,
|
||||
});
|
||||
|
||||
const capacitorNative = Capacitor.isNativePlatform();
|
||||
const isMobile = __IS_MOBILE__;
|
||||
const isMobile = typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : capacitorNative;
|
||||
const platform = Capacitor.getPlatform();
|
||||
|
||||
logger.log("Platform detection:", {
|
||||
@@ -22,12 +29,16 @@ export class QRScannerFactory {
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
|
||||
// Force native scanner on Android/iOS
|
||||
// Always use native scanner on Android/iOS
|
||||
if (platform === "android" || platform === "ios") {
|
||||
logger.log("Using native scanner due to platform:", platform);
|
||||
return true;
|
||||
}
|
||||
|
||||
return capacitorNative || isMobile;
|
||||
// For other platforms, use native if available
|
||||
const useNative = capacitorNative || isMobile;
|
||||
logger.log("Platform decision:", { useNative, reason: useNative ? "capacitorNative/isMobile" : "web" });
|
||||
return useNative;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,19 +51,24 @@ export class QRScannerFactory {
|
||||
`Creating QR scanner for platform: ${isNative ? "native" : "web"}`,
|
||||
);
|
||||
|
||||
if (isNative) {
|
||||
logger.log("Using native MLKit scanner");
|
||||
this.instance = new CapacitorQRScanner();
|
||||
} else if (__USE_QR_READER__) {
|
||||
logger.log("Using web QR scanner");
|
||||
this.instance = new WebDialogQRScanner();
|
||||
} else {
|
||||
throw new Error(
|
||||
"No QR scanner implementation available for this platform",
|
||||
);
|
||||
try {
|
||||
if (isNative) {
|
||||
logger.log("Using native MLKit scanner");
|
||||
this.instance = new CapacitorQRScanner();
|
||||
} else if (typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : !isNative) {
|
||||
logger.log("Using web QR scanner");
|
||||
this.instance = new WebDialogQRScanner();
|
||||
} else {
|
||||
throw new Error(
|
||||
"No QR scanner implementation available for this platform",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error creating QR scanner:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return this.instance!; // We know it's not null here
|
||||
return this.instance!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +76,13 @@ export class QRScannerFactory {
|
||||
*/
|
||||
static async cleanup(): Promise<void> {
|
||||
if (this.instance) {
|
||||
await this.instance.cleanup();
|
||||
this.instance = null;
|
||||
try {
|
||||
await this.instance.cleanup();
|
||||
} catch (error) {
|
||||
logger.error("Error cleaning up QR scanner:", error);
|
||||
} finally {
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
|
||||
private scanListener: ScanListener | null = null;
|
||||
private isScanning = false;
|
||||
private container: HTMLElement | null = null;
|
||||
|
||||
constructor(private options?: QRScannerOptions) {}
|
||||
|
||||
@@ -18,7 +19,9 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
});
|
||||
return permissions.state === "granted";
|
||||
} catch (error) {
|
||||
logger.error("Error checking camera permissions:", error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error checking camera permissions:", wrappedError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +32,9 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Error requesting camera permissions:", error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error requesting camera permissions:", wrappedError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -47,8 +52,8 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
this.isScanning = true;
|
||||
|
||||
// Create and mount dialog component
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
this.container = document.createElement("div");
|
||||
document.body.appendChild(this.container);
|
||||
|
||||
this.dialogInstance = createApp(QRScannerDialog, {
|
||||
onScan: (result: string) => {
|
||||
@@ -64,16 +69,18 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
options: this.options,
|
||||
});
|
||||
|
||||
this.dialogComponent = this.dialogInstance.mount(container).$refs
|
||||
this.dialogComponent = this.dialogInstance.mount(this.container).$refs
|
||||
.dialog as InstanceType<typeof QRScannerDialog>;
|
||||
} catch (error) {
|
||||
this.isScanning = false;
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
if (this.scanListener?.onError) {
|
||||
this.scanListener.onError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
this.scanListener.onError(wrappedError);
|
||||
}
|
||||
logger.error("Error starting scan:", error);
|
||||
logger.error("Error starting scan:", wrappedError);
|
||||
this.cleanupContainer();
|
||||
throw wrappedError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,9 +96,14 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
if (this.dialogInstance) {
|
||||
this.dialogInstance.unmount();
|
||||
}
|
||||
this.isScanning = false;
|
||||
} catch (error) {
|
||||
logger.error("Error stopping scan:", error);
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error stopping scan:", wrappedError);
|
||||
throw wrappedError;
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
this.cleanupContainer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +111,26 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
this.scanListener = listener;
|
||||
}
|
||||
|
||||
private cleanupContainer(): void {
|
||||
if (this.container && this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await this.stopScan();
|
||||
this.dialogComponent = null;
|
||||
this.dialogInstance = null;
|
||||
this.scanListener = null;
|
||||
try {
|
||||
await this.stopScan();
|
||||
} catch (error) {
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
logger.error("Error during cleanup:", wrappedError);
|
||||
throw wrappedError;
|
||||
} finally {
|
||||
this.dialogComponent = null;
|
||||
this.dialogInstance = null;
|
||||
this.scanListener = null;
|
||||
this.cleanupContainer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user