--- description: globs: alwaysApply: true --- # QR Code Implementation Guide ## Directory Structure ``` src/ ├── services/ │ └── QRScanner/ │ ├── types.ts # Core interfaces and types │ ├── QRScannerFactory.ts # Factory for creating scanner instances │ ├── CapacitorQRScanner.ts # Mobile implementation using MLKit │ ├── WebInlineQRScanner.ts # Web implementation using MediaDevices API │ └── interfaces.ts # Additional interfaces ├── components/ │ └── QRScanner/ │ └── QRScannerDialog.vue # Shared UI component ``` ## Core Interfaces ```typescript // types.ts export interface ScanListener { onScan: (result: string) => void; onError?: (error: Error) => void; } export interface QRScannerOptions { camera?: "front" | "back"; showPreview?: boolean; playSound?: boolean; } export interface QRScannerService { checkPermissions(): Promise; requestPermissions(): Promise; isSupported(): Promise; startScan(options?: QRScannerOptions): Promise; stopScan(): Promise; addListener(listener: ScanListener): void; onStream(callback: (stream: MediaStream | null) => void): void; cleanup(): Promise; } ``` ## Configuration Files ### Vite Configuration ```typescript // vite.config.common.mts export function createBuildConfig(mode: string) { return { define: { 'process.env.VITE_PLATFORM': JSON.stringify(mode), 'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative), __IS_MOBILE__: JSON.stringify(isCapacitor), __USE_QR_READER__: JSON.stringify(!isCapacitor) } }; } ``` ### Capacitor Configuration ```typescript // capacitor.config.ts const config: CapacitorConfig = { plugins: { MLKitBarcodeScanner: { formats: ['QR_CODE'], detectorSize: 1.0, lensFacing: 'back', googleBarcodeScannerModuleInstallState: true } } }; ``` ## Implementation Steps 1. **Install Dependencies** ```bash npm install @capacitor-mlkit/barcode-scanning ``` 2. **Create Core Types** Create the interface files as shown above. 3. **Implement Factory** ```typescript // QRScannerFactory.ts export class QRScannerFactory { private static instance: QRScannerService | null = null; private static isNativePlatform(): boolean { const capacitorNative = Capacitor.isNativePlatform(); const isMobile = typeof __IS_MOBILE__ !== "undefined" ? __IS_MOBILE__ : capacitorNative; const platform = Capacitor.getPlatform(); // Always use native scanner on Android/iOS if (platform === "android" || platform === "ios") { return true; } // For other platforms, use native if available return capacitorNative || isMobile; } static getInstance(): QRScannerService { if (!this.instance) { const isNative = this.isNativePlatform(); if (isNative) { this.instance = new CapacitorQRScanner(); } else { this.instance = new WebInlineQRScanner(); } } return this.instance!; } static async cleanup(): Promise { if (this.instance) { await this.instance.cleanup(); this.instance = null; } } } ``` 4. **Implement Mobile Scanner** ```typescript // CapacitorQRScanner.ts export class CapacitorQRScanner implements QRScannerService { private scanListener: ScanListener | null = null; private isScanning = false; private listenerHandles: Array<() => Promise> = []; private cleanupPromise: Promise | null = null; async checkPermissions(): Promise { try { const { camera } = await BarcodeScanner.checkPermissions(); return camera === "granted"; } catch (error) { logger.error("Error checking camera permissions:", error); return false; } } async requestPermissions(): Promise { try { if (await this.checkPermissions()) { return true; } const { camera } = await BarcodeScanner.requestPermissions(); return camera === "granted"; } catch (error) { logger.error("Error requesting camera permissions:", error); return false; } } async isSupported(): Promise { try { const { supported } = await BarcodeScanner.isSupported(); return supported; } catch (error) { logger.error("Error checking scanner support:", error); return false; } } async startScan(options?: QRScannerOptions): Promise { if (this.isScanning) return; if (this.cleanupPromise) { await this.cleanupPromise; } try { if (!(await this.checkPermissions())) { const granted = await this.requestPermissions(); if (!granted) { throw new Error("Camera permission denied"); } } if (!(await this.isSupported())) { throw new Error("QR scanning not supported on this device"); } this.isScanning = true; const scanOptions: StartScanOptions = { formats: [BarcodeFormat.QrCode], lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back, }; const handle = await BarcodeScanner.addListener("barcodeScanned", (result) => { if (this.scanListener && result.barcode?.rawValue) { this.scanListener.onScan(result.barcode.rawValue); } }); this.listenerHandles.push(handle.remove); await BarcodeScanner.startScan(scanOptions); } catch (error) { this.isScanning = false; await this.cleanup(); this.scanListener?.onError?.(error instanceof Error ? error : new Error(String(error))); throw error; } } async stopScan(): Promise { if (!this.isScanning) return; this.isScanning = false; try { await BarcodeScanner.stopScan(); } catch (error) { logger.error("Error stopping scan:", error); throw error; } } addListener(listener: ScanListener): void { this.scanListener = listener; } onStream(callback: (stream: MediaStream | null) => void): void { // No-op for native scanner callback(null); } async cleanup(): Promise { await this.stopScan(); for (const handle of this.listenerHandles) { await handle(); } this.listenerHandles = []; this.scanListener = null; } } ``` 5. **Implement Web Scanner** ```typescript // WebInlineQRScanner.ts export class WebInlineQRScanner implements QRScannerService { private scanListener: ScanListener | null = null; private isScanning = false; private stream: MediaStream | null = null; private events = new EventEmitter(); constructor(private options?: QRScannerOptions) {} async checkPermissions(): Promise { try { const permissions = await navigator.permissions.query({ name: "camera" as PermissionName, }); return permissions.state === "granted"; } catch (error) { logger.error("Error checking camera permissions:", error); return false; } } async requestPermissions(): Promise { try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 }, }, }); stream.getTracks().forEach(track => track.stop()); return true; } catch (error) { logger.error("Error requesting camera permissions:", error); return false; } } async isSupported(): Promise { return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices; } async startScan(): Promise { if (this.isScanning) return; try { this.isScanning = true; this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 }, }, }); this.events.emit("stream", this.stream); } catch (error) { this.isScanning = false; const wrappedError = error instanceof Error ? error : new Error(String(error)); this.scanListener?.onError?.(wrappedError); throw wrappedError; } } async stopScan(): Promise { if (!this.isScanning) return; try { if (this.stream) { this.stream.getTracks().forEach(track => track.stop()); this.stream = null; } this.events.emit("stream", null); } catch (error) { logger.error("Error stopping scan:", error); throw error; } finally { this.isScanning = false; } } addListener(listener: ScanListener): void { this.scanListener = listener; } onStream(callback: (stream: MediaStream | null) => void): void { this.events.on("stream", callback); } async cleanup(): Promise { try { await this.stopScan(); this.events.removeAllListeners(); } catch (error) { logger.error("Error during cleanup:", error); } } } ``` ## Usage Example ```typescript // Example usage in a Vue component import { QRScannerFactory } from '@/services/QRScanner/QRScannerFactory'; export default defineComponent({ async mounted() { const scanner = QRScannerFactory.getInstance(); try { // Check and request permissions if (!(await scanner.checkPermissions())) { const granted = await scanner.requestPermissions(); if (!granted) { throw new Error('Camera permission denied'); } } // Add scan listener scanner.addListener({ onScan: (result) => { console.log('QR Code scanned:', result); }, onError: (error) => { console.error('Scan error:', error); } }); // Start scanning await scanner.startScan({ camera: 'back', showPreview: true }); // Handle stream for preview scanner.onStream((stream) => { if (stream) { // Update video element with stream this.videoElement.srcObject = stream; } }); } catch (error) { console.error('Failed to start scanner:', error); } }, async beforeUnmount() { // Clean up scanner await QRScannerFactory.cleanup(); } }); ``` ## Best Practices 1. **Error Handling** - Always implement error handlers in scan listeners - Handle permission denials gracefully - Provide user feedback for errors - Clean up resources on errors 2. **Resource Management** - Always call cleanup when done - Stop camera streams properly - Remove event listeners - Handle component unmounting 3. **Performance** - Use appropriate camera resolution - Clean up resources promptly - Handle platform-specific optimizations - Monitor memory usage 4. **Security** - Require HTTPS for web implementation - Validate scanned data - Handle permissions properly - Sanitize user input 5. **Testing** - Test on multiple devices - Verify permission flows - Check error scenarios - Validate cleanup - Test cross-platform behavior ## Platform-Specific Notes ### Mobile (Capacitor) - Uses MLKit for optimal performance - Handles native permissions - Supports both iOS and Android - Uses back camera by default - Handles device rotation - Provides native UI for scanning ### Web - Uses MediaDevices API - Requires HTTPS for camera access - Handles browser compatibility - Manages memory and resources - Provides fallback UI - Uses vue-qrcode-reader for web scanning ## Testing 1. **Unit Tests** - Test factory pattern - Test platform detection - Test error handling - Test cleanup procedures - Test permission flows 2. **Integration Tests** - Test camera access - Test QR code detection - Test cross-platform behavior - Test UI components - Test error scenarios 3. **E2E Tests** - Test complete scanning flow - Test permission handling - Test cross-platform compatibility - Test error recovery - Test cleanup procedures ## Best Practices 1. **Error Handling** - Always handle permission errors gracefully - Provide clear error messages to users - Implement proper cleanup on errors - Log errors for debugging 2. **Performance** - Clean up resources when not in use - Handle device rotation properly - Optimize camera usage - Manage memory efficiently 3. **Security** - Request minimum required permissions - Handle sensitive data securely - Validate scanned data - Implement proper cleanup 4. **User Experience** - Provide clear feedback - Handle edge cases gracefully - Support both platforms seamlessly - Implement proper loading states ## Troubleshooting 1. **Common Issues** - Camera permissions denied - Device not supported - Scanner not working - Memory leaks - UI glitches 2. **Solutions** - Check permissions - Verify device support - Debug scanner implementation - Monitor memory usage - Test UI components ## Maintenance 1. **Regular Updates** - Keep dependencies updated - Monitor platform changes - Update documentation - Review security patches 2. **Performance Monitoring** - Track memory usage - Monitor camera performance - Check error rates - Analyze user feedback