--- 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<boolean>; requestPermissions(): Promise<boolean>; isSupported(): Promise<boolean>; startScan(): Promise<void>; stopScan(): Promise<void>; addListener(listener: ScanListener): void; cleanup(): Promise<void>; } ``` ## 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<void>> = []; 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<typeof QRScannerDialog> | null = null; private scanListener: ScanListener | null = null; async checkPermissions(): Promise<boolean> { 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<boolean> { 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<boolean> { 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<typeof QRScannerDialog>; } 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 <!-- QRScannerDialog.vue --> <template> <div v-if="visible" class="dialog-overlay z-[60]"> <div class="dialog relative"> <div class="dialog-header"> <h2>Scan QR Code</h2> <button @click="onClose" class="close-button">×</button> </div> <div class="dialog-content"> <div v-if="useQRReader"> <qrcode-stream class="w-full max-w-lg mx-auto" @detect="onScanDetect" @error="onScanError" /> </div> <div v-else> <button @click="startMobileScan" class="scan-button"> Start Camera </button> </div> </div> </div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { QrcodeStream } from 'vue-qrcode-reader'; export default defineComponent({ name: 'QRScannerDialog', components: { QrcodeStream }, props: { onScan: { type: Function, required: true }, onError: { type: Function, required: true }, onClose: { type: Function, required: true } }, data() { return { visible: true, useQRReader: __USE_QR_READER__ }; }, methods: { onScanDetect(promisedResult: Promise<string>) { promisedResult .then(result => this.onScan(result)) .catch(error => this.onError(error)); }, onScanError(error: Error) { this.onError(error); }, async startMobileScan() { try { const scanner = QRScannerFactory.getInstance(); await scanner.startScan(); } catch (error) { this.onError(error as Error); } } } }); </script> <style scoped> .dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; } .dialog { background: white; border-radius: 8px; padding: 20px; max-width: 90vw; max-height: 90vh; overflow: auto; } .dialog-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .close-button { background: none; border: none; font-size: 24px; cursor: pointer; } .scan-button { background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } .scan-button:hover { background: #45a049; } </style> ``` ## 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