21 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	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
- 
Factory Pattern - QRScannerFactory- Creates appropriate scanner instance based on platform
- Common interface QRScannerServiceimplemented by all scanners
- Platform detection via Capacitor and build flags
 
- 
Platform-Specific Implementations - CapacitorQRScanner- Native mobile implementation using MLKit
- WebInlineQRScanner- Web browser implementation using MediaDevices API
- QRScannerDialog.vue- Shared UI component
 
- 
View Components - ContactQRScanView- Dedicated view for scanning QR codes
- ContactQRScanShowView- Combined view for displaying and scanning QR codes
 
Implementation Details
Core Interfaces
interface QRScannerService {
  checkPermissions(): Promise<boolean>;
  requestPermissions(): Promise<boolean>;
  isSupported(): Promise<boolean>;
  startScan(options?: QRScannerOptions): Promise<void>;
  stopScan(): Promise<void>;
  addListener(listener: ScanListener): void;
  onStream(callback: (stream: MediaStream | null) => void): void;
  cleanup(): Promise<void>;
  getAvailableCameras(): Promise<MediaDeviceInfo[]>;
  switchCamera(deviceId: string): Promise<void>;
  getCurrentCamera(): Promise<MediaDeviceInfo | null>;
}
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:
// 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
- 
Initiation - User selects "Scan QR Code" option
- Platform-specific scanner is initialized
- Camera permissions are verified
- Appropriate scanner component is loaded
 
- 
Platform-Specific Implementation - Web: Uses qrcode-streamfor real-time scanning
- Native: Uses @capacitor-mlkit/barcode-scanning
 
- Web: Uses 
- 
Scanning Process - Camera stream initialization
- Real-time frame analysis
- QR code detection and decoding
- Validation of QR code format
- Processing of contact information
 
- 
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
// vite.config.common.mts
export async function createBuildConfig(mode: string) {
  const isCapacitor = mode === "capacitor";
  
  return defineConfig({
    define: {
      'process.env.VITE_PLATFORM': JSON.stringify(mode),
      // PWA is automatically enabled for web platforms via build configuration
      __IS_MOBILE__: JSON.stringify(isCapacitor),
      __USE_QR_READER__: JSON.stringify(!isCapacitor)
    },
    optimizeDeps: {
      include: [
        '@capacitor-mlkit/barcode-scanning',
        'vue-qrcode-reader'
      ]
    }
  });
}
Platform-Specific Builds
{
  "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
- No camera found
- Permission denied
- Camera in use by another application
- HTTPS required
- Browser compatibility issues
- Invalid QR code format
- Expired QR codes
- Duplicate contact attempts
- 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
- Always check for camera availability
- Request permissions explicitly
- Handle all error conditions
- Provide clear user feedback
- Implement proper cleanup
Performance
- Optimize camera resolution
- Implement proper resource cleanup
- Handle camera switching efficiently
- Manage memory usage
- Battery usage optimization
User Experience
- Clear visual feedback
- Camera preview
- Scanning status indicators
- Error messages
- Success confirmations
- Intuitive camera controls
- Smooth camera switching
- Responsive UI feedback
Testing
Test Scenarios
- Permission handling
- Camera switching
- Error conditions
- Platform compatibility
- Performance metrics
- QR code detection
- Contact processing
- 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
- 
Mobile (Capacitor) - Uses @capacitor-mlkit/barcode-scanning
- Supports front/back camera switching
- Native camera access through platform APIs
- Optimized for mobile performance
 // CapacitorQRScanner.ts async startScan(options?: QRScannerOptions): Promise<void> { const scanOptions: StartScanOptions = { formats: [BarcodeFormat.QrCode], lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back }; await BarcodeScanner.startScan(scanOptions); }
- Uses 
- 
Web (Desktop) - Uses browser's MediaDevices API
- Supports multiple camera devices
- Dynamic camera enumeration
- Real-time camera switching
 // WebInlineQRScanner.ts async getAvailableCameras(): Promise<MediaDeviceInfo[]> { const devices = await navigator.mediaDevices.enumerateDevices(); return devices.filter(device => device.kind === 'videoinput'); } async switchCamera(deviceId: string): Promise<void> { // 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
interface QRScannerService {
  // ... existing methods ...
  /** Get available cameras */
  getAvailableCameras(): Promise<MediaDeviceInfo[]>;
  
  /** Switch to a specific camera */
  switchCamera(deviceId: string): Promise<void>;
  
  /** Get current camera info */
  getCurrentCamera(): Promise<MediaDeviceInfo | null>;
}
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:
- 
Mobile Interface - Simple toggle button for front/back cameras
- Positioned in bottom-right corner
- Clear visual feedback during switching
- Native camera controls
 <button v-if="isNativePlatform" @click="toggleMobileCamera" class="camera-switch-btn" > <font-awesome icon="camera-rotate" /> Switch Camera </button>
- 
Desktop Interface - Dropdown menu with all available cameras
- Camera labels and device IDs
- Real-time camera switching
- Responsive design
 <select v-model="selectedCameraId" @change="onCameraChange" class="camera-select-dropdown" > <option v-for="camera in availableCameras" :key="camera.deviceId" :value="camera.deviceId" > {{ camera.label || `Camera ${camera.deviceId.slice(0, 4)}` }} </option> </select>
Error Handling
The camera switching implementation includes comprehensive error handling:
- 
Common Error Scenarios - Camera in use by another application
- Permission denied during switch
- Device not available
- Stream initialization failure
- Camera switch timeout
 
- 
Error Response private async handleCameraSwitch(deviceId: string): Promise<void> { 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; } }
- 
User Feedback - Visual indicators during switching
- Error notifications
- Camera state updates
- Permission request dialogs
 
State Management
The camera system maintains several states:
- 
Camera States 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
- 
State Transitions - Initialization → Ready
- Ready → Active
- Active → Switching
- Switching → Active/Error
- Any state → Off (on cleanup)
 
Best Practices
- 
Camera Access - Always check permissions before switching
- Handle camera busy states
- Implement proper cleanup
- Monitor camera state changes
 
- 
Performance - Optimize camera resolution
- Handle stream switching efficiently
- Manage memory usage
- Implement proper cleanup
 
- 
User Experience - Clear visual feedback
- Smooth camera transitions
- Intuitive camera controls
- Responsive UI updates
- Accessible camera selection
 
- 
Security - Secure camera access
- Permission management
- Device validation
- Stream security
 
Testing
- 
Test Scenarios - Camera switching on both platforms
- Permission handling
- Error conditions
- Multiple camera devices
- Camera busy states
- Stream initialization
- UI responsiveness
 
- 
Test Environment - Multiple mobile devices
- Various desktop browsers
- Different camera configurations
- Network conditions
- Permission states
 
Capacitor Implementation Details
MLKit Barcode Scanner Configuration
- 
Plugin Setup // 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 } } } };
- 
Camera Management // CapacitorQRScanner.ts export class CapacitorQRScanner implements QRScannerService { private currentLensFacing: LensFacing = LensFacing.Back; async getAvailableCameras(): Promise<MediaDeviceInfo[]> { // 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<void> { 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<MediaDeviceInfo | null> { return { deviceId: this.currentLensFacing === LensFacing.Front ? 'front' : 'back', label: this.currentLensFacing === LensFacing.Front ? 'Front Camera' : 'Back Camera', kind: 'videoinput' } as MediaDeviceInfo; } }
- 
Camera State Management // CapacitorQRScanner.ts private async handleCameraState(): Promise<void> { 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<boolean> { 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'); } }
- 
Error Handling // CapacitorQRScanner.ts private async handleCameraError(error: Error): Promise<void> { 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
- 
iOS Implementation - Camera permissions in Info.plist
- Privacy descriptions
- Camera usage description
- Background camera access
 <!-- ios/App/App/Info.plist --> <key>NSCameraUsageDescription</key> <string>We need access to your camera to scan QR codes</string> <key>NSPhotoLibraryUsageDescription</key> <string>We need access to save scanned QR codes</string>
- 
Android Implementation - Camera permissions in AndroidManifest.xml
- Runtime permission handling
- Camera features declaration
- Hardware feature requirements
 <!-- android/app/src/main/AndroidManifest.xml --> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />
- 
Platform-Specific Features - iOS: Camera orientation handling
- Android: Camera resolution optimization
- Both: Battery usage optimization
- Both: Memory management
 // 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
- 
Battery Usage // 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; }
- 
Memory Management // CapacitorQRScanner.ts private async cleanupResources(): Promise<void> { // 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
- 
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
 
- 
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
 
- 
Performance Testing - Battery usage monitoring
- Memory usage monitoring
- Camera switching speed
- QR code detection speed
- App responsiveness
- Background/foreground transitions