You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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

  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

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

  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

// 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

  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
    // 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);
    }
    
  2. 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:

  1. 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>
    
  2. 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:

  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

    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;
      }
    }
    
  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

    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

    // 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

    // 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;
      }
    }
    
  3. 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');
      }
    }
    
  4. 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

  1. 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>
    
  2. 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" />
    
  3. 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

  1. 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;
    }
    
  2. 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

  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