From cfc0730e75685c2459a554386fa4b30327f0fda8 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Mon, 19 May 2025 04:40:18 -0400 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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"; From 70174aea93a1e79c956680b1ab0a42e2c2a641d9 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 19 May 2025 18:50:08 +0800 Subject: [PATCH 04/12] Fix: current photo dialog --- src/components/PhotoDialog.vue | 237 +++++++++++++++++++++++++++++++-- src/lib/fontawesome.ts | 2 + 2 files changed, 225 insertions(+), 14 deletions(-) diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index 65d41e6b..342bf5d4 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -15,15 +15,16 @@ PhotoDialog.vue */
Uploading... Look Good? + Take Photo Say "Cheese"!
@@ -47,7 +48,7 @@ PhotoDialog.vue */ :options="{ viewMode: 1, dragMode: 'crop', - aspectRatio: 9 / 9, + aspectRatio: 1 / 1, }" class="max-h-[90vh] max-w-[90vw] object-contain" /> @@ -60,32 +61,45 @@ PhotoDialog.vue */ />
-
+
-
-
+
+
+ + +
+
@@ -142,20 +156,29 @@ export default class PhotoDialog extends Vue { /** Dialog visibility state */ visible = false; + /** Whether to show camera preview */ + showCameraPreview = false; + + /** Camera stream reference */ + private cameraStream: MediaStream | null = null; + private platformService = PlatformServiceFactory.getInstance(); URL = window.URL || window.webkitURL; isRegistered = false; + private platformCapabilities = this.platformService.getCapabilities(); /** * Lifecycle hook: Initializes component and retrieves user settings * @throws {Error} When settings retrieval fails */ async mounted() { + console.log('PhotoDialog mounted'); try { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.isRegistered = !!settings.isRegistered; + console.log('isRegistered:', this.isRegistered); } catch (error: unknown) { logger.error("Error retrieving settings from database:", error); this.$notify( @@ -173,6 +196,13 @@ export default class PhotoDialog extends Vue { } } + /** + * Lifecycle hook: Cleans up camera stream when component is destroyed + */ + beforeDestroy() { + this.stopCameraPreview(); + } + /** * Opens the photo dialog with specified configuration * @param setImageFn - Callback function to handle image URL after upload @@ -181,7 +211,7 @@ export default class PhotoDialog extends Vue { * @param blob - Optional existing image blob * @param inputFileName - Optional filename for the image */ - open( + async open( setImageFn: (arg: string) => void, claimType: string, crop?: boolean, @@ -204,6 +234,10 @@ export default class PhotoDialog extends Vue { this.blob = undefined; this.fileName = undefined; this.showRetry = true; + // Start camera preview automatically if no blob is provided + if (!this.platformCapabilities.isMobile) { + await this.startCameraPreview(); + } } } @@ -211,7 +245,9 @@ export default class PhotoDialog extends Vue { * Closes the photo dialog and resets state */ close() { + logger.debug("Dialog closing, current showCameraPreview:", this.showCameraPreview); this.visible = false; + this.stopCameraPreview(); const bottomNav = document.querySelector("#QuickNav") as HTMLElement; if (bottomNav) { bottomNav.style.display = ""; @@ -219,6 +255,138 @@ export default class PhotoDialog extends Vue { this.blob = undefined; } + /** + * Starts the camera preview + */ + async startCameraPreview() { + logger.debug("startCameraPreview called"); + logger.debug("Current showCameraPreview state:", this.showCameraPreview); + logger.debug("Platform capabilities:", this.platformCapabilities); + + // If we're on a mobile device or using Capacitor, use the platform service + if (this.platformCapabilities.isMobile) { + logger.debug("Using platform service for mobile device"); + try { + const result = await this.platformService.takePicture(); + this.blob = result.blob; + this.fileName = result.fileName; + } catch (error) { + logger.error("Error taking picture:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to take picture. Please try again.", + }, + 5000, + ); + } + return; + } + + // For desktop web browsers, use our custom preview + logger.debug("Starting camera preview for desktop browser"); + try { + // Set state before requesting camera access + this.showCameraPreview = true; + logger.debug("showCameraPreview set to:", this.showCameraPreview); + + // Force a re-render + await this.$nextTick(); + logger.debug("After nextTick, showCameraPreview is:", this.showCameraPreview); + + logger.debug("Requesting camera access..."); + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: "environment" }, + }); + logger.debug("Camera access granted, setting up video element"); + this.cameraStream = stream; + + // Force another re-render after getting the stream + await this.$nextTick(); + logger.debug("After getting stream, showCameraPreview is:", this.showCameraPreview); + + const videoElement = this.$refs.videoElement as HTMLVideoElement; + if (videoElement) { + logger.debug("Video element found, setting srcObject"); + videoElement.srcObject = stream; + // Wait for video to be ready + await new Promise((resolve) => { + videoElement.onloadedmetadata = () => { + logger.debug("Video metadata loaded"); + videoElement.play().then(() => { + logger.debug("Video playback started"); + resolve(true); + }); + }; + }); + } else { + logger.error("Video element not found"); + } + } catch (error) { + logger.error("Error starting camera preview:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to access camera. Please try again.", + }, + 5000, + ); + this.showCameraPreview = false; + } + } + + /** + * Stops the camera preview and cleans up resources + */ + stopCameraPreview() { + logger.debug("Stopping camera preview, current showCameraPreview:", this.showCameraPreview); + if (this.cameraStream) { + this.cameraStream.getTracks().forEach((track) => track.stop()); + this.cameraStream = null; + } + this.showCameraPreview = false; + logger.debug("After stopping, showCameraPreview is:", this.showCameraPreview); + } + + /** + * Captures a photo from the camera preview + */ + async capturePhoto() { + if (!this.cameraStream) return; + + try { + const videoElement = this.$refs.videoElement as HTMLVideoElement; + const canvas = document.createElement("canvas"); + canvas.width = videoElement.videoWidth; + canvas.height = videoElement.videoHeight; + const ctx = canvas.getContext("2d"); + ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + if (blob) { + this.blob = blob; + this.fileName = `photo_${Date.now()}.jpg`; + this.stopCameraPreview(); + } + }, "image/jpeg", 0.95); + } catch (error) { + logger.error("Error capturing photo:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to capture photo. Please try again.", + }, + 5000, + ); + } + } + /** * Captures a photo using device camera * @throws {Error} When camera access fails @@ -275,10 +443,13 @@ export default class PhotoDialog extends Vue { } /** - * Resets the current image selection + * Resets the current image selection and restarts camera preview */ async retryImage() { this.blob = undefined; + if (!this.platformCapabilities.isMobile) { + await this.startCameraPreview(); + } } /** @@ -422,5 +593,43 @@ export default class PhotoDialog extends Vue { border-radius: 0.5rem; width: 100%; max-width: 700px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* Camera preview styling */ +.camera-preview { + flex: 1; + background-color: #000; + overflow: hidden; + position: relative; +} + +.camera-container { + width: 100%; + height: 100%; + position: relative; +} + +.camera-video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.capture-button { + position: absolute; + bottom: 1rem; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(to bottom, #60a5fa, #2563eb); + color: white; + padding: 0.75rem 1.5rem; + border-radius: 9999px; + box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.5); + border: none; + cursor: pointer; } diff --git a/src/lib/fontawesome.ts b/src/lib/fontawesome.ts index 181bcb15..37b5343c 100644 --- a/src/lib/fontawesome.ts +++ b/src/lib/fontawesome.ts @@ -54,6 +54,7 @@ import { faHandHoldingDollar, faHandHoldingHeart, faHouseChimney, + faImage, faImagePortrait, faLeftRight, faLightbulb, @@ -135,6 +136,7 @@ library.add( faHandHoldingDollar, faHandHoldingHeart, faHouseChimney, + faImage, faImagePortrait, faLeftRight, faLightbulb, From 616a69b7fdc0ee19de94b3ee81f40ce3ea4f32f8 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Mon, 19 May 2025 22:18:24 -0400 Subject: [PATCH 05/12] chore: update capacitor config and script paths - Update capacitor.config.json: - Change appId from com.brownspank.timesafari to app.timesafari - Add server configuration with cleartext enabled - Add plugins configuration for App URL handling - Update script documentation paths: - Change ./openssl_signing_console.sh to /scripts/openssl_signing_console.sh - Change ./openssl_signing_console.rst to /doc/openssl_signing_console.rst This change standardizes the app identifier and adds necessary capacitor configurations for development, while also fixing script documentation paths to use absolute references. --- capacitor.config.json | 23 +++++++++++++++---- .../openssl_signing_console.rst | 2 +- .../openssl_signing_console.sh | 4 ++-- 3 files changed, 22 insertions(+), 7 deletions(-) rename openssl_signing_console.rst => doc/openssl_signing_console.rst (97%) rename openssl_signing_console.sh => scripts/openssl_signing_console.sh (90%) diff --git a/capacitor.config.json b/capacitor.config.json index 18f42003..dba3e9d8 100644 --- a/capacitor.config.json +++ b/capacitor.config.json @@ -1,6 +1,21 @@ { - "appId": "com.brownspank.timesafari", - "appName": "TimeSafari", - "webDir": "dist", - "bundledWebRuntime": false + "appId": "app.timesafari", + "appName": "TimeSafari", + "webDir": "dist", + "bundledWebRuntime": false, + "server": { + "cleartext": true + }, + "plugins": { + "App": { + "appUrlOpen": { + "handlers": [ + { + "url": "timesafari://*", + "autoVerify": true + } + ] + } + } + } } diff --git a/openssl_signing_console.rst b/doc/openssl_signing_console.rst similarity index 97% rename from openssl_signing_console.rst rename to doc/openssl_signing_console.rst index 8b7befdf..0bfabdec 100644 --- a/openssl_signing_console.rst +++ b/doc/openssl_signing_console.rst @@ -1,6 +1,6 @@ JWT Creation & Verification -To run this in a script, see ./openssl_signing_console.sh +To run this in a script, see /scripts/openssl_signing_console.sh Prerequisites: openssl, jq diff --git a/openssl_signing_console.sh b/scripts/openssl_signing_console.sh similarity index 90% rename from openssl_signing_console.sh rename to scripts/openssl_signing_console.sh index b459dad0..eb187c59 100755 --- a/openssl_signing_console.sh +++ b/scripts/openssl_signing_console.sh @@ -4,9 +4,9 @@ # # Prerequisites: openssl, jq # -# Usage: source ./openssl_signing_console.sh +# Usage: source /scripts/openssl_signing_console.sh # -# For a more complete explanation, see ./openssl_signing_console.rst +# For a more complete explanation, see /doc/openssl_signing_console.rst # Generate a key and extract the public part From 788d162b1ce7dc3b78431d32569035edc95e6f7d Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Mon, 19 May 2025 22:25:12 -0400 Subject: [PATCH 06/12] refactor: move lib directory to libs for consistency - Move src/lib/capacitor to src/libs/capacitor - Move src/lib/fontawesome.ts to src/libs/fontawesome.ts - Update import paths in main.capacitor.ts and main.common.ts - Remove empty src/lib directory This change standardizes the project structure by using the 'libs' directory consistently throughout the codebase. --- src/{lib => libs}/capacitor/app.ts | 0 src/{lib => libs}/fontawesome.ts | 0 src/main.capacitor.ts | 2 +- src/main.common.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{lib => libs}/capacitor/app.ts (100%) rename src/{lib => libs}/fontawesome.ts (100%) diff --git a/src/lib/capacitor/app.ts b/src/libs/capacitor/app.ts similarity index 100% rename from src/lib/capacitor/app.ts rename to src/libs/capacitor/app.ts diff --git a/src/lib/fontawesome.ts b/src/libs/fontawesome.ts similarity index 100% rename from src/lib/fontawesome.ts rename to src/libs/fontawesome.ts diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index 715828e5..b0b4290f 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -29,7 +29,7 @@ */ import { initializeApp } from "./main.common"; -import { App } from "./lib/capacitor/app"; +import { App } from "./libs/capacitor/app"; import router from "./router"; import { handleApiError } from "./services/api"; import { AxiosError } from "axios"; diff --git a/src/main.common.ts b/src/main.common.ts index dadb5d5b..ab3944d3 100644 --- a/src/main.common.ts +++ b/src/main.common.ts @@ -6,7 +6,7 @@ import axios from "axios"; import VueAxios from "vue-axios"; import Notifications from "notiwind"; import "./assets/styles/tailwind.css"; -import { FontAwesomeIcon } from "./lib/fontawesome"; +import { FontAwesomeIcon } from "./libs/fontawesome"; import Camera from "simple-vue-camera"; import { logger } from "./utils/logger"; From a86e577127c1d371bed7b6368ab8bfe5bc3ab7bd Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Tue, 20 May 2025 01:15:47 -0400 Subject: [PATCH 07/12] style: improve code formatting and readability - Format Vue template attributes and event handlers for better readability - Reorganize component props and event bindings - Improve error handling and state management in QR scanner - Add proper aria labels and accessibility attributes - Refactor camera state handling in WebInlineQRScanner - Clean up promise handling in WebPlatformService - Standardize string quotes to double quotes - Improve component structure and indentation No functional changes, purely code style and maintainability improvements. --- src/components/ImageMethodDialog.vue | 4 +- src/components/PhotoDialog.vue | 51 +++-- src/services/QRScanner/WebInlineQRScanner.ts | 117 ++++++++---- src/services/QRScanner/types.ts | 18 +- src/services/platforms/WebPlatformService.ts | 187 ++++++++++--------- src/views/AccountViewView.vue | 110 +++++++---- src/views/ContactQRScanShowView.vue | 76 ++++---- 7 files changed, 340 insertions(+), 223 deletions(-) diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 6585b2b5..25c8db93 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -51,7 +51,9 @@
diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index 342bf5d4..651ef9ac 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -173,12 +173,12 @@ export default class PhotoDialog extends Vue { * @throws {Error} When settings retrieval fails */ async mounted() { - console.log('PhotoDialog mounted'); + logger.log("PhotoDialog mounted"); try { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.isRegistered = !!settings.isRegistered; - console.log('isRegistered:', this.isRegistered); + logger.log("isRegistered:", this.isRegistered); } catch (error: unknown) { logger.error("Error retrieving settings from database:", error); this.$notify( @@ -245,7 +245,10 @@ export default class PhotoDialog extends Vue { * Closes the photo dialog and resets state */ close() { - logger.debug("Dialog closing, current showCameraPreview:", this.showCameraPreview); + logger.debug( + "Dialog closing, current showCameraPreview:", + this.showCameraPreview, + ); this.visible = false; this.stopCameraPreview(); const bottomNav = document.querySelector("#QuickNav") as HTMLElement; @@ -291,10 +294,13 @@ export default class PhotoDialog extends Vue { // Set state before requesting camera access this.showCameraPreview = true; logger.debug("showCameraPreview set to:", this.showCameraPreview); - + // Force a re-render await this.$nextTick(); - logger.debug("After nextTick, showCameraPreview is:", this.showCameraPreview); + logger.debug( + "After nextTick, showCameraPreview is:", + this.showCameraPreview, + ); logger.debug("Requesting camera access..."); const stream = await navigator.mediaDevices.getUserMedia({ @@ -302,10 +308,13 @@ export default class PhotoDialog extends Vue { }); logger.debug("Camera access granted, setting up video element"); this.cameraStream = stream; - + // Force another re-render after getting the stream await this.$nextTick(); - logger.debug("After getting stream, showCameraPreview is:", this.showCameraPreview); + logger.debug( + "After getting stream, showCameraPreview is:", + this.showCameraPreview, + ); const videoElement = this.$refs.videoElement as HTMLVideoElement; if (videoElement) { @@ -343,13 +352,19 @@ export default class PhotoDialog extends Vue { * Stops the camera preview and cleans up resources */ stopCameraPreview() { - logger.debug("Stopping camera preview, current showCameraPreview:", this.showCameraPreview); + logger.debug( + "Stopping camera preview, current showCameraPreview:", + this.showCameraPreview, + ); if (this.cameraStream) { this.cameraStream.getTracks().forEach((track) => track.stop()); this.cameraStream = null; } this.showCameraPreview = false; - logger.debug("After stopping, showCameraPreview is:", this.showCameraPreview); + logger.debug( + "After stopping, showCameraPreview is:", + this.showCameraPreview, + ); } /** @@ -366,13 +381,17 @@ export default class PhotoDialog extends Vue { const ctx = canvas.getContext("2d"); ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height); - canvas.toBlob((blob) => { - if (blob) { - this.blob = blob; - this.fileName = `photo_${Date.now()}.jpg`; - this.stopCameraPreview(); - } - }, "image/jpeg", 0.95); + canvas.toBlob( + (blob) => { + if (blob) { + this.blob = blob; + this.fileName = `photo_${Date.now()}.jpg`; + this.stopCameraPreview(); + } + }, + "image/jpeg", + 0.95, + ); } catch (error) { logger.error("Error capturing photo:", error); this.$notify( diff --git a/src/services/QRScanner/WebInlineQRScanner.ts b/src/services/QRScanner/WebInlineQRScanner.ts index a038dc22..3f71d953 100644 --- a/src/services/QRScanner/WebInlineQRScanner.ts +++ b/src/services/QRScanner/WebInlineQRScanner.ts @@ -1,4 +1,10 @@ -import { QRScannerService, ScanListener, QRScannerOptions, CameraState, CameraStateListener } from "./types"; +import { + QRScannerService, + ScanListener, + QRScannerOptions, + CameraState, + CameraStateListener, +} from "./types"; import { logger } from "@/utils/logger"; import { EventEmitter } from "events"; import jsQR from "jsqr"; @@ -22,7 +28,7 @@ export class WebInlineQRScanner implements QRScannerService { private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames private lastFrameTime = 0; private cameraStateListeners: Set = new Set(); - private currentState: CameraState = 'off'; + private currentState: CameraState = "off"; private currentStateMessage?: string; constructor(private options?: QRScannerOptions) { @@ -49,15 +55,21 @@ export class WebInlineQRScanner implements QRScannerService { private updateCameraState(state: CameraState, message?: string) { this.currentState = state; this.currentStateMessage = message; - this.cameraStateListeners.forEach(listener => { + this.cameraStateListeners.forEach((listener) => { try { listener.onStateChange(state, message); - logger.info(`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`, { - 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); + logger.error( + `[WebInlineQRScanner:${this.id}] Error in camera state listener:`, + error, + ); } }); } @@ -74,7 +86,7 @@ export class WebInlineQRScanner implements QRScannerService { async checkPermissions(): Promise { try { - this.updateCameraState('initializing', 'Checking camera permissions...'); + this.updateCameraState("initializing", "Checking camera permissions..."); logger.error( `[WebInlineQRScanner:${this.id}] Checking camera permissions...`, ); @@ -86,7 +98,7 @@ export class WebInlineQRScanner implements QRScannerService { permissions.state, ); const granted = permissions.state === "granted"; - this.updateCameraState(granted ? 'ready' : 'permission_denied'); + this.updateCameraState(granted ? "ready" : "permission_denied"); return granted; } catch (error) { logger.error( @@ -96,14 +108,17 @@ export class WebInlineQRScanner implements QRScannerService { stack: error instanceof Error ? error.stack : undefined, }, ); - this.updateCameraState('error', 'Error checking camera permissions'); + this.updateCameraState("error", "Error checking camera permissions"); return false; } } async requestPermissions(): Promise { try { - this.updateCameraState('initializing', 'Requesting camera permissions...'); + this.updateCameraState( + "initializing", + "Requesting camera permissions...", + ); logger.error( `[WebInlineQRScanner:${this.id}] Requesting camera permissions...`, ); @@ -141,8 +156,8 @@ export class WebInlineQRScanner implements QRScannerService { }, }); - this.updateCameraState('ready', 'Camera permissions granted'); - + this.updateCameraState("ready", "Camera permissions granted"); + // Stop the test stream immediately stream.getTracks().forEach((track) => { logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, { @@ -154,20 +169,35 @@ export class WebInlineQRScanner implements QRScannerService { }); return true; } catch (error) { - const wrappedError = error instanceof Error ? error : new Error(String(error)); - + 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'); + 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") { - 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'); + } 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); + this.updateCameraState("error", wrappedError.message); throw new Error(`Camera error: ${wrappedError.message}`); } } @@ -406,7 +436,7 @@ export class WebInlineQRScanner implements QRScannerService { this.isScanning = true; this.scanAttempts = 0; this.lastScanTime = Date.now(); - this.updateCameraState('initializing', 'Starting camera...'); + this.updateCameraState("initializing", "Starting camera..."); logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`); // Get camera stream @@ -421,8 +451,8 @@ export class WebInlineQRScanner implements QRScannerService { }, }); - this.updateCameraState('active', 'Camera is active'); - + this.updateCameraState("active", "Camera is active"); + logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, { tracks: this.stream.getTracks().map((t) => ({ kind: t.kind, @@ -448,15 +478,22 @@ export class WebInlineQRScanner implements QRScannerService { this.scanQRCode(); } catch (error) { this.isScanning = false; - const wrappedError = error instanceof Error ? error : new Error(String(error)); - + 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'); + if ( + wrappedError.name === "NotReadableError" || + wrappedError.name === "TrackStartError" + ) { + this.updateCameraState( + "in_use", + "Camera is in use by another application", + ); } else { - this.updateCameraState('error', wrappedError.message); + this.updateCameraState("error", wrappedError.message); } - + if (this.scanListener?.onError) { this.scanListener.onError(wrappedError); } @@ -513,8 +550,11 @@ export class WebInlineQRScanner implements QRScannerService { `[WebInlineQRScanner:${this.id}] Stream stopped event emitted`, ); } catch (error) { - logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, error); - this.updateCameraState('error', 'Error stopping camera'); + logger.error( + `[WebInlineQRScanner:${this.id}] Error stopping scan:`, + error, + ); + this.updateCameraState("error", "Error stopping camera"); throw error; } finally { this.isScanning = false; @@ -557,8 +597,11 @@ export class WebInlineQRScanner implements QRScannerService { `[WebInlineQRScanner:${this.id}] Cleanup completed successfully`, ); } catch (error) { - logger.error(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, error); - this.updateCameraState('error', 'Error during cleanup'); + 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 9d21a69c..65e56918 100644 --- a/src/services/QRScanner/types.ts +++ b/src/services/QRScanner/types.ts @@ -22,15 +22,15 @@ 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 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; diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 2643ccd1..9e545aa5 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -80,7 +80,9 @@ export class WebPlatformService implements PlatformService { */ async takePicture(): Promise { const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); - const hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); + const hasGetUserMedia = !!( + navigator.mediaDevices && navigator.mediaDevices.getUserMedia + ); // If on mobile, use file input with capture attribute (existing behavior) if (isMobile || !hasGetUserMedia) { @@ -113,107 +115,120 @@ export class WebPlatformService implements PlatformService { } // Desktop: Use getUserMedia for webcam capture - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { let stream: MediaStream | null = null; let video: HTMLVideoElement | null = null; let captureButton: HTMLButtonElement | null = null; let overlay: HTMLDivElement | null = null; - let cleanup = () => { + const cleanup = () => { if (stream) { stream.getTracks().forEach((track) => track.stop()); } if (video && video.parentNode) video.parentNode.removeChild(video); - if (captureButton && captureButton.parentNode) captureButton.parentNode.removeChild(captureButton); - if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); + if (captureButton && captureButton.parentNode) + captureButton.parentNode.removeChild(captureButton); + if (overlay && overlay.parentNode) + overlay.parentNode.removeChild(overlay); }; - try { - stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" } }); - // Create overlay for video and button - overlay = document.createElement("div"); - overlay.style.position = "fixed"; - overlay.style.top = "0"; - overlay.style.left = "0"; - overlay.style.width = "100vw"; - overlay.style.height = "100vh"; - overlay.style.background = "rgba(0,0,0,0.8)"; - overlay.style.display = "flex"; - overlay.style.flexDirection = "column"; - overlay.style.justifyContent = "center"; - overlay.style.alignItems = "center"; - overlay.style.zIndex = "9999"; - video = document.createElement("video"); - video.autoplay = true; - video.playsInline = true; - video.style.maxWidth = "90vw"; - video.style.maxHeight = "70vh"; - video.srcObject = stream; - overlay.appendChild(video); + // Move async operations inside Promise body + navigator.mediaDevices.getUserMedia({ + video: { facingMode: "user" }, + }) + .then((mediaStream) => { + stream = mediaStream; + // Create overlay for video and button + overlay = document.createElement("div"); + overlay.style.position = "fixed"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.background = "rgba(0,0,0,0.8)"; + overlay.style.display = "flex"; + overlay.style.flexDirection = "column"; + overlay.style.justifyContent = "center"; + overlay.style.alignItems = "center"; + overlay.style.zIndex = "9999"; - captureButton = document.createElement("button"); - captureButton.textContent = "Capture Photo"; - captureButton.style.marginTop = "2rem"; - captureButton.style.padding = "1rem 2rem"; - captureButton.style.fontSize = "1.2rem"; - captureButton.style.background = "#2563eb"; - captureButton.style.color = "white"; - captureButton.style.border = "none"; - captureButton.style.borderRadius = "0.5rem"; - captureButton.style.cursor = "pointer"; - overlay.appendChild(captureButton); + video = document.createElement("video"); + video.autoplay = true; + video.playsInline = true; + video.style.maxWidth = "90vw"; + video.style.maxHeight = "70vh"; + video.srcObject = stream; + overlay.appendChild(video); - document.body.appendChild(overlay); + captureButton = document.createElement("button"); + captureButton.textContent = "Capture Photo"; + captureButton.style.marginTop = "2rem"; + captureButton.style.padding = "1rem 2rem"; + captureButton.style.fontSize = "1.2rem"; + captureButton.style.background = "#2563eb"; + captureButton.style.color = "white"; + captureButton.style.border = "none"; + captureButton.style.borderRadius = "0.5rem"; + captureButton.style.cursor = "pointer"; + overlay.appendChild(captureButton); - captureButton.onclick = async () => { - try { - // Create a canvas to capture the frame - const canvas = document.createElement("canvas"); - canvas.width = video!.videoWidth; - canvas.height = video!.videoHeight; - const ctx = canvas.getContext("2d"); - ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height); - canvas.toBlob((blob) => { + document.body.appendChild(overlay); + + captureButton.onclick = () => { + try { + // Create a canvas to capture the frame + const canvas = document.createElement("canvas"); + canvas.width = video!.videoWidth; + canvas.height = video!.videoHeight; + const ctx = canvas.getContext("2d"); + ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height); + canvas.toBlob( + (blob) => { + cleanup(); + if (blob) { + resolve({ + blob, + fileName: `photo_${Date.now()}.jpg`, + }); + } else { + reject(new Error("Failed to capture image from webcam")); + } + }, + "image/jpeg", + 0.95, + ); + } catch (err) { cleanup(); - if (blob) { - resolve({ - blob, - fileName: `photo_${Date.now()}.jpg`, + reject(err); + } + }; + }) + .catch((error) => { + cleanup(); + logger.error("Error accessing webcam:", error); + // Fallback to file input + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + this.processImageFile(file) + .then((blob) => { + resolve({ + blob, + fileName: file.name || "photo.jpg", + }); + }) + .catch((error) => { + logger.error("Error processing fallback image:", error); + reject(new Error("Failed to process fallback image")); }); - } else { - reject(new Error("Failed to capture image from webcam")); - } - }, "image/jpeg", 0.95); - } catch (err) { - cleanup(); - reject(err); - } - }; - } catch (error) { - cleanup(); - logger.error("Error accessing webcam:", error); - // Fallback to file input - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - try { - const blob = await this.processImageFile(file); - resolve({ - blob, - fileName: file.name || "photo.jpg", - }); - } catch (error) { - logger.error("Error processing fallback image:", error); - reject(new Error("Failed to process fallback image")); + } else { + reject(new Error("No image selected")); } - } else { - reject(new Error("No image selected")); - } - }; - input.click(); - } + }; + input.click(); + }); }); } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index f4b55383..b97284af 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -3,7 +3,12 @@ -
+

Your Identity @@ -78,31 +83,28 @@ :icon-size="96" :profile-image-url="profileImageUrl" class="inline-block align-text-bottom border border-slate-300 rounded" - @click="showLargeIdenticonUrl = profileImageUrl" role="button" aria-label="View profile image in large size" tabindex="0" + @click="showLargeIdenticonUrl = profileImageUrl" />
- +
@@ -171,14 +176,20 @@ {{ activeDid }} - Copied + Copied
@@ -201,8 +212,8 @@ aria-live="polite" >

- Before you can publicly announce a new project or time - commitment, a friend needs to register you. + Before you can publicly announce a new project or time commitment, a + friend needs to register you.

- +
@@ -297,7 +311,9 @@ class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" aria-labelledby="searchLocationHeading" > -

Location for Searches

+

+ Location for Searches +

@@ -408,9 +424,18 @@ >

Usage Limits

-
+
Checking… - +
{{ limitsMessage }} @@ -468,9 +493,13 @@ class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer" @click="showAdvanced = !showAdvanced" > - {{ showAdvanced ? 'Hide Advanced Settings' : 'Show Advanced Settings' }} + {{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}

-
+

Advanced Settings

Beware: the features here can be confusing and even change data in ways @@ -642,8 +671,14 @@

Claim Server

-
-

Claim Server Configuration

+
+

+ Claim Server Configuration +

-

Error: {{ error }}

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

@@ -246,7 +251,7 @@ export default class ContactQRScanShow extends Vue { initializationStatus = "Initializing camera..."; useQRReader = __USE_QR_READER__; preferredCamera: "user" | "environment" = "environment"; - cameraState: CameraState = 'off'; + cameraState: CameraState = "off"; cameraStateMessage?: string; ETHR_DID_PREFIX = ETHR_DID_PREFIX; @@ -321,36 +326,36 @@ export default class ContactQRScanShow extends Vue { onStateChange: (state, message) => { this.cameraState = state; this.cameraStateMessage = message; - + // Update UI based on camera state switch (state) { - case 'in_use': + 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", + 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, - ); + }, + 5000, + ); break; - case 'not_found': + case "not_found": this.error = "No camera found"; this.isScanning = false; this.$notify( @@ -363,7 +368,7 @@ export default class ContactQRScanShow extends Vue { 5000, ); break; - case 'error': + case "error": this.error = this.cameraStateMessage || "Camera error"; this.isScanning = false; break; @@ -373,7 +378,8 @@ export default class ContactQRScanShow extends Vue { // 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.$notify( { From 85aa2981ad572ab0a3f570687d14c49fbe6e6ef0 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Tue, 20 May 2025 02:50:56 -0400 Subject: [PATCH 08/12] docs: add comprehensive camera switching implementation guide Add detailed documentation for camera switching functionality across web and mobile platforms: - Add camera management interfaces to QRScannerService - Document MLKit Barcode Scanner configuration for Capacitor - Add platform-specific implementations for iOS and Android - Include camera state management and error handling - Add performance optimization guidelines - Document testing requirements and scenarios Key additions: - Camera switching implementation for both platforms - Platform-specific considerations (iOS/Android) - Battery and memory optimization strategies - Comprehensive testing guidelines - Error handling and state management - Security and permission considerations This update provides a complete reference for implementing robust camera switching functionality in the QR code scanner. --- doc/qr-code-implementation-guide.md | 525 +++++++++++++++++++++++++++- 1 file changed, 523 insertions(+), 2 deletions(-) diff --git a/doc/qr-code-implementation-guide.md b/doc/qr-code-implementation-guide.md index bf7e448d..e6e36fcd 100644 --- a/doc/qr-code-implementation-guide.md +++ b/doc/qr-code-implementation-guide.md @@ -54,6 +54,9 @@ interface QRScannerService { addListener(listener: ScanListener): void; onStream(callback: (stream: MediaStream | null) => void): void; cleanup(): Promise; + getAvailableCameras(): Promise; + switchCamera(deviceId: string): Promise; + getCurrentCamera(): Promise; } interface ScanListener { @@ -87,7 +90,15 @@ const config: CapacitorConfig = { formats: ['QR_CODE'], detectorSize: 1.0, lensFacing: 'back', - googleBarcodeScannerModuleInstallState: true + googleBarcodeScannerModuleInstallState: true, + // Additional camera options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + sourceType: 'CAMERA', + saveToGallery: false + } } } }; @@ -281,4 +292,514 @@ export async function createBuildConfig(mode: string) { - Track memory usage - Monitor camera performance - Check error rates -- Analyze user feedback \ No newline at end of file +- Analyze user feedback + +## Camera Handling + +### Camera Switching Implementation + +The QR scanner supports camera switching on both mobile and desktop platforms through a unified interface. + +#### Platform-Specific Implementations + +1. **Mobile (Capacitor)** + - Uses `@capacitor-mlkit/barcode-scanning` + - Supports front/back camera switching + - Native camera access through platform APIs + - Optimized for mobile performance + + ```typescript + // CapacitorQRScanner.ts + async startScan(options?: QRScannerOptions): Promise { + const scanOptions: StartScanOptions = { + formats: [BarcodeFormat.QrCode], + lensFacing: options?.camera === "front" ? + LensFacing.Front : LensFacing.Back + }; + await BarcodeScanner.startScan(scanOptions); + } + ``` + +2. **Web (Desktop)** + - Uses browser's MediaDevices API + - Supports multiple camera devices + - Dynamic camera enumeration + - Real-time camera switching + + ```typescript + // WebInlineQRScanner.ts + async getAvailableCameras(): Promise { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.filter(device => device.kind === 'videoinput'); + } + + async switchCamera(deviceId: string): Promise { + // Stop current stream + await this.stopScan(); + + // Start new stream with selected camera + this.stream = await navigator.mediaDevices.getUserMedia({ + video: { + deviceId: { exact: deviceId }, + width: { ideal: 1280 }, + height: { ideal: 720 } + } + }); + + // Update video and restart scanning + if (this.video) { + this.video.srcObject = this.stream; + await this.video.play(); + } + this.scanQRCode(); + } + ``` + +### Core Interfaces + +```typescript +interface QRScannerService { + // ... existing methods ... + + /** Get available cameras */ + getAvailableCameras(): Promise; + + /** Switch to a specific camera */ + switchCamera(deviceId: string): Promise; + + /** Get current camera info */ + getCurrentCamera(): Promise; +} + +interface QRScannerOptions { + /** Camera to use ('front' or 'back' for mobile) */ + camera?: "front" | "back"; + /** Whether to show a preview of the camera feed */ + showPreview?: boolean; + /** Whether to play a sound on successful scan */ + playSound?: boolean; +} +``` + +### UI Components + +The camera switching UI adapts to the platform: + +1. **Mobile Interface** + - Simple toggle button for front/back cameras + - Positioned in bottom-right corner + - Clear visual feedback during switching + - Native camera controls + + ```vue + + ``` + +2. **Desktop Interface** + - Dropdown menu with all available cameras + - Camera labels and device IDs + - Real-time camera switching + - Responsive design + + ```vue + + ``` + +### Error Handling + +The camera switching implementation includes comprehensive error handling: + +1. **Common Error Scenarios** + - Camera in use by another application + - Permission denied during switch + - Device not available + - Stream initialization failure + - Camera switch timeout + +2. **Error Response** + ```typescript + private async handleCameraSwitch(deviceId: string): Promise { + try { + this.updateCameraState("initializing", "Switching camera..."); + await this.switchCamera(deviceId); + this.updateCameraState("active", "Camera switched successfully"); + } catch (error) { + this.updateCameraState("error", "Failed to switch camera"); + throw error; + } + } + ``` + +3. **User Feedback** + - Visual indicators during switching + - Error notifications + - Camera state updates + - Permission request dialogs + +### State Management + +The camera system maintains several states: + +1. **Camera States** + ```typescript + type CameraState = + | "initializing" // Camera is being initialized + | "ready" // Camera is ready to use + | "active" // Camera is actively streaming + | "in_use" // Camera is in use by another application + | "permission_denied" // Camera permission was denied + | "not_found" // No camera found on device + | "error" // Generic error state + | "off"; // Camera is off + ``` + +2. **State Transitions** + - Initialization → Ready + - Ready → Active + - Active → Switching + - Switching → Active/Error + - Any state → Off (on cleanup) + +### Best Practices + +1. **Camera Access** + - Always check permissions before switching + - Handle camera busy states + - Implement proper cleanup + - Monitor camera state changes + +2. **Performance** + - Optimize camera resolution + - Handle stream switching efficiently + - Manage memory usage + - Implement proper cleanup + +3. **User Experience** + - Clear visual feedback + - Smooth camera transitions + - Intuitive camera controls + - Responsive UI updates + - Accessible camera selection + +4. **Security** + - Secure camera access + - Permission management + - Device validation + - Stream security + +### Testing + +1. **Test Scenarios** + - Camera switching on both platforms + - Permission handling + - Error conditions + - Multiple camera devices + - Camera busy states + - Stream initialization + - UI responsiveness + +2. **Test Environment** + - Multiple mobile devices + - Various desktop browsers + - Different camera configurations + - Network conditions + - Permission states + +### Capacitor Implementation Details + +#### MLKit Barcode Scanner Configuration + +1. **Plugin Setup** + ```typescript + // capacitor.config.ts + const config: CapacitorConfig = { + plugins: { + MLKitBarcodeScanner: { + formats: ['QR_CODE'], + detectorSize: 1.0, + lensFacing: 'back', + googleBarcodeScannerModuleInstallState: true, + // Additional camera options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + sourceType: 'CAMERA', + saveToGallery: false + } + } + } + }; + ``` + +2. **Camera Management** + ```typescript + // CapacitorQRScanner.ts + export class CapacitorQRScanner implements QRScannerService { + private currentLensFacing: LensFacing = LensFacing.Back; + + async getAvailableCameras(): Promise { + // On mobile, we have two fixed cameras + return [ + { + deviceId: 'back', + label: 'Back Camera', + kind: 'videoinput' + }, + { + deviceId: 'front', + label: 'Front Camera', + kind: 'videoinput' + } + ] as MediaDeviceInfo[]; + } + + async switchCamera(deviceId: string): Promise { + if (!this.isScanning) return; + + const newLensFacing = deviceId === 'front' ? + LensFacing.Front : LensFacing.Back; + + // Stop current scan + await this.stopScan(); + + // Update lens facing + this.currentLensFacing = newLensFacing; + + // Restart scan with new camera + await this.startScan({ + camera: deviceId as 'front' | 'back' + }); + } + + async getCurrentCamera(): Promise { + return { + deviceId: this.currentLensFacing === LensFacing.Front ? 'front' : 'back', + label: this.currentLensFacing === LensFacing.Front ? + 'Front Camera' : 'Back Camera', + kind: 'videoinput' + } as MediaDeviceInfo; + } + } + ``` + +3. **Camera State Management** + ```typescript + // CapacitorQRScanner.ts + private async handleCameraState(): Promise { + try { + // Check if camera is available + const { camera } = await BarcodeScanner.checkPermissions(); + + if (camera === 'denied') { + this.updateCameraState('permission_denied'); + return; + } + + // Check if camera is in use + const isInUse = await this.isCameraInUse(); + if (isInUse) { + this.updateCameraState('in_use'); + return; + } + + this.updateCameraState('ready'); + } catch (error) { + this.updateCameraState('error', error.message); + } + } + + private async isCameraInUse(): Promise { + try { + // Try to start a test scan + await BarcodeScanner.startScan({ + formats: [BarcodeFormat.QrCode], + lensFacing: this.currentLensFacing + }); + // If successful, stop it immediately + await BarcodeScanner.stopScan(); + return false; + } catch (error) { + return error.message.includes('camera in use'); + } + } + ``` + +4. **Error Handling** + ```typescript + // CapacitorQRScanner.ts + private async handleCameraError(error: Error): Promise { + switch (error.name) { + case 'CameraPermissionDenied': + this.updateCameraState('permission_denied'); + break; + case 'CameraInUse': + this.updateCameraState('in_use'); + break; + case 'CameraUnavailable': + this.updateCameraState('not_found'); + break; + default: + this.updateCameraState('error', error.message); + } + } + ``` + +#### Platform-Specific Considerations + +1. **iOS Implementation** + - Camera permissions in Info.plist + - Privacy descriptions + - Camera usage description + - Background camera access + + ```xml + + NSCameraUsageDescription + We need access to your camera to scan QR codes + NSPhotoLibraryUsageDescription + We need access to save scanned QR codes + ``` + +2. **Android Implementation** + - Camera permissions in AndroidManifest.xml + - Runtime permission handling + - Camera features declaration + - Hardware feature requirements + + ```xml + + + + + ``` + +3. **Platform-Specific Features** + - iOS: Camera orientation handling + - Android: Camera resolution optimization + - Both: Battery usage optimization + - Both: Memory management + + ```typescript + // Platform-specific optimizations + private getPlatformSpecificOptions(): StartScanOptions { + const baseOptions: StartScanOptions = { + formats: [BarcodeFormat.QrCode], + lensFacing: this.currentLensFacing + }; + + if (Capacitor.getPlatform() === 'ios') { + return { + ...baseOptions, + // iOS-specific options + cameraOptions: { + quality: 0.7, // Lower quality for better performance + allowEditing: false, + resultType: 'uri' + } + }; + } else if (Capacitor.getPlatform() === 'android') { + return { + ...baseOptions, + // Android-specific options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + saveToGallery: false + } + }; + } + + return baseOptions; + } + ``` + +#### Performance Optimization + +1. **Battery Usage** + ```typescript + // CapacitorQRScanner.ts + private optimizeBatteryUsage(): void { + // Reduce scan frequency when battery is low + if (this.isLowBattery()) { + this.scanInterval = 2000; // 2 seconds between scans + } else { + this.scanInterval = 1000; // 1 second between scans + } + } + + private isLowBattery(): boolean { + // Check battery level if available + if (Capacitor.isPluginAvailable('Battery')) { + const { level } = await Battery.getBatteryLevel(); + return level < 0.2; // 20% or lower + } + return false; + } + ``` + +2. **Memory Management** + ```typescript + // CapacitorQRScanner.ts + private async cleanupResources(): Promise { + // Stop scanning + await this.stopScan(); + + // Clear any stored camera data + this.currentLensFacing = LensFacing.Back; + + // Remove listeners + this.listenerHandles.forEach(handle => handle()); + this.listenerHandles = []; + + // Reset state + this.isScanning = false; + this.updateCameraState('off'); + } + ``` + +#### Testing on Capacitor + +1. **Device Testing** + - Test on multiple iOS devices + - Test on multiple Android devices + - Test different camera configurations + - Test with different screen sizes + - Test with different OS versions + +2. **Camera Testing** + - Test front camera switching + - Test back camera switching + - Test camera permissions + - Test camera in use scenarios + - Test low light conditions + - Test different QR code sizes + - Test different QR code distances + +3. **Performance Testing** + - Battery usage monitoring + - Memory usage monitoring + - Camera switching speed + - QR code detection speed + - App responsiveness + - Background/foreground transitions \ No newline at end of file From 7b3b1c930e375362ffaa83b71b501d2ff9fa4872 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Tue, 20 May 2025 03:15:23 -0400 Subject: [PATCH 09/12] refactor: consolidate type system and improve documentation - Move type definitions from src/types/ to src/interfaces/ for better organization - Enhance deep linking type system documentation with detailed examples - Update package dependencies to latest versions - Improve code organization in README.md - Fix formatting in WebPlatformService.ts This change consolidates all type definitions into the interfaces folder, improves type safety documentation, and updates dependencies for better maintainability. The deep linking system now has clearer documentation about its type system and validation approach. Breaking: Removes src/types/ directory in favor of src/interfaces/ --- .../rules/architectural_decision_record.mdc | 2 +- README.md | 18 ++ doc/DEEP_LINKS.md | 78 ++++- package-lock.json | 275 +++++++++--------- src/interfaces/deepLinks.ts | 103 ++++++- src/interfaces/give.ts | 21 ++ src/services/deepLinks.ts | 4 +- src/services/platforms/WebPlatformService.ts | 7 +- src/types/deepLinks.ts | 103 ------- src/types/index.ts | 25 -- src/views/DeepLinkErrorView.vue | 2 +- 11 files changed, 356 insertions(+), 282 deletions(-) create mode 100644 src/interfaces/give.ts delete mode 100644 src/types/deepLinks.ts delete mode 100644 src/types/index.ts diff --git a/.cursor/rules/architectural_decision_record.mdc b/.cursor/rules/architectural_decision_record.mdc index 06ed4d9f..a013a3e1 100644 --- a/.cursor/rules/architectural_decision_record.mdc +++ b/.cursor/rules/architectural_decision_record.mdc @@ -31,7 +31,7 @@ src/ ├── electron/ # Electron-specific code ├── constants/ # Application constants ├── db/ # Database related code -├── interfaces/ # TypeScript interfaces +├── interfaces/ # TypeScript interfaces and type definitions └── assets/ # Static assets ``` diff --git a/README.md b/README.md index 3139bb57..9f3f2bd1 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,24 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib * If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",` +### Code Organization + +The project uses a centralized approach to type definitions and interfaces: + +* `src/interfaces/` - Contains all TypeScript interfaces and type definitions + * `deepLinks.ts` - Deep linking type system and Zod validation schemas + * `give.ts` - Give-related interfaces and type definitions + * `claims.ts` - Claim-related interfaces and verifiable credentials + * `common.ts` - Shared interfaces and utility types + * Other domain-specific interface files + +Key principles: +- All interfaces and types are defined in the interfaces folder +- Zod schemas are used for runtime validation and type generation +- Domain-specific interfaces are separated into their own files +- Common interfaces are shared through `common.ts` +- Type definitions are generated from Zod schemas where possible + ### Kudos Gifts make the world go 'round! diff --git a/doc/DEEP_LINKS.md b/doc/DEEP_LINKS.md index ba6f728d..a68a5ed1 100644 --- a/doc/DEEP_LINKS.md +++ b/doc/DEEP_LINKS.md @@ -9,21 +9,95 @@ The deep linking system uses a multi-layered type safety approach: - Enforces parameter requirements - Sanitizes input data - Provides detailed validation errors + - Generates TypeScript types automatically 2. **TypeScript Types** - - Generated from Zod schemas + - Generated from Zod schemas using `z.infer` - Ensures compile-time type safety - Provides IDE autocompletion - Catches type errors during development + - Maintains single source of truth for types 3. **Router Integration** - Type-safe parameter passing - Route-specific parameter validation - Query parameter type checking + - Automatic type inference for route parameters + +## Type System Implementation + +### Zod Schema to TypeScript Type Generation + +```typescript +// Define the schema +const claimSchema = z.object({ + id: z.string(), + view: z.enum(["details", "certificate", "raw"]).optional() +}); + +// TypeScript type is automatically generated +type ClaimParams = z.infer; +// Equivalent to: +// type ClaimParams = { +// id: string; +// view?: "details" | "certificate" | "raw"; +// } +``` + +### Type Safety Layers + +1. **Schema Definition** + ```typescript + // src/interfaces/deepLinks.ts + export const deepLinkSchemas = { + claim: z.object({ + id: z.string(), + view: z.enum(["details", "certificate", "raw"]).optional() + }), + // Other route schemas... + }; + ``` + +2. **Type Generation** + ```typescript + // Types are automatically generated from schemas + export type DeepLinkParams = { + [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; + }; + ``` + +3. **Runtime Validation** + ```typescript + // In DeepLinkHandler + const result = deepLinkSchemas.claim.safeParse(params); + if (!result.success) { + // Handle validation errors + console.error(result.error); + } + ``` + +### Error Handling Types + +```typescript +export interface DeepLinkError extends Error { + code: string; + details?: unknown; +} + +// Usage in error handling +try { + await handler.handleDeepLink(url); +} catch (error) { + if (error instanceof DeepLinkError) { + // Type-safe error handling + console.error(error.code, error.message); + } +} +``` ## Implementation Files -- `src/types/deepLinks.ts`: Type definitions and validation schemas +- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas - `src/services/deepLinks.ts`: Deep link processing service - `src/main.capacitor.ts`: Capacitor integration diff --git a/package-lock.json b/package-lock.json index 96fc5099..1ad0b837 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6465,9 +6465,9 @@ } }, "node_modules/@ipld/dag-pb/node_modules/multiformats": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz", - "integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ==" + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz", + "integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -7610,9 +7610,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", "cpu": [ "arm" ], @@ -7623,9 +7623,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", "cpu": [ "arm64" ], @@ -7636,9 +7636,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", "cpu": [ "arm64" ], @@ -7649,9 +7649,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", "cpu": [ "x64" ], @@ -7662,9 +7662,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", "cpu": [ "arm64" ], @@ -7675,9 +7675,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", "cpu": [ "x64" ], @@ -7688,9 +7688,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", "cpu": [ "arm" ], @@ -7701,9 +7701,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", "cpu": [ "arm" ], @@ -7714,9 +7714,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", "cpu": [ "arm64" ], @@ -7727,9 +7727,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", + "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", "cpu": [ "arm64" ], @@ -7740,9 +7740,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", "cpu": [ "loong64" ], @@ -7753,9 +7753,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", "cpu": [ "ppc64" ], @@ -7766,9 +7766,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", "cpu": [ "riscv64" ], @@ -7779,9 +7779,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", "cpu": [ "riscv64" ], @@ -7792,9 +7792,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", "cpu": [ "s390x" ], @@ -7805,9 +7805,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", + "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", "cpu": [ "x64" ], @@ -7818,9 +7818,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", + "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", "cpu": [ "x64" ], @@ -7831,9 +7831,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", "cpu": [ "arm64" ], @@ -7844,9 +7844,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", "cpu": [ "ia32" ], @@ -7857,9 +7857,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", "cpu": [ "x64" ], @@ -8737,9 +8737,9 @@ } }, "node_modules/@types/leaflet": { - "version": "1.9.17", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz", - "integrity": "sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==", + "version": "1.9.18", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.18.tgz", + "integrity": "sha512-ht2vsoPjezor5Pmzi5hdsA7F++v5UGq9OlUduWHmMZiuQGIpJ2WS5+Gg9HaAA79gNh1AIPtCqhzejcIZ3lPzXQ==", "devOptional": true, "dependencies": { "@types/geojson": "*" @@ -8764,9 +8764,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.47", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.47.tgz", - "integrity": "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==", + "version": "20.17.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.48.tgz", + "integrity": "sha512-KpSfKOHPsiSC4IkZeu2LsusFwExAIVGkhG1KkbaBMLwau0uMhj0fCrvyg9ddM2sAvd+gtiBJLir4LAw1MNMIaw==", "dependencies": { "undici-types": "~6.19.2" } @@ -9461,9 +9461,9 @@ } }, "node_modules/@veramo/did-provider-peer/node_modules/did-jwt-vc": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/did-jwt-vc/-/did-jwt-vc-4.0.12.tgz", - "integrity": "sha512-xhQ8tY6tanrgzkhKmoSt3A/XkInufMo73qSJU1cXWxfYpMpYYmldvaxvJm2nqMjCly276ajP6LNeXgkYg9elRw==", + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/did-jwt-vc/-/did-jwt-vc-4.0.13.tgz", + "integrity": "sha512-T1IUneS7Rgpao8dOeZy7dMUvAvcLLn7T8YlWRk/8HsEpaVLDx5NrjRfbfDJU8FL8CI8aBIAhoDnPQO3PNV+BWg==", "dependencies": { "did-jwt": "^8.0.0", "did-resolver": "^4.1.0" @@ -9486,9 +9486,9 @@ } }, "node_modules/@veramo/did-provider-peer/node_modules/multiformats": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz", - "integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ==" + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz", + "integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w==" }, "node_modules/@veramo/did-resolver": { "version": "5.6.0", @@ -13901,9 +13901,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.154", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.154.tgz", - "integrity": "sha512-G4VCFAyKbp1QJ+sWdXYIRYsPGvlV5sDACfCmoMFog3rjm1syLhI41WXm/swZypwCIWIm4IFLWzHY14joWMQ5Fw==", + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", "devOptional": true }, "node_modules/elementtree": { @@ -14568,9 +14568,9 @@ } }, "node_modules/ethers": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.0.tgz", - "integrity": "sha512-KgHwltNSMdbrGWEyKkM0Rt2s+u1nDH/5BVDQakLinzGEJi4bWindBzZSCC4gKsbZjwDTI6ex/8suR9Ihbmz4IQ==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.1.tgz", + "integrity": "sha512-JnFiPFi3sK2Z6y7jZ3qrafDMwiXmU+6cNZ0M+kPq+mTy9skqEzwqAdFW3nb/em2xjlIVXX6Lz8ID6i3LmS4+fQ==", "funding": [ { "type": "individual", @@ -14653,9 +14653,9 @@ "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==" }, "node_modules/ethr-did": { - "version": "3.0.34", - "resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.34.tgz", - "integrity": "sha512-0NloieyCPi6iRebLRufFns00sRZJ46GB+Oc/thu3hqIc/7rOUQjNEQmUbSTo2OTEIW3FOIuaAVo2eh58HQ9SwA==", + "version": "3.0.35", + "resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.35.tgz", + "integrity": "sha512-vWTGIcdnzyTeahNw25P4eQEMo6gVQEVEg0Kit8spDPB6neUAk5HaJXfxG9i8gKPJBOgyVNkMQ/aPOgVhnSig3w==", "dependencies": { "did-jwt": "^8.0.0", "did-resolver": "^4.1.0", @@ -21089,9 +21089,9 @@ "optional": true }, "node_modules/papaparse": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz", - "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==" + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -21750,9 +21750,9 @@ } }, "node_modules/protons-runtime/node_modules/multiformats": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz", - "integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ==" + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz", + "integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w==" }, "node_modules/protons-runtime/node_modules/uint8arrays": { "version": "5.1.0", @@ -23241,9 +23241,9 @@ } }, "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", + "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "dev": true, "dependencies": { "@types/estree": "1.0.7" @@ -23256,26 +23256,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.41.0", + "@rollup/rollup-android-arm64": "4.41.0", + "@rollup/rollup-darwin-arm64": "4.41.0", + "@rollup/rollup-darwin-x64": "4.41.0", + "@rollup/rollup-freebsd-arm64": "4.41.0", + "@rollup/rollup-freebsd-x64": "4.41.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", + "@rollup/rollup-linux-arm-musleabihf": "4.41.0", + "@rollup/rollup-linux-arm64-gnu": "4.41.0", + "@rollup/rollup-linux-arm64-musl": "4.41.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-gnu": "4.41.0", + "@rollup/rollup-linux-riscv64-musl": "4.41.0", + "@rollup/rollup-linux-s390x-gnu": "4.41.0", + "@rollup/rollup-linux-x64-gnu": "4.41.0", + "@rollup/rollup-linux-x64-musl": "4.41.0", + "@rollup/rollup-win32-arm64-msvc": "4.41.0", + "@rollup/rollup-win32-ia32-msvc": "4.41.0", + "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" } }, @@ -25050,13 +25050,12 @@ } }, "node_modules/synckit": { - "version": "0.11.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.5.tgz", - "integrity": "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==", + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz", + "integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==", "dev": true, "dependencies": { - "@pkgr/core": "^0.2.4", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -25065,12 +25064,6 @@ "url": "https://opencollective.com/synckit" } }, - "node_modules/synckit/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -25267,13 +25260,13 @@ } }, "node_modules/terser": { - "version": "5.39.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz", - "integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "devOptional": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -25989,9 +25982,9 @@ } }, "node_modules/uint8-varint/node_modules/multiformats": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz", - "integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ==" + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz", + "integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w==" }, "node_modules/uint8-varint/node_modules/uint8arrays": { "version": "5.1.0", @@ -26010,9 +26003,9 @@ } }, "node_modules/uint8arraylist/node_modules/multiformats": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz", - "integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ==" + "version": "13.3.5", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz", + "integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w==" }, "node_modules/uint8arraylist/node_modules/uint8arrays": { "version": "5.1.0", @@ -27445,15 +27438,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { @@ -27601,9 +27594,9 @@ } }, "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "version": "3.25.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.7.tgz", + "integrity": "sha512-YGdT1cVRmKkOg6Sq7vY7IkxdphySKnXhaUmFI4r4FcuFVNgpCb9tZfNwXbT6BPjD5oz0nubFsoo9pIqKrDcCvg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index a708a321..1e06aab1 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -1,11 +1,106 @@ /** - * @file Deep Link Interface Definitions + * @file Deep Link Type Definitions and Validation Schemas * @author Matthew Raymer * - * Defines the core interfaces for the deep linking system. - * These interfaces are used across the deep linking implementation - * to ensure type safety and consistent error handling. + * This file defines the type system and validation schemas for deep linking in the TimeSafari app. + * It uses Zod for runtime validation while providing TypeScript types for compile-time checking. + * + * Type Strategy: + * 1. Define base URL schema to validate the fundamental deep link structure + * 2. Define route-specific parameter schemas with exact validation rules + * 3. Generate TypeScript types from Zod schemas for type safety + * 4. Export both schemas and types for use in deep link handling + * + * Usage: + * - Import schemas for runtime validation in deep link handlers + * - Import types for type-safe parameter handling in components + * - Use DeepLinkParams type for type-safe access to route parameters + * + * @example + * // Runtime validation + * const params = deepLinkSchemas.claim.parse({ id: "123", view: "details" }); + * + * // Type-safe parameter access + * function handleClaimParams(params: DeepLinkParams["claim"]) { + * // TypeScript knows params.id exists and params.view is optional + * } */ +import { z } from "zod"; + +// Add a union type of all valid route paths +export const VALID_DEEP_LINK_ROUTES = [ + "user-profile", + "project-details", + "onboard-meeting-setup", + "invite-one-accept", + "contact-import", + "confirm-gift", + "claim", + "claim-cert", + "claim-add-raw", + "contact-edit", + "contacts", + "did", +] as const; + +// Create a type from the array +export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; + +// Update your schema definitions to use this type +export const baseUrlSchema = z.object({ + scheme: z.literal("timesafari"), + path: z.string(), + queryParams: z.record(z.string()).optional(), +}); + +// Use the type to ensure route validation +export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES); + +// Parameter validation schemas for each route type +export const deepLinkSchemas = { + "user-profile": z.object({ + id: z.string(), + }), + "project-details": z.object({ + id: z.string(), + }), + "onboard-meeting-setup": z.object({ + id: z.string(), + }), + "invite-one-accept": z.object({ + id: z.string(), + }), + "contact-import": z.object({ + jwt: z.string(), + }), + "confirm-gift": z.object({ + id: z.string(), + }), + claim: z.object({ + id: z.string(), + }), + "claim-cert": z.object({ + id: z.string(), + }), + "claim-add-raw": z.object({ + id: z.string(), + claim: z.string().optional(), + claimJwtId: z.string().optional(), + }), + "contact-edit": z.object({ + did: z.string(), + }), + contacts: z.object({ + contacts: z.string(), // JSON string of contacts array + }), + did: z.object({ + did: z.string(), + }), +}; + +export type DeepLinkParams = { + [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; +}; export interface DeepLinkError extends Error { code: string; diff --git a/src/interfaces/give.ts b/src/interfaces/give.ts new file mode 100644 index 00000000..241955c7 --- /dev/null +++ b/src/interfaces/give.ts @@ -0,0 +1,21 @@ +import { GiveSummaryRecord } from "./records"; + +// Common interface for contact information +export interface ContactInfo { + known: boolean; + displayName: string; + profileImageUrl?: string; +} + +// Define the contact information fields +interface GiveContactInfo { + giver: ContactInfo; + issuer: ContactInfo; + receiver: ContactInfo; + providerPlanName?: string; + recipientProjectName?: string; + image?: string; +} + +// Combine GiveSummaryRecord with contact information using intersection type +export type GiveRecordWithContactInfo = GiveSummaryRecord & GiveContactInfo; diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index ac8d480f..f745c2b0 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -7,7 +7,7 @@ * * Architecture: * 1. DeepLinkHandler class encapsulates all deep link processing logic - * 2. Uses Zod schemas from types/deepLinks for parameter validation + * 2. Uses Zod schemas from interfaces/deepLinks for parameter validation * 3. Provides consistent error handling and logging * 4. Maps validated parameters to Vue router calls * @@ -51,7 +51,7 @@ import { baseUrlSchema, routeSchema, DeepLinkRoute, -} from "../types/deepLinks"; +} from "../interfaces/deepLinks"; import { logConsoleAndDb } from "../db"; import type { DeepLinkError } from "../interfaces/deepLinks"; diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 9e545aa5..7f09c4d3 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -132,9 +132,10 @@ export class WebPlatformService implements PlatformService { }; // Move async operations inside Promise body - navigator.mediaDevices.getUserMedia({ - video: { facingMode: "user" }, - }) + navigator.mediaDevices + .getUserMedia({ + video: { facingMode: "user" }, + }) .then((mediaStream) => { stream = mediaStream; // Create overlay for video and button diff --git a/src/types/deepLinks.ts b/src/types/deepLinks.ts deleted file mode 100644 index 0c046045..00000000 --- a/src/types/deepLinks.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @file Deep Link Type Definitions and Validation Schemas - * @author Matthew Raymer - * - * This file defines the type system and validation schemas for deep linking in the TimeSafari app. - * It uses Zod for runtime validation while providing TypeScript types for compile-time checking. - * - * Type Strategy: - * 1. Define base URL schema to validate the fundamental deep link structure - * 2. Define route-specific parameter schemas with exact validation rules - * 3. Generate TypeScript types from Zod schemas for type safety - * 4. Export both schemas and types for use in deep link handling - * - * Usage: - * - Import schemas for runtime validation in deep link handlers - * - Import types for type-safe parameter handling in components - * - Use DeepLinkParams type for type-safe access to route parameters - * - * @example - * // Runtime validation - * const params = deepLinkSchemas.claim.parse({ id: "123", view: "details" }); - * - * // Type-safe parameter access - * function handleClaimParams(params: DeepLinkParams["claim"]) { - * // TypeScript knows params.id exists and params.view is optional - * } - */ -import { z } from "zod"; - -// Add a union type of all valid route paths -export const VALID_DEEP_LINK_ROUTES = [ - "user-profile", - "project-details", - "onboard-meeting-setup", - "invite-one-accept", - "contact-import", - "confirm-gift", - "claim", - "claim-cert", - "claim-add-raw", - "contact-edit", - "contacts", - "did", -] as const; - -// Create a type from the array -export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; - -// Update your schema definitions to use this type -export const baseUrlSchema = z.object({ - scheme: z.literal("timesafari"), - path: z.string(), - queryParams: z.record(z.string()).optional(), -}); - -// Use the type to ensure route validation -export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES); - -// Parameter validation schemas for each route type -export const deepLinkSchemas = { - "user-profile": z.object({ - id: z.string(), - }), - "project-details": z.object({ - id: z.string(), - }), - "onboard-meeting-setup": z.object({ - id: z.string(), - }), - "invite-one-accept": z.object({ - id: z.string(), - }), - "contact-import": z.object({ - jwt: z.string(), - }), - "confirm-gift": z.object({ - id: z.string(), - }), - claim: z.object({ - id: z.string(), - }), - "claim-cert": z.object({ - id: z.string(), - }), - "claim-add-raw": z.object({ - id: z.string(), - claim: z.string().optional(), - claimJwtId: z.string().optional(), - }), - "contact-edit": z.object({ - did: z.string(), - }), - contacts: z.object({ - contacts: z.string(), // JSON string of contacts array - }), - did: z.object({ - did: z.string(), - }), -}; - -export type DeepLinkParams = { - [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; -}; diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 4aaeaa12..00000000 --- a/src/types/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GiveSummaryRecord, GiveVerifiableCredential } from "../interfaces"; - -export interface GiveRecordWithContactInfo extends GiveSummaryRecord { - jwtId: string; - fullClaim: GiveVerifiableCredential; - giver: { - known: boolean; - displayName: string; - profileImageUrl?: string; - }; - issuer: { - known: boolean; - displayName: string; - profileImageUrl?: string; - }; - receiver: { - known: boolean; - displayName: string; - profileImageUrl?: string; - }; - providerPlanName?: string; - recipientProjectName?: string; - description: string; - image?: string; -} diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue index 9f2be5cc..3f404b87 100644 --- a/src/views/DeepLinkErrorView.vue +++ b/src/views/DeepLinkErrorView.vue @@ -41,7 +41,7 @@ @@ -191,5 +548,9 @@ export default class ImageMethodDialog extends Vue { border-radius: 0.5rem; width: 100%; max-width: 700px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; } From 981ccbf2693351e2d77bb493b620cf5b6705e6d6 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 20 May 2025 21:33:15 +0800 Subject: [PATCH 11/12] iOS photo library permission --- ios/App/App/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 23d09c00..08c0cee2 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -47,5 +47,7 @@ NSCameraUsageDescription This app uses the camera to scan QR codes and capture photos. + NSPhotoLibraryUsageDescription + This app needs access to your photo library to save and select photos. From 141415977edf39b0e8dec32c90b66785ab7ede9c Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 20 May 2025 21:35:42 +0800 Subject: [PATCH 12/12] Fix linting errors --- src/components/ImageMethodDialog.vue | 47 +++++++++++++++++----------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index cef2dfeb..0e56ee75 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -2,10 +2,7 @@
-

+

Uploading Image… Crop Image Upload Image @@ -29,7 +26,10 @@ Take a photo with your camera

-
+
-
+