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
QRScannerService
implemented by all scanners - Platform detection via Capacitor and build flags
-
Platform-Specific Implementations
CapacitorQRScanner
- Native mobile implementation using MLKitWebInlineQRScanner
- Web browser implementation using MediaDevices APIQRScannerDialog.vue
- Shared UI component
-
View Components
ContactQRScanView
- Dedicated view for scanning QR codesContactQRScanShowView
- 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-stream
for 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),
'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
{
"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