# QR Code Implementation Guide ## Overview This document describes the QR code scanning and generation implementation in the TimeSafari application. The system uses a platform-agnostic design with specific implementations for web and mobile platforms. ## Architecture ### 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 └── views/ ├── ContactQRScanView.vue # Dedicated scanning view └── ContactQRScanShowView.vue # Combined QR display and scanning view ``` ### Core Components 1. **Factory Pattern** - `QRScannerFactory` - Creates appropriate scanner instance based on platform - Common interface `QRScannerService` implemented by all scanners - Platform detection via Capacitor and build flags 2. **Platform-Specific Implementations** - `CapacitorQRScanner` - Native mobile implementation using MLKit - `WebInlineQRScanner` - Web browser implementation using MediaDevices API - `QRScannerDialog.vue` - Shared UI component 3. **View Components** - `ContactQRScanView` - Dedicated view for scanning QR codes - `ContactQRScanShowView` - Combined view for displaying and scanning QR codes ## Implementation Details ### Core Interfaces ```typescript 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; getAvailableCameras(): Promise; switchCamera(deviceId: string): Promise; getCurrentCamera(): Promise; } interface ScanListener { onScan: (result: string) => void; onError?: (error: Error) => void; } interface QRScannerOptions { camera?: "front" | "back"; showPreview?: boolean; playSound?: boolean; } ``` ### Platform-Specific Implementations #### Mobile (Capacitor) - Uses `@capacitor-mlkit/barcode-scanning` - Native camera access through platform APIs - Optimized for mobile performance - Supports both iOS and Android - Real-time QR code detection - Back camera preferred for scanning Configuration: ```typescript // capacitor.config.ts const config: CapacitorConfig = { plugins: { MLKitBarcodeScanner: { formats: ['QR_CODE'], detectorSize: 1.0, lensFacing: 'back', googleBarcodeScannerModuleInstallState: true, // Additional camera options cameraOptions: { quality: 0.8, allowEditing: false, resultType: 'uri', sourceType: 'CAMERA', saveToGallery: false } } } }; ``` #### Web - Uses browser's MediaDevices API - Vue.js components for UI - EventEmitter for stream management - Browser-based camera access - Inline camera preview - Responsive design - Cross-browser compatibility ### View Components #### ContactQRScanView - Dedicated view for scanning QR codes - Full-screen camera interface - Simple UI focused on scanning - Used primarily on native platforms - Streamlined scanning experience #### ContactQRScanShowView - Combined view for QR code display and scanning - Shows user's own QR code - Handles user registration status - Provides options to copy contact information - Platform-specific scanning implementation: - Native: Button to navigate to ContactQRScanView - Web: Built-in scanning functionality ### QR Code Workflow 1. **Initiation** - User selects "Scan QR Code" option - Platform-specific scanner is initialized - Camera permissions are verified - Appropriate scanner component is loaded 2. **Platform-Specific Implementation** - Web: Uses `qrcode-stream` for real-time scanning - Native: Uses `@capacitor-mlkit/barcode-scanning` 3. **Scanning Process** - Camera stream initialization - Real-time frame analysis - QR code detection and decoding - Validation of QR code format - Processing of contact information 4. **Contact Processing** - Decryption of contact data - Validation of user information - Verification of timestamp - Check for duplicate contacts - Processing of shared data ## Build Configuration ### Common Vite Configuration ```typescript // vite.config.common.mts export async function createBuildConfig(mode: string) { const isCapacitor = mode === "capacitor"; return defineConfig({ 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) }, optimizeDeps: { include: [ '@capacitor-mlkit/barcode-scanning', 'vue-qrcode-reader' ] } }); } ``` ### Platform-Specific Builds ```json { "scripts": { "build:web": "vite build --config vite.config.web.mts", "build:capacitor": "vite build --config vite.config.capacitor.mts", "build:all": "npm run build:web && npm run build:capacitor" } } ``` ## Error Handling ### Common Error Scenarios 1. No camera found 2. Permission denied 3. Camera in use by another application 4. HTTPS required 5. Browser compatibility issues 6. Invalid QR code format 7. Expired QR codes 8. Duplicate contact attempts 9. Network connectivity issues ### Error Response - User-friendly error messages - Troubleshooting tips - Clear instructions for resolution - Platform-specific guidance ## Security Considerations ### QR Code Security - Encryption of contact data - Timestamp validation - Version checking - User verification - Rate limiting for scans ### Data Protection - Secure transmission of contact data - Validation of QR code authenticity - Prevention of duplicate scans - Protection against malicious codes - Secure storage of contact information ## Best Practices ### Camera Access 1. Always check for camera availability 2. Request permissions explicitly 3. Handle all error conditions 4. Provide clear user feedback 5. Implement proper cleanup ### Performance 1. Optimize camera resolution 2. Implement proper resource cleanup 3. Handle camera switching efficiently 4. Manage memory usage 5. Battery usage optimization ### User Experience 1. Clear visual feedback 2. Camera preview 3. Scanning status indicators 4. Error messages 5. Success confirmations 6. Intuitive camera controls 7. Smooth camera switching 8. Responsive UI feedback ## Testing ### Test Scenarios 1. Permission handling 2. Camera switching 3. Error conditions 4. Platform compatibility 5. Performance metrics 6. QR code detection 7. Contact processing 8. Security validation ### Test Environment - Multiple browsers - iOS and Android devices - Various network conditions - Different camera configurations ## Dependencies ### Key Packages - `@capacitor-mlkit/barcode-scanning` - `qrcode-stream` - `vue-qrcode-reader` - Platform-specific camera APIs ## Maintenance ### Regular Updates - Keep dependencies updated - Monitor platform changes - Update documentation - Review security patches ### Performance Monitoring - Track memory usage - Monitor camera performance - Check error rates - Analyze user feedback ## Camera Handling ### Camera Switching Implementation The QR scanner supports camera switching on both mobile and desktop platforms through a unified interface. #### Platform-Specific Implementations 1. **Mobile (Capacitor)** - Uses `@capacitor-mlkit/barcode-scanning` - Supports front/back camera switching - Native camera access through platform APIs - Optimized for mobile performance ```typescript // CapacitorQRScanner.ts async startScan(options?: QRScannerOptions): Promise { const scanOptions: StartScanOptions = { formats: [BarcodeFormat.QrCode], lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back }; await BarcodeScanner.startScan(scanOptions); } ``` 2. **Web (Desktop)** - Uses browser's MediaDevices API - Supports multiple camera devices - Dynamic camera enumeration - Real-time camera switching ```typescript // WebInlineQRScanner.ts async getAvailableCameras(): Promise { const devices = await navigator.mediaDevices.enumerateDevices(); return devices.filter(device => device.kind === 'videoinput'); } async switchCamera(deviceId: string): Promise { // Stop current stream await this.stopScan(); // Start new stream with selected camera this.stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 720 } } }); // Update video and restart scanning if (this.video) { this.video.srcObject = this.stream; await this.video.play(); } this.scanQRCode(); } ``` ### Core Interfaces ```typescript interface QRScannerService { // ... existing methods ... /** Get available cameras */ getAvailableCameras(): Promise; /** Switch to a specific camera */ switchCamera(deviceId: string): Promise; /** Get current camera info */ getCurrentCamera(): Promise; } interface QRScannerOptions { /** Camera to use ('front' or 'back' for mobile) */ camera?: "front" | "back"; /** Whether to show a preview of the camera feed */ showPreview?: boolean; /** Whether to play a sound on successful scan */ playSound?: boolean; } ``` ### UI Components The camera switching UI adapts to the platform: 1. **Mobile Interface** - Simple toggle button for front/back cameras - Positioned in bottom-right corner - Clear visual feedback during switching - Native camera controls ```vue ``` 2. **Desktop Interface** - Dropdown menu with all available cameras - Camera labels and device IDs - Real-time camera switching - Responsive design ```vue ``` ### Error Handling The camera switching implementation includes comprehensive error handling: 1. **Common Error Scenarios** - Camera in use by another application - Permission denied during switch - Device not available - Stream initialization failure - Camera switch timeout 2. **Error Response** ```typescript private async handleCameraSwitch(deviceId: string): Promise { try { this.updateCameraState("initializing", "Switching camera..."); await this.switchCamera(deviceId); this.updateCameraState("active", "Camera switched successfully"); } catch (error) { this.updateCameraState("error", "Failed to switch camera"); throw error; } } ``` 3. **User Feedback** - Visual indicators during switching - Error notifications - Camera state updates - Permission request dialogs ### State Management The camera system maintains several states: 1. **Camera States** ```typescript type CameraState = | "initializing" // Camera is being initialized | "ready" // Camera is ready to use | "active" // Camera is actively streaming | "in_use" // Camera is in use by another application | "permission_denied" // Camera permission was denied | "not_found" // No camera found on device | "error" // Generic error state | "off"; // Camera is off ``` 2. **State Transitions** - Initialization → Ready - Ready → Active - Active → Switching - Switching → Active/Error - Any state → Off (on cleanup) ### Best Practices 1. **Camera Access** - Always check permissions before switching - Handle camera busy states - Implement proper cleanup - Monitor camera state changes 2. **Performance** - Optimize camera resolution - Handle stream switching efficiently - Manage memory usage - Implement proper cleanup 3. **User Experience** - Clear visual feedback - Smooth camera transitions - Intuitive camera controls - Responsive UI updates - Accessible camera selection 4. **Security** - Secure camera access - Permission management - Device validation - Stream security ### Testing 1. **Test Scenarios** - Camera switching on both platforms - Permission handling - Error conditions - Multiple camera devices - Camera busy states - Stream initialization - UI responsiveness 2. **Test Environment** - Multiple mobile devices - Various desktop browsers - Different camera configurations - Network conditions - Permission states ### Capacitor Implementation Details #### MLKit Barcode Scanner Configuration 1. **Plugin Setup** ```typescript // capacitor.config.ts const config: CapacitorConfig = { plugins: { MLKitBarcodeScanner: { formats: ['QR_CODE'], detectorSize: 1.0, lensFacing: 'back', googleBarcodeScannerModuleInstallState: true, // Additional camera options cameraOptions: { quality: 0.8, allowEditing: false, resultType: 'uri', sourceType: 'CAMERA', saveToGallery: false } } } }; ``` 2. **Camera Management** ```typescript // CapacitorQRScanner.ts export class CapacitorQRScanner implements QRScannerService { private currentLensFacing: LensFacing = LensFacing.Back; async getAvailableCameras(): Promise { // On mobile, we have two fixed cameras return [ { deviceId: 'back', label: 'Back Camera', kind: 'videoinput' }, { deviceId: 'front', label: 'Front Camera', kind: 'videoinput' } ] as MediaDeviceInfo[]; } async switchCamera(deviceId: string): Promise { if (!this.isScanning) return; const newLensFacing = deviceId === 'front' ? LensFacing.Front : LensFacing.Back; // Stop current scan await this.stopScan(); // Update lens facing this.currentLensFacing = newLensFacing; // Restart scan with new camera await this.startScan({ camera: deviceId as 'front' | 'back' }); } async getCurrentCamera(): Promise { return { deviceId: this.currentLensFacing === LensFacing.Front ? 'front' : 'back', label: this.currentLensFacing === LensFacing.Front ? 'Front Camera' : 'Back Camera', kind: 'videoinput' } as MediaDeviceInfo; } } ``` 3. **Camera State Management** ```typescript // CapacitorQRScanner.ts private async handleCameraState(): Promise { try { // Check if camera is available const { camera } = await BarcodeScanner.checkPermissions(); if (camera === 'denied') { this.updateCameraState('permission_denied'); return; } // Check if camera is in use const isInUse = await this.isCameraInUse(); if (isInUse) { this.updateCameraState('in_use'); return; } this.updateCameraState('ready'); } catch (error) { this.updateCameraState('error', error.message); } } private async isCameraInUse(): Promise { try { // Try to start a test scan await BarcodeScanner.startScan({ formats: [BarcodeFormat.QrCode], lensFacing: this.currentLensFacing }); // If successful, stop it immediately await BarcodeScanner.stopScan(); return false; } catch (error) { return error.message.includes('camera in use'); } } ``` 4. **Error Handling** ```typescript // CapacitorQRScanner.ts private async handleCameraError(error: Error): Promise { switch (error.name) { case 'CameraPermissionDenied': this.updateCameraState('permission_denied'); break; case 'CameraInUse': this.updateCameraState('in_use'); break; case 'CameraUnavailable': this.updateCameraState('not_found'); break; default: this.updateCameraState('error', error.message); } } ``` #### Platform-Specific Considerations 1. **iOS Implementation** - Camera permissions in Info.plist - Privacy descriptions - Camera usage description - Background camera access ```xml NSCameraUsageDescription We need access to your camera to scan QR codes NSPhotoLibraryUsageDescription We need access to save scanned QR codes ``` 2. **Android Implementation** - Camera permissions in AndroidManifest.xml - Runtime permission handling - Camera features declaration - Hardware feature requirements ```xml ``` 3. **Platform-Specific Features** - iOS: Camera orientation handling - Android: Camera resolution optimization - Both: Battery usage optimization - Both: Memory management ```typescript // Platform-specific optimizations private getPlatformSpecificOptions(): StartScanOptions { const baseOptions: StartScanOptions = { formats: [BarcodeFormat.QrCode], lensFacing: this.currentLensFacing }; if (Capacitor.getPlatform() === 'ios') { return { ...baseOptions, // iOS-specific options cameraOptions: { quality: 0.7, // Lower quality for better performance allowEditing: false, resultType: 'uri' } }; } else if (Capacitor.getPlatform() === 'android') { return { ...baseOptions, // Android-specific options cameraOptions: { quality: 0.8, allowEditing: false, resultType: 'uri', saveToGallery: false } }; } return baseOptions; } ``` #### Performance Optimization 1. **Battery Usage** ```typescript // CapacitorQRScanner.ts private optimizeBatteryUsage(): void { // Reduce scan frequency when battery is low if (this.isLowBattery()) { this.scanInterval = 2000; // 2 seconds between scans } else { this.scanInterval = 1000; // 1 second between scans } } private isLowBattery(): boolean { // Check battery level if available if (Capacitor.isPluginAvailable('Battery')) { const { level } = await Battery.getBatteryLevel(); return level < 0.2; // 20% or lower } return false; } ``` 2. **Memory Management** ```typescript // CapacitorQRScanner.ts private async cleanupResources(): Promise { // Stop scanning await this.stopScan(); // Clear any stored camera data this.currentLensFacing = LensFacing.Back; // Remove listeners this.listenerHandles.forEach(handle => handle()); this.listenerHandles = []; // Reset state this.isScanning = false; this.updateCameraState('off'); } ``` #### Testing on Capacitor 1. **Device Testing** - Test on multiple iOS devices - Test on multiple Android devices - Test different camera configurations - Test with different screen sizes - Test with different OS versions 2. **Camera Testing** - Test front camera switching - Test back camera switching - Test camera permissions - Test camera in use scenarios - Test low light conditions - Test different QR code sizes - Test different QR code distances 3. **Performance Testing** - Battery usage monitoring - Memory usage monitoring - Camera switching speed - QR code detection speed - App responsiveness - Background/foreground transitions