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/.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/README.md b/README.md index f64f6659..f8d6ca21 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,24 @@ To add a Font Awesome icon, add to main.ts and reference with `font-awesome` ele * 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/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/docs/DEEP_LINKS.md b/doc/DEEP_LINKS.md similarity index 52% rename from docs/DEEP_LINKS.md rename to doc/DEEP_LINKS.md index ba6f728d..a68a5ed1 100644 --- a/docs/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/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/doc/qr-code-implementation-guide.md b/doc/qr-code-implementation-guide.md new file mode 100644 index 00000000..e6e36fcd --- /dev/null +++ b/doc/qr-code-implementation-guide.md @@ -0,0 +1,805 @@ +# 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; + getAvailableCameras(): Promise; + switchCamera(deviceId: string): Promise; + getCurrentCamera(): 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, + // Additional camera options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + sourceType: 'CAMERA', + saveToGallery: false + } + } + } +}; +``` + +#### Web +- Uses browser's MediaDevices API +- Vue.js components for UI +- EventEmitter for stream management +- Browser-based camera access +- Inline camera preview +- Responsive design +- Cross-browser compatibility + +### View Components + +#### ContactQRScanView +- Dedicated view for scanning QR codes +- Full-screen camera interface +- Simple UI focused on scanning +- Used primarily on native platforms +- Streamlined scanning experience + +#### ContactQRScanShowView +- Combined view for QR code display and scanning +- Shows user's own QR code +- Handles user registration status +- Provides options to copy contact information +- Platform-specific scanning implementation: + - Native: Button to navigate to ContactQRScanView + - Web: Built-in scanning functionality + +### QR Code Workflow + +1. **Initiation** + - User selects "Scan QR Code" option + - Platform-specific scanner is initialized + - Camera permissions are verified + - Appropriate scanner component is loaded + +2. **Platform-Specific Implementation** + - Web: Uses `qrcode-stream` for real-time scanning + - Native: Uses `@capacitor-mlkit/barcode-scanning` + +3. **Scanning Process** + - Camera stream initialization + - Real-time frame analysis + - QR code detection and decoding + - Validation of QR code format + - Processing of contact information + +4. **Contact Processing** + - Decryption of contact data + - Validation of user information + - Verification of timestamp + - Check for duplicate contacts + - Processing of shared data + +## Build Configuration + +### Common Vite Configuration +```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 + +## 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 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/docs/camera-implementation.md b/docs/camera-implementation.md deleted file mode 100644 index bd00e268..00000000 --- a/docs/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/package-lock.json b/package-lock.json index e0864587..fce02999 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/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/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 diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 6585b2b5..0e56ee75 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -1,77 +1,181 @@ @@ -189,5 +557,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; } diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index 65d41e6b..651ef9ac 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() { + logger.log("PhotoDialog mounted"); try { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.isRegistered = !!settings.isRegistered; + logger.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,12 @@ 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 +258,154 @@ 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 +462,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 +612,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/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/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 99% rename from src/lib/fontawesome.ts rename to src/libs/fontawesome.ts index 181bcb15..37b5343c 100644 --- a/src/lib/fontawesome.ts +++ b/src/libs/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, 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"; diff --git a/src/services/QRScanner/WebInlineQRScanner.ts b/src/services/QRScanner/WebInlineQRScanner.ts index e7560415..3f71d953 100644 --- a/src/services/QRScanner/WebInlineQRScanner.ts +++ b/src/services/QRScanner/WebInlineQRScanner.ts @@ -1,4 +1,10 @@ -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 +27,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 +52,41 @@ 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 +97,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 +108,17 @@ 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,9 +156,7 @@ 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) => { @@ -124,25 +171,19 @@ export class WebInlineQRScanner implements QRScannerService { } 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 + // 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" ) { + this.updateCameraState("permission_denied", "Camera access denied"); throw new Error( "Camera access denied. Please grant camera permission and try again", ); @@ -150,8 +191,13 @@ export class WebInlineQRScanner implements QRScannerService { 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 +436,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 +451,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, @@ -431,11 +480,20 @@ export class WebInlineQRScanner implements QRScannerService { 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, - }); + + // 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 +550,12 @@ 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 +597,12 @@ 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..65e56918 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/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 2643ccd1..7f09c4d3 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,121 @@ 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/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/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 +

- @@ -202,6 +213,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 +251,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 +316,71 @@ 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.isScanning = false; - this.isInitializing = false; this.$notify( { group: "alert", @@ -328,40 +393,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 +864,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 +879,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 +910,7 @@ export default class ContactQRScanShow extends Vue { this.handleError(error); } finally { this.isScanning = false; - this.cameraStatus = "Active"; + this.cameraState = "active"; } } @@ -897,12 +933,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, 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 @@