--- description: globs: alwaysApply: true --- # QR Code Implementation Guide ## Directory Structure ``` src/ ├── components/ │ └── QRScanner/ │ ├── types.ts │ ├── factory.ts │ ├── CapacitorScanner.ts │ ├── WebDialogScanner.ts │ └── QRScannerDialog.vue ├── services/ │ └── QRScanner/ │ ├── types.ts │ ├── QRScannerFactory.ts │ ├── CapacitorQRScanner.ts │ └── WebDialogQRScanner.ts ``` ## Core Interfaces ```typescript // types.ts export interface ScanListener { onScan: (result: string) => void; onError?: (error: Error) => void; } export interface QRScannerService { checkPermissions(): Promise; requestPermissions(): Promise; isSupported(): Promise; startScan(): Promise; stopScan(): Promise; addListener(listener: ScanListener): void; cleanup(): Promise; } ``` ## Configuration Files ### Vite Configuration ```typescript // vite.config.ts export default defineConfig({ define: { __USE_QR_READER__: JSON.stringify(!isMobile), __IS_MOBILE__: JSON.stringify(isMobile), }, build: { rollupOptions: { external: isMobile ? ['vue-qrcode-reader'] : [], } } }); ``` ### 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 vue-qrcode-reader ``` 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; static getInstance(): QRScannerService { if (!this.instance) { if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { this.instance = new CapacitorQRScanner(); } else if (__USE_QR_READER__) { this.instance = new WebDialogQRScanner(); } else { throw new Error('No QR scanner implementation available'); } } return this.instance; } static async cleanup() { 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> = []; async checkPermissions() { try { const { camera } = await BarcodeScanner.checkPermissions(); return camera === 'granted'; } catch (error) { logger.error('Error checking camera permissions:', error); return false; } } async requestPermissions() { try { const { camera } = await BarcodeScanner.requestPermissions(); return camera === 'granted'; } catch (error) { logger.error('Error requesting camera permissions:', error); return false; } } async isSupported() { return Capacitor.isNativePlatform(); } async startScan() { if (this.isScanning) return; this.isScanning = true; try { await BarcodeScanner.startScan(); } catch (error) { this.isScanning = false; throw error; } } async stopScan() { if (!this.isScanning) return; this.isScanning = false; try { await BarcodeScanner.stopScan(); } catch (error) { logger.error('Error stopping scan:', error); } } addListener(listener: ScanListener) { this.scanListener = listener; const handle = BarcodeScanner.addListener('barcodeScanned', (result) => { if (this.scanListener) { this.scanListener.onScan(result.barcode); } }); this.listenerHandles.push(handle.remove); } async cleanup() { await this.stopScan(); for (const handle of this.listenerHandles) { await handle(); } this.listenerHandles = []; this.scanListener = null; } } ``` 5. **Implement Web Scanner** ```typescript // WebDialogQRScanner.ts export class WebDialogQRScanner implements QRScannerService { private dialogInstance: App | null = null; private dialogComponent: InstanceType | null = null; private scanListener: ScanListener | null = null; 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: true }); 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() { if (this.dialogInstance) return; const container = document.createElement('div'); document.body.appendChild(container); this.dialogInstance = createApp(QRScannerDialog, { onScan: (result: string) => { if (this.scanListener) { this.scanListener.onScan(result); } }, onError: (error: Error) => { if (this.scanListener?.onError) { this.scanListener.onError(error); } }, onClose: () => { this.cleanup(); } }); this.dialogComponent = this.dialogInstance.mount(container) as InstanceType; } async stopScan() { await this.cleanup(); } addListener(listener: ScanListener) { this.scanListener = listener; } async cleanup() { if (this.dialogInstance) { this.dialogInstance.unmount(); this.dialogInstance = null; this.dialogComponent = null; } } } ``` 6. **Create Dialog Component** ```vue ``` ## Usage Example ```typescript // In your component async function scanQRCode() { const scanner = QRScannerFactory.getInstance(); if (!(await scanner.checkPermissions())) { const granted = await scanner.requestPermissions(); if (!granted) { throw new Error('Camera permission denied'); } } scanner.addListener({ onScan: (result) => { console.log('Scanned:', result); }, onError: (error) => { console.error('Scan error:', error); } }); await scanner.startScan(); } // Cleanup when done onUnmounted(() => { QRScannerFactory.cleanup(); }); ``` ## 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