Files
crowd-funder-for-time-pwa/src/services/QRScanner/CapacitorQRScanner.ts
Matthew Raymer 67cf77a6fb 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.
2025-04-22 11:26:27 +00:00

190 lines
5.8 KiB
TypeScript

import {
BarcodeScanner,
BarcodeFormat,
StartScanOptions,
LensFacing,
} from "@capacitor-mlkit/barcode-scanning";
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
import { logger } from "@/utils/logger";
export class CapacitorQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
private listenerHandles: Array<() => Promise<void>> = [];
private cleanupPromise: Promise<void> | null = null;
async checkPermissions(): Promise<boolean> {
try {
logger.debug("Checking camera permissions");
const { camera } = await BarcodeScanner.checkPermissions();
return camera === "granted";
} catch (error) {
const wrappedError = error instanceof Error ? error : new Error(String(error));
logger.error("Error checking camera permissions:", { error: wrappedError.message });
return false;
}
}
async requestPermissions(): Promise<boolean> {
try {
// First check if we already have permissions
if (await this.checkPermissions()) {
logger.debug("Camera permissions already granted");
return true;
}
logger.debug("Requesting camera permissions");
const { camera } = await BarcodeScanner.requestPermissions();
const granted = camera === "granted";
logger.debug(`Camera permissions ${granted ? "granted" : "denied"}`);
return granted;
} catch (error) {
const wrappedError = error instanceof Error ? error : new Error(String(error));
logger.error("Error requesting camera permissions:", { error: wrappedError.message });
return false;
}
}
async isSupported(): Promise<boolean> {
try {
logger.debug("Checking scanner support");
const { supported } = await BarcodeScanner.isSupported();
logger.debug(`Scanner support: ${supported}`);
return supported;
} catch (error) {
const wrappedError = error instanceof Error ? error : new Error(String(error));
logger.error("Error checking scanner support:", { error: wrappedError.message });
return false;
}
}
async startScan(options?: QRScannerOptions): Promise<void> {
if (this.isScanning) {
logger.debug("Scanner already running");
return;
}
if (this.cleanupPromise) {
logger.debug("Waiting for previous cleanup to complete");
await this.cleanupPromise;
}
try {
// Ensure we have permissions before starting
if (!(await this.checkPermissions())) {
logger.debug("Requesting camera permissions");
const granted = await this.requestPermissions();
if (!granted) {
throw new Error("Camera permission denied");
}
}
// Check if scanning is supported
if (!(await this.isSupported())) {
throw new Error("QR scanning not supported on this device");
}
logger.info("Starting MLKit scanner");
this.isScanning = true;
const scanOptions: StartScanOptions = {
formats: [BarcodeFormat.QrCode],
lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back,
};
logger.debug("Scanner options:", scanOptions);
// Add listener for barcode scans
const handle = await BarcodeScanner.addListener("barcodeScanned", (result) => {
if (this.scanListener && result.barcode?.rawValue) {
this.scanListener.onScan(result.barcode.rawValue);
}
});
this.listenerHandles.push(handle.remove);
// Start continuous scanning
await BarcodeScanner.startScan(scanOptions);
logger.info("MLKit scanner started successfully");
} catch (error) {
const wrappedError = error instanceof Error ? error : new Error(String(error));
logger.error("Error during QR scan:", {
error: wrappedError.message,
stack: wrappedError.stack
});
this.isScanning = false;
await this.cleanup();
this.scanListener?.onError?.(wrappedError);
throw wrappedError;
}
}
async stopScan(): Promise<void> {
if (!this.isScanning) {
logger.debug("Scanner not running");
return;
}
try {
logger.debug("Stopping QR scanner");
await BarcodeScanner.stopScan();
logger.info("QR scanner stopped successfully");
} catch (error) {
const wrappedError = error instanceof Error ? error : new Error(String(error));
logger.error("Error stopping QR scan:", {
error: wrappedError.message,
stack: wrappedError.stack
});
this.scanListener?.onError?.(wrappedError);
throw wrappedError;
} finally {
this.isScanning = false;
}
}
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
async cleanup(): Promise<void> {
// Prevent multiple simultaneous cleanup attempts
if (this.cleanupPromise) {
return this.cleanupPromise;
}
this.cleanupPromise = (async () => {
try {
logger.debug("Starting QR scanner cleanup");
// Stop scanning if active
if (this.isScanning) {
await this.stopScan();
}
// Remove all listeners
for (const handle of this.listenerHandles) {
try {
await handle();
} catch (error) {
logger.warn("Error removing listener:", error);
}
}
logger.info("QR scanner cleanup completed");
} catch (error) {
const wrappedError = error instanceof Error ? error : new Error(String(error));
logger.error("Error during cleanup:", {
error: wrappedError.message,
stack: wrappedError.stack
});
throw wrappedError;
} finally {
this.listenerHandles = [];
this.scanListener = null;
this.cleanupPromise = null;
}
})();
return this.cleanupPromise;
}
}