From cfc0730e75685c2459a554386fa4b30327f0fda8 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Mon, 19 May 2025 04:40:18 -0400 Subject: [PATCH 1/3] feat: implement comprehensive camera state management - Add CameraState type and CameraStateListener interface for standardized state handling - Implement camera state tracking in WebInlineQRScanner: - Add state management properties and methods - Update state transitions during camera operations - Add proper error state handling for different scenarios - Enhance QR scanner UI with improved state feedback: - Add color-coded status indicators - Implement state-specific messages and notifications - Add user-friendly error notifications for common issues - Improve error handling with specific states for: - Camera in use by another application - Permission denied - Camera not found - General errors This change improves the user experience by providing clear visual feedback about the camera's state and better error handling with actionable notifications. --- src/services/QRScanner/WebInlineQRScanner.ts | 119 ++++++++++-------- src/services/QRScanner/types.ts | 20 +++ src/views/ContactQRScanShowView.vue | 126 ++++++++++++------- 3 files changed, 165 insertions(+), 100 deletions(-) diff --git a/src/services/QRScanner/WebInlineQRScanner.ts b/src/services/QRScanner/WebInlineQRScanner.ts index e7560415..a038dc22 100644 --- a/src/services/QRScanner/WebInlineQRScanner.ts +++ b/src/services/QRScanner/WebInlineQRScanner.ts @@ -1,4 +1,4 @@ -import { QRScannerService, ScanListener, QRScannerOptions } from "./types"; +import { QRScannerService, ScanListener, QRScannerOptions, CameraState, CameraStateListener } from "./types"; import { logger } from "@/utils/logger"; import { EventEmitter } from "events"; import jsQR from "jsqr"; @@ -21,6 +21,9 @@ export class WebInlineQRScanner implements QRScannerService { private readonly TARGET_FPS = 15; // Target 15 FPS for scanning private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames private lastFrameTime = 0; + private cameraStateListeners: Set = new Set(); + private currentState: CameraState = 'off'; + private currentStateMessage?: string; constructor(private options?: QRScannerOptions) { // Generate a short random ID for this scanner instance @@ -43,8 +46,35 @@ export class WebInlineQRScanner implements QRScannerService { ); } + private updateCameraState(state: CameraState, message?: string) { + this.currentState = state; + this.currentStateMessage = message; + this.cameraStateListeners.forEach(listener => { + try { + listener.onStateChange(state, message); + logger.info(`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`, { + state, + message, + }); + } catch (error) { + logger.error(`[WebInlineQRScanner:${this.id}] Error in camera state listener:`, error); + } + }); + } + + addCameraStateListener(listener: CameraStateListener): void { + this.cameraStateListeners.add(listener); + // Immediately notify the new listener of current state + listener.onStateChange(this.currentState, this.currentStateMessage); + } + + removeCameraStateListener(listener: CameraStateListener): void { + this.cameraStateListeners.delete(listener); + } + async checkPermissions(): Promise { try { + this.updateCameraState('initializing', 'Checking camera permissions...'); logger.error( `[WebInlineQRScanner:${this.id}] Checking camera permissions...`, ); @@ -55,7 +85,9 @@ export class WebInlineQRScanner implements QRScannerService { `[WebInlineQRScanner:${this.id}] Permission state:`, permissions.state, ); - return permissions.state === "granted"; + const granted = permissions.state === "granted"; + this.updateCameraState(granted ? 'ready' : 'permission_denied'); + return granted; } catch (error) { logger.error( `[WebInlineQRScanner:${this.id}] Error checking camera permissions:`, @@ -64,12 +96,14 @@ export class WebInlineQRScanner implements QRScannerService { stack: error instanceof Error ? error.stack : undefined, }, ); + this.updateCameraState('error', 'Error checking camera permissions'); return false; } } async requestPermissions(): Promise { try { + this.updateCameraState('initializing', 'Requesting camera permissions...'); logger.error( `[WebInlineQRScanner:${this.id}] Requesting camera permissions...`, ); @@ -107,10 +141,8 @@ export class WebInlineQRScanner implements QRScannerService { }, }); - logger.error( - `[WebInlineQRScanner:${this.id}] Camera stream obtained successfully`, - ); - + this.updateCameraState('ready', 'Camera permissions granted'); + // Stop the test stream immediately stream.getTracks().forEach((track) => { logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, { @@ -122,36 +154,20 @@ export class WebInlineQRScanner implements QRScannerService { }); return true; } catch (error) { - const wrappedError = - error instanceof Error ? error : new Error(String(error)); - logger.error( - `[WebInlineQRScanner:${this.id}] Error requesting camera permissions:`, - { - error: wrappedError.message, - stack: wrappedError.stack, - name: wrappedError.name, - }, - ); - - // Provide more specific error messages - if ( - wrappedError.name === "NotFoundError" || - wrappedError.name === "DevicesNotFoundError" - ) { + const wrappedError = error instanceof Error ? error : new Error(String(error)); + + // Update state based on error type + if (wrappedError.name === "NotFoundError" || wrappedError.name === "DevicesNotFoundError") { + this.updateCameraState('not_found', 'No camera found on this device'); throw new Error("No camera found on this device"); - } else if ( - wrappedError.name === "NotAllowedError" || - wrappedError.name === "PermissionDeniedError" - ) { - throw new Error( - "Camera access denied. Please grant camera permission and try again", - ); - } else if ( - wrappedError.name === "NotReadableError" || - wrappedError.name === "TrackStartError" - ) { + } else if (wrappedError.name === "NotAllowedError" || wrappedError.name === "PermissionDeniedError") { + this.updateCameraState('permission_denied', 'Camera access denied'); + throw new Error("Camera access denied. Please grant camera permission and try again"); + } else if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") { + this.updateCameraState('in_use', 'Camera is in use by another application'); throw new Error("Camera is in use by another application"); } else { + this.updateCameraState('error', wrappedError.message); throw new Error(`Camera error: ${wrappedError.message}`); } } @@ -390,6 +406,7 @@ export class WebInlineQRScanner implements QRScannerService { this.isScanning = true; this.scanAttempts = 0; this.lastScanTime = Date.now(); + this.updateCameraState('initializing', 'Starting camera...'); logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`); // Get camera stream @@ -404,6 +421,8 @@ export class WebInlineQRScanner implements QRScannerService { }, }); + this.updateCameraState('active', 'Camera is active'); + logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, { tracks: this.stream.getTracks().map((t) => ({ kind: t.kind, @@ -429,13 +448,15 @@ export class WebInlineQRScanner implements QRScannerService { this.scanQRCode(); } catch (error) { this.isScanning = false; - const wrappedError = - error instanceof Error ? error : new Error(String(error)); - logger.error(`[WebInlineQRScanner:${this.id}] Error starting scan:`, { - error: wrappedError.message, - stack: wrappedError.stack, - name: wrappedError.name, - }); + const wrappedError = error instanceof Error ? error : new Error(String(error)); + + // Update state based on error type + if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") { + this.updateCameraState('in_use', 'Camera is in use by another application'); + } else { + this.updateCameraState('error', wrappedError.message); + } + if (this.scanListener?.onError) { this.scanListener.onError(wrappedError); } @@ -492,14 +513,9 @@ export class WebInlineQRScanner implements QRScannerService { `[WebInlineQRScanner:${this.id}] Stream stopped event emitted`, ); } catch (error) { - const wrappedError = - error instanceof Error ? error : new Error(String(error)); - logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, { - error: wrappedError.message, - stack: wrappedError.stack, - name: wrappedError.name, - }); - throw wrappedError; + logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, error); + this.updateCameraState('error', 'Error stopping camera'); + throw error; } finally { this.isScanning = false; logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`); @@ -541,10 +557,9 @@ export class WebInlineQRScanner implements QRScannerService { `[WebInlineQRScanner:${this.id}] Cleanup completed successfully`, ); } catch (error) { - logger.error(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - }); + logger.error(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, error); + this.updateCameraState('error', 'Error during cleanup'); + throw error; } } } diff --git a/src/services/QRScanner/types.ts b/src/services/QRScanner/types.ts index dda1a38a..9d21a69c 100644 --- a/src/services/QRScanner/types.ts +++ b/src/services/QRScanner/types.ts @@ -22,6 +22,20 @@ export interface QRScannerOptions { playSound?: boolean; } +export 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/stopped + +export interface CameraStateListener { + onStateChange: (state: CameraState, message?: string) => void; +} + /** * Interface for QR scanner service implementations */ @@ -44,6 +58,12 @@ export interface QRScannerService { /** Add a listener for scan events */ addListener(listener: ScanListener): void; + /** Add a listener for camera state changes */ + addCameraStateListener(listener: CameraStateListener): void; + + /** Remove a camera state listener */ + removeCameraStateListener(listener: CameraStateListener): void; + /** Clean up scanner resources */ cleanup(): Promise; } diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 52439805..da7cc538 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -88,7 +88,7 @@ class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10" >
- {{ initializationStatus }} + {{ cameraStateMessage || 'Initializing camera...' }}

Error: {{ error }}

- - Ready to scan + + {{ cameraStateMessage || 'Ready to scan' }}

@@ -202,6 +208,7 @@ import { retrieveAccountMetadata } from "../libs/util"; import { Router } from "vue-router"; import { logger } from "../utils/logger"; import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory"; +import { CameraState } from "@/services/QRScanner/types"; interface QRScanResult { rawValue?: string; @@ -239,7 +246,8 @@ export default class ContactQRScanShow extends Vue { initializationStatus = "Initializing camera..."; useQRReader = __USE_QR_READER__; preferredCamera: "user" | "environment" = "environment"; - cameraStatus = "Initializing"; + cameraState: CameraState = 'off'; + cameraStateMessage?: string; ETHR_DID_PREFIX = ETHR_DID_PREFIX; @@ -303,19 +311,70 @@ export default class ContactQRScanShow extends Vue { try { this.error = null; this.isScanning = true; - this.isInitializing = true; - this.initializationStatus = "Initializing camera..."; this.lastScannedValue = ""; this.lastScanTime = 0; const scanner = QRScannerFactory.getInstance(); + // Add camera state listener + scanner.addCameraStateListener({ + onStateChange: (state, message) => { + this.cameraState = state; + this.cameraStateMessage = message; + + // Update UI based on camera state + switch (state) { + case 'in_use': + this.error = "Camera is in use by another application"; + this.isScanning = false; + this.$notify( + { + group: "alert", + type: "warning", + title: "Camera in Use", + text: "Please close other applications using the camera and try again", + }, + 5000, + ); + break; + case 'permission_denied': + this.error = "Camera permission denied"; + this.isScanning = false; + this.$notify( + { + group: "alert", + type: "warning", + title: "Camera Access Required", + text: "Please grant camera permission to scan QR codes", + }, + 5000, + ); + break; + case 'not_found': + this.error = "No camera found"; + this.isScanning = false; + this.$notify( + { + group: "alert", + type: "warning", + title: "No Camera", + text: "No camera was found on this device", + }, + 5000, + ); + break; + case 'error': + this.error = this.cameraStateMessage || "Camera error"; + this.isScanning = false; + break; + } + }, + }); + // Check if scanning is supported first if (!(await scanner.isSupported())) { - this.error = - "Camera access requires HTTPS. Please use a secure connection."; + this.error = "Camera access requires HTTPS. Please use a secure connection."; this.isScanning = false; - this.isInitializing = false; this.$notify( { group: "alert", @@ -328,40 +387,11 @@ export default class ContactQRScanShow extends Vue { return; } - // Check permissions first - if (!(await scanner.checkPermissions())) { - this.initializationStatus = "Requesting camera permission..."; - const granted = await scanner.requestPermissions(); - if (!granted) { - this.error = "Camera permission denied"; - this.isScanning = false; - this.isInitializing = false; - // Show notification for better visibility - this.$notify( - { - group: "alert", - type: "warning", - title: "Camera Access Required", - text: "Camera permission denied", - }, - 5000, - ); - return; - } - } - - // For native platforms, use the scanner service - scanner.addListener({ - onScan: this.onScanDetect, - onError: this.onScanError, - }); - // Start scanning await scanner.startScan(); } catch (error) { this.error = error instanceof Error ? error.message : String(error); this.isScanning = false; - this.isInitializing = false; logger.error("Error starting scan:", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, @@ -828,12 +858,12 @@ export default class ContactQRScanShow extends Vue { try { await promise; this.isInitializing = false; - this.cameraStatus = "Ready"; + this.cameraState = "ready"; } catch (error) { const wrappedError = error instanceof Error ? error : new Error(String(error)); this.error = wrappedError.message; - this.cameraStatus = "Error"; + this.cameraState = "error"; this.isInitializing = false; logger.error("Error during QR scanner initialization:", { error: wrappedError.message, @@ -843,17 +873,17 @@ export default class ContactQRScanShow extends Vue { } onCameraOn(): void { - this.cameraStatus = "Active"; + this.cameraState = "active"; this.isInitializing = false; } onCameraOff(): void { - this.cameraStatus = "Off"; + this.cameraState = "off"; } onDetect(result: unknown): void { this.isScanning = true; - this.cameraStatus = "Detecting"; + this.cameraState = "detecting"; try { let rawValue: string | undefined; if ( @@ -874,7 +904,7 @@ export default class ContactQRScanShow extends Vue { this.handleError(error); } finally { this.isScanning = false; - this.cameraStatus = "Active"; + this.cameraState = "active"; } } @@ -897,12 +927,12 @@ export default class ContactQRScanShow extends Vue { const wrappedError = error instanceof Error ? error : new Error(String(error)); this.error = wrappedError.message; - this.cameraStatus = "Error"; + this.cameraState = "error"; } onError(error: Error): void { this.error = error.message; - this.cameraStatus = "Error"; + this.cameraState = "error"; logger.error("QR code scan error:", { error: error.message, stack: error.stack, From 8f0d09e4800449b21d7ebf221d35eb2f69b9ab83 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Mon, 19 May 2025 05:44:12 -0400 Subject: [PATCH 2/3] chore: cleanup documents --- {docs => doc}/DEEP_LINKS.md | 0 {docs => doc}/camera-implementation.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {docs => doc}/DEEP_LINKS.md (100%) rename {docs => doc}/camera-implementation.md (100%) diff --git a/docs/DEEP_LINKS.md b/doc/DEEP_LINKS.md similarity index 100% rename from docs/DEEP_LINKS.md rename to doc/DEEP_LINKS.md diff --git a/docs/camera-implementation.md b/doc/camera-implementation.md similarity index 100% rename from docs/camera-implementation.md rename to doc/camera-implementation.md From 7f12595c91bf1e3de40d181b276dc8153fea5f48 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Mon, 19 May 2025 06:28:46 -0400 Subject: [PATCH 3/3] docs: consolidate QR code implementation documentation Merge multiple QR code documentation files into a single comprehensive guide that accurately reflects the current implementation. The consolidated guide: - Combines information from qr-code-implementation-guide.mdc, qr-code-handling-rule.mdc, and camera-implementation.md - Clarifies the relationship between ContactQRScanView and ContactQRScanShowView - Streamlines build configuration documentation - Adds detailed sections on error handling, security, and best practices - Improves organization and readability of implementation details - Removes redundant information while preserving critical details This change improves documentation maintainability and provides a single source of truth for QR code implementation details. --- .cursor/rules/qr-code-handling-rule.mdc | 177 ------ .../rules/qr-code-implementation-guide.mdc | 533 ------------------ doc/camera-implementation.md | 507 ----------------- doc/qr-code-implementation-guide.md | 284 ++++++++++ web-push.md => doc/web-push.md | 0 qr-code-implementation-guide.md | 156 ----- src/views/ContactQRScanShowView.vue | 36 +- 7 files changed, 302 insertions(+), 1391 deletions(-) delete mode 100644 .cursor/rules/qr-code-handling-rule.mdc delete mode 100644 .cursor/rules/qr-code-implementation-guide.mdc delete mode 100644 doc/camera-implementation.md create mode 100644 doc/qr-code-implementation-guide.md rename web-push.md => doc/web-push.md (100%) delete mode 100644 qr-code-implementation-guide.md diff --git a/.cursor/rules/qr-code-handling-rule.mdc b/.cursor/rules/qr-code-handling-rule.mdc deleted file mode 100644 index d78e2e28..00000000 --- a/.cursor/rules/qr-code-handling-rule.mdc +++ /dev/null @@ -1,177 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# QR Code Handling Rule - -## Architecture Overview - -The QR code scanning functionality follows a platform-agnostic design using a factory pattern that provides different implementations for web and mobile platforms. - -### 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 - -## Mobile Implementation (Capacitor) - -### Technology Stack -- Uses `@capacitor-mlkit/barcode-scanning` plugin -- Configured in `capacitor.config.ts` -- Native camera access through platform APIs - -### Key Features -- Direct camera access via native APIs -- Optimized for mobile performance -- Supports both iOS and Android -- Real-time QR code detection -- Back camera preferred for scanning - -### Configuration -```typescript -MLKitBarcodeScanner: { - formats: ['QR_CODE'], - detectorSize: 1.0, - lensFacing: 'back', - googleBarcodeScannerModuleInstallState: true -} -``` - -### Permissions Handling -1. Check permissions via `BarcodeScanner.checkPermissions()` -2. Request permissions if needed -3. Handle permission states (granted/denied) -4. Graceful fallbacks for permission issues - -## Web Implementation - -### Technology Stack -- Uses browser's MediaDevices API -- Vue.js components for UI -- EventEmitter for stream management - -### Key Features -- Browser-based camera access -- Inline camera preview -- Responsive design -- Cross-browser compatibility -- Progressive enhancement - -### Permissions Handling -1. Uses browser's permission API -2. MediaDevices API for camera access -3. Handles secure context requirements -4. Provides user feedback for permission states - -## Shared Features - -### Error Handling -1. Permission denied scenarios -2. Device compatibility checks -3. Camera access failures -4. QR code validation -5. Network connectivity issues - -### User Experience -1. Clear feedback during scanning -2. Loading states -3. Error messages -4. Success confirmations -5. Camera preview - -### Security -1. HTTPS requirement for web -2. Permission validation -3. Data validation -4. Safe error handling - -## Usage Guidelines - -### Platform Detection -```typescript -const isNative = QRScannerFactory.isNativePlatform(); -if (isNative) { - // Use native scanner -} else { - // Use web scanner -} -``` - -### Implementation Example -```typescript -const scanner = QRScannerFactory.getInstance(); -await scanner.checkPermissions(); -await scanner.startScan(); -scanner.addListener({ - onScan: (result) => { - // Handle scan result - }, - onError: (error) => { - // Handle error - } -}); -``` - -### Best Practices -1. Always check permissions before starting scan -2. Clean up resources after scanning -3. Handle all error cases -4. Provide clear user feedback -5. Test on multiple devices/browsers - -## Platform-Specific Notes - -### Mobile (Capacitor) -1. Use native camera API when available -2. Handle device rotation -3. Support both front/back cameras -4. Manage system permissions properly -5. Handle app lifecycle events - -### Web -1. Check browser compatibility -2. Handle secure context requirement -3. Manage memory usage -4. Clean up MediaStream -5. Handle tab visibility changes - -## Testing Requirements - -1. Test on multiple devices -2. Verify permission flows -3. Check error handling -4. Validate cleanup -5. Verify cross-platform behavior - -## Service Interface - -```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; -} - -interface ScanListener { - onScan: (result: string) => void; - onError?: (error: Error) => void; -} - -interface QRScannerOptions { - camera?: "front" | "back"; - showPreview?: boolean; - playSound?: boolean; -} \ No newline at end of file diff --git a/.cursor/rules/qr-code-implementation-guide.mdc b/.cursor/rules/qr-code-implementation-guide.mdc deleted file mode 100644 index fd488f96..00000000 --- a/.cursor/rules/qr-code-implementation-guide.mdc +++ /dev/null @@ -1,533 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# QR Code Implementation Guide - -## 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 -``` - -## Core Interfaces - -```typescript -// types.ts -export interface ScanListener { - onScan: (result: string) => void; - onError?: (error: Error) => void; -} - -export interface QRScannerOptions { - camera?: "front" | "back"; - showPreview?: boolean; - playSound?: boolean; -} - -export 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; -} -``` - -## Configuration Files - -### Vite Configuration -```typescript -// vite.config.common.mts -export function createBuildConfig(mode: string) { - return { - 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) - } - }; -} -``` - -### 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 -``` - -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; - - private static isNativePlatform(): boolean { - const capacitorNative = Capacitor.isNativePlatform(); - const isMobile = typeof __IS_MOBILE__ !== "undefined" ? __IS_MOBILE__ : capacitorNative; - const platform = Capacitor.getPlatform(); - - // Always use native scanner on Android/iOS - if (platform === "android" || platform === "ios") { - return true; - } - - // For other platforms, use native if available - return capacitorNative || isMobile; - } - - static getInstance(): QRScannerService { - if (!this.instance) { - const isNative = this.isNativePlatform(); - - if (isNative) { - this.instance = new CapacitorQRScanner(); - } else { - this.instance = new WebInlineQRScanner(); - } - } - return this.instance!; - } - - static async cleanup(): Promise { - 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> = []; - private cleanupPromise: Promise | null = null; - - async checkPermissions(): Promise { - try { - const { camera } = await BarcodeScanner.checkPermissions(); - return camera === "granted"; - } catch (error) { - logger.error("Error checking camera permissions:", error); - return false; - } - } - - async requestPermissions(): Promise { - try { - if (await this.checkPermissions()) { - return true; - } - const { camera } = await BarcodeScanner.requestPermissions(); - return camera === "granted"; - } catch (error) { - logger.error("Error requesting camera permissions:", error); - return false; - } - } - - async isSupported(): Promise { - try { - const { supported } = await BarcodeScanner.isSupported(); - return supported; - } catch (error) { - logger.error("Error checking scanner support:", error); - return false; - } - } - - async startScan(options?: QRScannerOptions): Promise { - if (this.isScanning) return; - if (this.cleanupPromise) { - await this.cleanupPromise; - } - - try { - if (!(await this.checkPermissions())) { - const granted = await this.requestPermissions(); - if (!granted) { - throw new Error("Camera permission denied"); - } - } - - if (!(await this.isSupported())) { - throw new Error("QR scanning not supported on this device"); - } - - this.isScanning = true; - - const scanOptions: StartScanOptions = { - formats: [BarcodeFormat.QrCode], - lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back, - }; - - const handle = await BarcodeScanner.addListener("barcodeScanned", (result) => { - if (this.scanListener && result.barcode?.rawValue) { - this.scanListener.onScan(result.barcode.rawValue); - } - }); - this.listenerHandles.push(handle.remove); - - await BarcodeScanner.startScan(scanOptions); - } catch (error) { - this.isScanning = false; - await this.cleanup(); - this.scanListener?.onError?.(error instanceof Error ? error : new Error(String(error))); - throw error; - } - } - - async stopScan(): Promise { - if (!this.isScanning) return; - this.isScanning = false; - - try { - await BarcodeScanner.stopScan(); - } catch (error) { - logger.error("Error stopping scan:", error); - throw error; - } - } - - addListener(listener: ScanListener): void { - this.scanListener = listener; - } - - onStream(callback: (stream: MediaStream | null) => void): void { - // No-op for native scanner - callback(null); - } - - async cleanup(): Promise { - await this.stopScan(); - for (const handle of this.listenerHandles) { - await handle(); - } - this.listenerHandles = []; - this.scanListener = null; - } -} -``` - -5. **Implement Web Scanner** -```typescript -// WebInlineQRScanner.ts -export class WebInlineQRScanner implements QRScannerService { - private scanListener: ScanListener | null = null; - private isScanning = false; - private stream: MediaStream | null = null; - private events = new EventEmitter(); - - constructor(private options?: QRScannerOptions) {} - - async checkPermissions(): Promise { - 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 { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - video: { - facingMode: "environment", - width: { ideal: 1280 }, - height: { ideal: 720 }, - }, - }); - stream.getTracks().forEach(track => track.stop()); - return true; - } catch (error) { - logger.error("Error requesting camera permissions:", error); - return false; - } - } - - async isSupported(): Promise { - return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices; - } - - async startScan(): Promise { - if (this.isScanning) return; - - try { - this.isScanning = true; - this.stream = await navigator.mediaDevices.getUserMedia({ - video: { - facingMode: "environment", - width: { ideal: 1280 }, - height: { ideal: 720 }, - }, - }); - this.events.emit("stream", this.stream); - } catch (error) { - this.isScanning = false; - const wrappedError = error instanceof Error ? error : new Error(String(error)); - this.scanListener?.onError?.(wrappedError); - throw wrappedError; - } - } - - async stopScan(): Promise { - if (!this.isScanning) return; - - try { - if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()); - this.stream = null; - } - this.events.emit("stream", null); - } catch (error) { - logger.error("Error stopping scan:", error); - throw error; - } finally { - this.isScanning = false; - } - } - - addListener(listener: ScanListener): void { - this.scanListener = listener; - } - - onStream(callback: (stream: MediaStream | null) => void): void { - this.events.on("stream", callback); - } - - async cleanup(): Promise { - try { - await this.stopScan(); - this.events.removeAllListeners(); - } catch (error) { - logger.error("Error during cleanup:", error); - } - } -} -``` - -## Usage Example - -```typescript -// Example usage in a Vue component -import { QRScannerFactory } from '@/services/QRScanner/QRScannerFactory'; - -export default defineComponent({ - async mounted() { - const scanner = QRScannerFactory.getInstance(); - - try { - // Check and request permissions - if (!(await scanner.checkPermissions())) { - const granted = await scanner.requestPermissions(); - if (!granted) { - throw new Error('Camera permission denied'); - } - } - - // Add scan listener - scanner.addListener({ - onScan: (result) => { - console.log('QR Code scanned:', result); - }, - onError: (error) => { - console.error('Scan error:', error); - } - }); - - // Start scanning - await scanner.startScan({ - camera: 'back', - showPreview: true - }); - - // Handle stream for preview - scanner.onStream((stream) => { - if (stream) { - // Update video element with stream - this.videoElement.srcObject = stream; - } - }); - } catch (error) { - console.error('Failed to start scanner:', error); - } - }, - - async beforeUnmount() { - // Clean up scanner - await QRScannerFactory.cleanup(); - } -}); -``` - -## Best Practices - -1. **Error Handling** - - Always implement error handlers in scan listeners - - Handle permission denials gracefully - - Provide user feedback for errors - - Clean up resources on errors - -2. **Resource Management** - - Always call cleanup when done - - Stop camera streams properly - - Remove event listeners - - Handle component unmounting - -3. **Performance** - - Use appropriate camera resolution - - Clean up resources promptly - - Handle platform-specific optimizations - - Monitor memory usage - -4. **Security** - - Require HTTPS for web implementation - - Validate scanned data - - Handle permissions properly - - Sanitize user input - -5. **Testing** - - Test on multiple devices - - Verify permission flows - - Check error scenarios - - Validate cleanup - - Test cross-platform behavior - -## 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 diff --git a/doc/camera-implementation.md b/doc/camera-implementation.md deleted file mode 100644 index bd00e268..00000000 --- a/doc/camera-implementation.md +++ /dev/null @@ -1,507 +0,0 @@ -# Camera Implementation Documentation - -## Overview - -This document describes how camera functionality is implemented across the TimeSafari application. The application uses cameras for several purposes: - -1. QR Code scanning for contact sharing and verification -2. Photo capture for gift records -3. Profile photo management -4. Shared photo handling -5. Image upload and processing - -## Components - -### QRScannerDialog.vue - -Primary component for QR code scanning in web browsers. - -**Key Features:** - -- Uses `qrcode-stream` for web-based QR scanning -- Supports both front and back cameras -- Provides real-time camera status feedback -- Implements error handling with user-friendly messages -- Includes camera switching functionality - -**Camera Access Flow:** - -1. Checks for camera API availability -2. Enumerates available video devices -3. Requests camera permissions -4. Initializes camera stream with preferred settings -5. Handles various error conditions with specific messages - -### PhotoDialog.vue - -Component for photo capture and selection. - -**Key Features:** - -- Cross-platform photo capture interface -- Image cropping capabilities -- File selection fallback -- Unified interface for different platforms -- Progress feedback during upload -- Comprehensive error handling - -**Camera Access Flow:** - -1. User initiates photo capture -2. Platform-specific camera access is requested -3. Image is captured or selected -4. Optional cropping is performed -5. Image is processed and uploaded -6. URL is returned to caller - -### ImageMethodDialog.vue - -Component for selecting image input method. - -**Key Features:** -- Multiple input methods (camera, file upload, URL) -- Unified interface for image selection -- Integration with PhotoDialog for processing -- Support for image cropping -- URL-based image handling - -**Camera Access Flow:** - -1. User selects camera option -2. PhotoDialog is opened for capture -3. Captured image is processed -4. Image is returned to parent component - -### SharedPhotoView.vue - -Component for handling shared photos. - -**Key Features:** -- Processes incoming shared photos -- Options to use photo for gifts or profile -- Image preview and confirmation -- Server upload integration -- Temporary storage management - -**Photo Processing Flow:** - -1. Photo is shared to application -2. Stored temporarily in IndexedDB -3. User chooses usage (gift/profile) -4. Image is processed accordingly -5. Server upload is performed - -### ContactQRScanShowView.vue - -Component for QR code scanning in contact sharing. - -**Key Features:** -- QR code scanning interface -- Camera controls (start/stop) -- Platform-specific implementations -- Error handling and status feedback - -**Camera Access Flow:** - -1. User initiates scanning -2. Camera permissions are checked -3. Camera stream is initialized -4. QR codes are detected in real-time -5. Results are processed - -## Services - -### QRScanner Services - -#### WebDialogQRScanner - -Web-based implementation of QR scanning. - -**Key Methods:** - -- `checkPermissions()`: Verifies camera permission status -- `requestPermissions()`: Requests camera access -- `isSupported()`: Checks for camera API support -- Handles various error conditions with specific messages - -#### CapacitorQRScanner - -Native implementation using Capacitor's MLKit. - -**Key Features:** - -- Uses `@capacitor-mlkit/barcode-scanning` -- Supports both front and back cameras -- Implements permission management -- Provides continuous scanning capability - -### Platform Services - -#### WebPlatformService - -Web-specific implementation of platform features. - -**Camera Capabilities:** - -- Uses HTML5 file input with capture attribute for mobile -- Uses getUserMedia API for desktop webcam access -- Falls back to file selection if camera unavailable -- Processes captured images for consistent format -- Handles both mobile and desktop browser environments - -#### CapacitorPlatformService - -Native implementation using Capacitor. - -**Camera Features:** - -- Uses `Camera.getPhoto()` for native camera access -- Supports image editing -- Configures high-quality image capture -- Handles base64 image processing -- Provides native camera UI - -#### ElectronPlatformService - -Desktop implementation (currently unimplemented). - -**Status:** - -- Camera functionality not yet implemented -- Planned to use Electron's media APIs -- Will support desktop camera access - -## Camera Usage Scenarios - -### Gift Photo Capture - -**Implementation:** -- Uses PhotoDialog for capture/selection -- Supports multiple input methods -- Optional image cropping -- Server upload with authentication -- Integration with gift records - -**Flow:** -1. User initiates photo capture from gift details -2. ImageMethodDialog presents input options -3. PhotoDialog handles capture/selection -4. Image is processed and uploaded -5. URL is attached to gift record - -### Profile Photo Management - -**Implementation:** -- Uses same PhotoDialog component -- Enforces square aspect ratio -- Requires image cropping -- Updates user profile settings -- Handles profile image updates - -**Flow:** -1. User initiates profile photo update -2. PhotoDialog opens with cropping enabled -3. Image is captured/selected -4. User crops to square aspect ratio -5. Image is uploaded and profile updated - -### Shared Photo Processing - -**Implementation:** -- Handles incoming shared photos -- Temporary storage in IndexedDB -- Options for photo usage -- Server upload integration -- Cleanup after processing - -**Flow:** -1. Photo is shared to application -2. Stored temporarily in IndexedDB -3. SharedPhotoView presents options -4. User chooses usage (gift/profile) -5. Image is processed accordingly - -### QR Code Scanning - -**Implementation:** -- Platform-specific scanning components -- Real-time camera feed processing -- QR code detection and validation -- Contact information processing -- Error handling and retry - -**Flow:** -1. User initiates QR scanning -2. Camera permissions are checked -3. Camera stream is initialized -4. QR codes are detected -5. Contact information is processed - -### QR Code Workflow - -**Implementation Details:** - -The QR code scanning workflow is implemented across multiple components and services to provide a seamless experience for contact sharing and verification. The system supports both web-based and native implementations through platform-specific services. - -#### QR Code Generation - -**Contact QR Codes:** -- Generated using `qrcode.vue` component -- Contains encrypted contact information -- Includes user ID and verification data -- Supports offline sharing -- Implements error correction - -**QR Code Format:** -```json -{ - "type": "contact", - "userId": "encrypted_user_id", - "timestamp": "creation_time", - "version": "qr_code_version", - "data": "encrypted_contact_data" -} -``` - -#### QR Code Scanning 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 Implementation:* -- Uses `qrcode-stream` for real-time scanning -- Supports both front and back cameras -- Implements continuous scanning -- Provides visual feedback for scanning status -- Handles browser compatibility issues - -*Native Implementation (Capacitor):* -- Uses `@capacitor-mlkit/barcode-scanning` -- Leverages native camera capabilities -- Provides optimized scanning performance -- Supports multiple barcode formats -- Implements native permission handling - -**3. Scanning Process:** -- Camera stream is initialized -- Real-time frame analysis begins -- QR codes are detected and decoded -- 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 - -**5. Error Handling:** -- Invalid QR code format -- Expired QR codes -- Duplicate contact attempts -- Network connectivity issues -- Permission denials -- Camera access problems - -**6. Success Flow:** -- Contact information is extracted -- User is prompted for confirmation -- Contact is added to user's list -- Success notification is displayed -- Camera resources are cleaned up - -#### Security Measures - -**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 - -#### User Experience - -**Scanning Interface:** -- Clear visual feedback -- Camera preview -- Scanning status indicators -- Error messages -- Success confirmations - -**Accessibility:** -- Support for different screen sizes -- Clear instructions -- Error recovery options -- Alternative input methods -- Offline capability - -#### Performance Considerations - -**Optimization:** -- Efficient camera resource usage -- Quick QR code detection -- Minimal processing overhead -- Battery usage optimization -- Memory management - -**Platform-Specific Optimizations:** -- Web: Optimized for browser performance -- Native: Leverages device capabilities -- Desktop: Efficient resource usage -- Mobile: Battery and performance balance - -## Platform-Specific Considerations - -### iOS - -- Requires `NSCameraUsageDescription` in Info.plist -- Supports both front and back cameras -- Implements proper permission handling -- Uses native camera UI through Capacitor -- Handles photo library access - -### Android - -- Requires camera permissions in manifest -- Supports both front and back cameras -- Handles permission requests through Capacitor -- Uses native camera UI -- Manages photo library access - -### Web - -- Requires HTTPS for camera access -- Implements fallback mechanisms -- Handles browser compatibility issues -- Uses getUserMedia API on desktop -- Uses file input with capture on mobile -- Supports multiple input methods - -## 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. Upload failures -7. Image processing errors - -### Error Response - -- User-friendly error messages -- Troubleshooting tips -- Clear instructions for resolution -- Platform-specific guidance -- Graceful fallbacks - -## Security Considerations - -### Permission Management - -- Explicit permission requests -- Permission state tracking -- Graceful handling of denied permissions -- Platform-specific permission handling -- Secure permission storage - -### Data Handling - -- Secure image processing -- Proper cleanup of camera resources -- No persistent storage of camera data -- Secure server upload -- Temporary storage management - -## 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 -6. Use platform-specific optimizations - -### Performance - -1. Optimize camera resolution -2. Implement proper resource cleanup -3. Handle camera switching efficiently -4. Manage memory usage -5. Optimize image processing -6. Handle upload efficiently - -### User Experience - -1. Clear status indicators -2. Intuitive camera controls -3. Helpful error messages -4. Smooth camera switching -5. Responsive UI feedback -6. Platform-appropriate UI - -## Future Improvements - -### Planned Enhancements - -1. Implement Electron camera support -2. Add advanced camera features -3. Improve error handling -4. Enhance user feedback -5. Optimize performance -6. Add image compression options - -### Known Issues - -1. Electron camera implementation pending -2. Some browser compatibility limitations -3. Platform-specific quirks to address -4. Mobile browser camera access limitations -5. Image upload performance on slow connections - -## Dependencies - -### Key Packages - -- `@capacitor-mlkit/barcode-scanning` -- `qrcode-stream` -- `vue-picture-cropper` -- `@capacitor/camera` -- Platform-specific camera APIs - -## Testing - -### Test Scenarios - -1. Permission handling -2. Camera switching -3. Error conditions -4. Platform compatibility -5. Performance metrics -6. Upload scenarios -7. Image processing - -### Test Environment - -- Multiple browsers -- iOS and Android devices -- Desktop platforms -- Various network conditions -- Different camera configurations diff --git a/doc/qr-code-implementation-guide.md b/doc/qr-code-implementation-guide.md new file mode 100644 index 00000000..bf7e448d --- /dev/null +++ b/doc/qr-code-implementation-guide.md @@ -0,0 +1,284 @@ +# 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; +} + +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 + } + } +}; +``` + +#### 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 \ No newline at end of file diff --git a/web-push.md b/doc/web-push.md similarity index 100% rename from web-push.md rename to doc/web-push.md diff --git a/qr-code-implementation-guide.md b/qr-code-implementation-guide.md deleted file mode 100644 index ef25a90b..00000000 --- a/qr-code-implementation-guide.md +++ /dev/null @@ -1,156 +0,0 @@ -## Build Configuration - -### Common Vite Configuration -```typescript -// vite.config.common.mts -export async function createBuildConfig(mode: string) { - const isCapacitor = mode === "capacitor"; - - return defineConfig({ - build: { - rollupOptions: { - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'vue-facing-decorator'] - } - } - } - }, - define: { - __USE_QR_READER__: JSON.stringify(!isCapacitor), - __IS_MOBILE__: JSON.stringify(isCapacitor), - }, - optimizeDeps: { - include: [ - '@capacitor-mlkit/barcode-scanning', - 'vue-qrcode-reader' - ] - }, - resolve: { - alias: { - '@capacitor/app': path.resolve(__dirname, 'node_modules/@capacitor/app') - } - } - }); -} -``` - -### Web-Specific Configuration -```typescript -// vite.config.web.mts -import { defineConfig, mergeConfig } from "vite"; -import { createBuildConfig } from "./vite.config.common.mts"; - -export default defineConfig(async () => { - const baseConfig = await createBuildConfig('web'); - - return mergeConfig(baseConfig, { - define: { - __USE_QR_READER__: true, - __IS_MOBILE__: false, - } - }); -}); -``` - -### Capacitor-Specific Configuration -```typescript -// vite.config.capacitor.mts -import { defineConfig, mergeConfig } from "vite"; -import { createBuildConfig } from "./vite.config.common.mts"; - -export default defineConfig(async () => { - const baseConfig = await createBuildConfig('capacitor'); - - return mergeConfig(baseConfig, { - define: { - __USE_QR_READER__: false, - __IS_MOBILE__: true, - }, - build: { - rollupOptions: { - external: ['vue-qrcode-reader'], // Exclude web QR reader from mobile builds - output: { - entryFileNames: '[name]-mobile.js', - chunkFileNames: '[name]-mobile.js', - assetFileNames: '[name]-mobile.[ext]' - } - } - } - }); -}); -``` - -### Build Scripts -Add these scripts to your `package.json`: -```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" - } -} -``` - -### Environment Variables -Create a `.env` file: -```bash -# QR Scanner Configuration -VITE_QR_SCANNER_ENABLED=true -VITE_DEFAULT_CAMERA=back -``` - -### Build Process - -1. **Web Build** -```bash -npm run build:web -``` -This will: -- Include vue-qrcode-reader -- Set __USE_QR_READER__ to true -- Set __IS_MOBILE__ to false -- Build for web browsers - -2. **Capacitor Build** -```bash -npm run build:capacitor -``` -This will: -- Exclude vue-qrcode-reader -- Set __USE_QR_READER__ to false -- Set __IS_MOBILE__ to true -- Build for mobile platforms - -3. **Build Both** -```bash -npm run build:all -``` - -### Important Notes - -1. **Dependencies** -- Ensure all QR-related dependencies are properly listed in package.json -- Use exact versions to avoid compatibility issues -- Consider using peer dependencies for shared libraries - -2. **Bundle Size** -- Web build includes vue-qrcode-reader (~100KB) -- Mobile build includes @capacitor-mlkit/barcode-scanning (~50KB) -- Consider using dynamic imports for lazy loading - -3. **Platform Detection** -- Build flags determine which implementation to use -- Runtime checks provide fallback options -- Environment variables can override defaults - -4. **Performance** -- Mobile builds optimize for native performance -- Web builds include necessary polyfills -- Chunk splitting improves load times - -5. **Debugging** -- Source maps are enabled for development -- Build artifacts are properly named for identification -- Console logs help track initialization \ No newline at end of file diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index da7cc538..cd4d6246 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -326,29 +326,29 @@ export default class ContactQRScanShow extends Vue { switch (state) { case 'in_use': this.error = "Camera is in use by another application"; - this.isScanning = false; - this.$notify( - { - group: "alert", - type: "warning", + this.isScanning = false; + this.$notify( + { + group: "alert", + type: "warning", title: "Camera in Use", text: "Please close other applications using the camera and try again", - }, - 5000, - ); + }, + 5000, + ); break; case 'permission_denied': - this.error = "Camera permission denied"; - this.isScanning = false; - this.$notify( - { - group: "alert", - type: "warning", - title: "Camera Access Required", + this.error = "Camera permission denied"; + this.isScanning = false; + this.$notify( + { + group: "alert", + type: "warning", + title: "Camera Access Required", text: "Please grant camera permission to scan QR codes", - }, - 5000, - ); + }, + 5000, + ); break; case 'not_found': this.error = "No camera found";