forked from trent_larson/crowd-funder-for-time-pwa
Merge branch 'qrcode-reboot' into trent-tweaks
This commit is contained in:
@@ -31,7 +31,7 @@ src/
|
|||||||
├── electron/ # Electron-specific code
|
├── electron/ # Electron-specific code
|
||||||
├── constants/ # Application constants
|
├── constants/ # Application constants
|
||||||
├── db/ # Database related code
|
├── db/ # Database related code
|
||||||
├── interfaces/ # TypeScript interfaces
|
├── interfaces/ # TypeScript interfaces and type definitions
|
||||||
└── assets/ # Static assets
|
└── assets/ # Static assets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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<boolean>;
|
|
||||||
requestPermissions(): Promise<boolean>;
|
|
||||||
isSupported(): Promise<boolean>;
|
|
||||||
startScan(options?: QRScannerOptions): Promise<void>;
|
|
||||||
stopScan(): Promise<void>;
|
|
||||||
addListener(listener: ScanListener): void;
|
|
||||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
|
||||||
cleanup(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScanListener {
|
|
||||||
onScan: (result: string) => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QRScannerOptions {
|
|
||||||
camera?: "front" | "back";
|
|
||||||
showPreview?: boolean;
|
|
||||||
playSound?: boolean;
|
|
||||||
}
|
|
||||||
@@ -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<boolean>;
|
|
||||||
requestPermissions(): Promise<boolean>;
|
|
||||||
isSupported(): Promise<boolean>;
|
|
||||||
startScan(options?: QRScannerOptions): Promise<void>;
|
|
||||||
stopScan(): Promise<void>;
|
|
||||||
addListener(listener: ScanListener): void;
|
|
||||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
|
||||||
cleanup(): Promise<void>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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<void> {
|
|
||||||
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<void>> = [];
|
|
||||||
private cleanupPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
async checkPermissions(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const { camera } = await BarcodeScanner.checkPermissions();
|
|
||||||
return camera === "granted";
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking camera permissions:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestPermissions(): Promise<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
try {
|
|
||||||
const { supported } = await BarcodeScanner.isSupported();
|
|
||||||
return supported;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking scanner support:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async startScan(options?: QRScannerOptions): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
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<boolean> {
|
|
||||||
return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
|
|
||||||
}
|
|
||||||
|
|
||||||
async startScan(): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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
|
|
||||||
18
README.md
18
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/",`
|
* 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
|
### Kudos
|
||||||
|
|
||||||
Gifts make the world go 'round!
|
Gifts make the world go 'round!
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"appId": "com.brownspank.timesafari",
|
"appId": "app.timesafari",
|
||||||
"appName": "TimeSafari",
|
"appName": "TimeSafari",
|
||||||
"webDir": "dist",
|
"webDir": "dist",
|
||||||
"bundledWebRuntime": false
|
"bundledWebRuntime": false,
|
||||||
|
"server": {
|
||||||
|
"cleartext": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"App": {
|
||||||
|
"appUrlOpen": {
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"url": "timesafari://*",
|
||||||
|
"autoVerify": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,95 @@ The deep linking system uses a multi-layered type safety approach:
|
|||||||
- Enforces parameter requirements
|
- Enforces parameter requirements
|
||||||
- Sanitizes input data
|
- Sanitizes input data
|
||||||
- Provides detailed validation errors
|
- Provides detailed validation errors
|
||||||
|
- Generates TypeScript types automatically
|
||||||
|
|
||||||
2. **TypeScript Types**
|
2. **TypeScript Types**
|
||||||
- Generated from Zod schemas
|
- Generated from Zod schemas using `z.infer`
|
||||||
- Ensures compile-time type safety
|
- Ensures compile-time type safety
|
||||||
- Provides IDE autocompletion
|
- Provides IDE autocompletion
|
||||||
- Catches type errors during development
|
- Catches type errors during development
|
||||||
|
- Maintains single source of truth for types
|
||||||
|
|
||||||
3. **Router Integration**
|
3. **Router Integration**
|
||||||
- Type-safe parameter passing
|
- Type-safe parameter passing
|
||||||
- Route-specific parameter validation
|
- Route-specific parameter validation
|
||||||
- Query parameter type checking
|
- 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<typeof claimSchema>;
|
||||||
|
// 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
|
## 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/services/deepLinks.ts`: Deep link processing service
|
||||||
- `src/main.capacitor.ts`: Capacitor integration
|
- `src/main.capacitor.ts`: Capacitor integration
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
JWT Creation & Verification
|
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
|
Prerequisites: openssl, jq
|
||||||
|
|
||||||
805
doc/qr-code-implementation-guide.md
Normal file
805
doc/qr-code-implementation-guide.md
Normal file
@@ -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<boolean>;
|
||||||
|
requestPermissions(): Promise<boolean>;
|
||||||
|
isSupported(): Promise<boolean>;
|
||||||
|
startScan(options?: QRScannerOptions): Promise<void>;
|
||||||
|
stopScan(): Promise<void>;
|
||||||
|
addListener(listener: ScanListener): void;
|
||||||
|
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||||
|
cleanup(): Promise<void>;
|
||||||
|
getAvailableCameras(): Promise<MediaDeviceInfo[]>;
|
||||||
|
switchCamera(deviceId: string): Promise<void>;
|
||||||
|
getCurrentCamera(): Promise<MediaDeviceInfo | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanListener {
|
||||||
|
onScan: (result: string) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QRScannerOptions {
|
||||||
|
camera?: "front" | "back";
|
||||||
|
showPreview?: boolean;
|
||||||
|
playSound?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Implementations
|
||||||
|
|
||||||
|
#### Mobile (Capacitor)
|
||||||
|
- Uses `@capacitor-mlkit/barcode-scanning`
|
||||||
|
- Native camera access through platform APIs
|
||||||
|
- Optimized for mobile performance
|
||||||
|
- Supports both iOS and Android
|
||||||
|
- Real-time QR code detection
|
||||||
|
- Back camera preferred for scanning
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
```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<void> {
|
||||||
|
const scanOptions: StartScanOptions = {
|
||||||
|
formats: [BarcodeFormat.QrCode],
|
||||||
|
lensFacing: options?.camera === "front" ?
|
||||||
|
LensFacing.Front : LensFacing.Back
|
||||||
|
};
|
||||||
|
await BarcodeScanner.startScan(scanOptions);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Web (Desktop)**
|
||||||
|
- Uses browser's MediaDevices API
|
||||||
|
- Supports multiple camera devices
|
||||||
|
- Dynamic camera enumeration
|
||||||
|
- Real-time camera switching
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WebInlineQRScanner.ts
|
||||||
|
async getAvailableCameras(): Promise<MediaDeviceInfo[]> {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.filter(device => device.kind === 'videoinput');
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchCamera(deviceId: string): Promise<void> {
|
||||||
|
// Stop current stream
|
||||||
|
await this.stopScan();
|
||||||
|
|
||||||
|
// Start new stream with selected camera
|
||||||
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
deviceId: { exact: deviceId },
|
||||||
|
width: { ideal: 1280 },
|
||||||
|
height: { ideal: 720 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update video and restart scanning
|
||||||
|
if (this.video) {
|
||||||
|
this.video.srcObject = this.stream;
|
||||||
|
await this.video.play();
|
||||||
|
}
|
||||||
|
this.scanQRCode();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface QRScannerService {
|
||||||
|
// ... existing methods ...
|
||||||
|
|
||||||
|
/** Get available cameras */
|
||||||
|
getAvailableCameras(): Promise<MediaDeviceInfo[]>;
|
||||||
|
|
||||||
|
/** Switch to a specific camera */
|
||||||
|
switchCamera(deviceId: string): Promise<void>;
|
||||||
|
|
||||||
|
/** Get current camera info */
|
||||||
|
getCurrentCamera(): Promise<MediaDeviceInfo | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QRScannerOptions {
|
||||||
|
/** Camera to use ('front' or 'back' for mobile) */
|
||||||
|
camera?: "front" | "back";
|
||||||
|
/** Whether to show a preview of the camera feed */
|
||||||
|
showPreview?: boolean;
|
||||||
|
/** Whether to play a sound on successful scan */
|
||||||
|
playSound?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
The camera switching UI adapts to the platform:
|
||||||
|
|
||||||
|
1. **Mobile Interface**
|
||||||
|
- Simple toggle button for front/back cameras
|
||||||
|
- Positioned in bottom-right corner
|
||||||
|
- Clear visual feedback during switching
|
||||||
|
- Native camera controls
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<button
|
||||||
|
v-if="isNativePlatform"
|
||||||
|
@click="toggleMobileCamera"
|
||||||
|
class="camera-switch-btn"
|
||||||
|
>
|
||||||
|
<font-awesome icon="camera-rotate" />
|
||||||
|
Switch Camera
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Desktop Interface**
|
||||||
|
- Dropdown menu with all available cameras
|
||||||
|
- Camera labels and device IDs
|
||||||
|
- Real-time camera switching
|
||||||
|
- Responsive design
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<select
|
||||||
|
v-model="selectedCameraId"
|
||||||
|
@change="onCameraChange"
|
||||||
|
class="camera-select-dropdown"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="camera in availableCameras"
|
||||||
|
:key="camera.deviceId"
|
||||||
|
:value="camera.deviceId"
|
||||||
|
>
|
||||||
|
{{ camera.label || `Camera ${camera.deviceId.slice(0, 4)}` }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The camera switching implementation includes comprehensive error handling:
|
||||||
|
|
||||||
|
1. **Common Error Scenarios**
|
||||||
|
- Camera in use by another application
|
||||||
|
- Permission denied during switch
|
||||||
|
- Device not available
|
||||||
|
- Stream initialization failure
|
||||||
|
- Camera switch timeout
|
||||||
|
|
||||||
|
2. **Error Response**
|
||||||
|
```typescript
|
||||||
|
private async handleCameraSwitch(deviceId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.updateCameraState("initializing", "Switching camera...");
|
||||||
|
await this.switchCamera(deviceId);
|
||||||
|
this.updateCameraState("active", "Camera switched successfully");
|
||||||
|
} catch (error) {
|
||||||
|
this.updateCameraState("error", "Failed to switch camera");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **User Feedback**
|
||||||
|
- Visual indicators during switching
|
||||||
|
- Error notifications
|
||||||
|
- Camera state updates
|
||||||
|
- Permission request dialogs
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
The camera system maintains several states:
|
||||||
|
|
||||||
|
1. **Camera States**
|
||||||
|
```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<MediaDeviceInfo[]> {
|
||||||
|
// On mobile, we have two fixed cameras
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
deviceId: 'back',
|
||||||
|
label: 'Back Camera',
|
||||||
|
kind: 'videoinput'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: 'front',
|
||||||
|
label: 'Front Camera',
|
||||||
|
kind: 'videoinput'
|
||||||
|
}
|
||||||
|
] as MediaDeviceInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchCamera(deviceId: string): Promise<void> {
|
||||||
|
if (!this.isScanning) return;
|
||||||
|
|
||||||
|
const newLensFacing = deviceId === 'front' ?
|
||||||
|
LensFacing.Front : LensFacing.Back;
|
||||||
|
|
||||||
|
// Stop current scan
|
||||||
|
await this.stopScan();
|
||||||
|
|
||||||
|
// Update lens facing
|
||||||
|
this.currentLensFacing = newLensFacing;
|
||||||
|
|
||||||
|
// Restart scan with new camera
|
||||||
|
await this.startScan({
|
||||||
|
camera: deviceId as 'front' | 'back'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentCamera(): Promise<MediaDeviceInfo | null> {
|
||||||
|
return {
|
||||||
|
deviceId: this.currentLensFacing === LensFacing.Front ? 'front' : 'back',
|
||||||
|
label: this.currentLensFacing === LensFacing.Front ?
|
||||||
|
'Front Camera' : 'Back Camera',
|
||||||
|
kind: 'videoinput'
|
||||||
|
} as MediaDeviceInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Camera State Management**
|
||||||
|
```typescript
|
||||||
|
// CapacitorQRScanner.ts
|
||||||
|
private async handleCameraState(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if camera is available
|
||||||
|
const { camera } = await BarcodeScanner.checkPermissions();
|
||||||
|
|
||||||
|
if (camera === 'denied') {
|
||||||
|
this.updateCameraState('permission_denied');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if camera is in use
|
||||||
|
const isInUse = await this.isCameraInUse();
|
||||||
|
if (isInUse) {
|
||||||
|
this.updateCameraState('in_use');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCameraState('ready');
|
||||||
|
} catch (error) {
|
||||||
|
this.updateCameraState('error', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isCameraInUse(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Try to start a test scan
|
||||||
|
await BarcodeScanner.startScan({
|
||||||
|
formats: [BarcodeFormat.QrCode],
|
||||||
|
lensFacing: this.currentLensFacing
|
||||||
|
});
|
||||||
|
// If successful, stop it immediately
|
||||||
|
await BarcodeScanner.stopScan();
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return error.message.includes('camera in use');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Error Handling**
|
||||||
|
```typescript
|
||||||
|
// CapacitorQRScanner.ts
|
||||||
|
private async handleCameraError(error: Error): Promise<void> {
|
||||||
|
switch (error.name) {
|
||||||
|
case 'CameraPermissionDenied':
|
||||||
|
this.updateCameraState('permission_denied');
|
||||||
|
break;
|
||||||
|
case 'CameraInUse':
|
||||||
|
this.updateCameraState('in_use');
|
||||||
|
break;
|
||||||
|
case 'CameraUnavailable':
|
||||||
|
this.updateCameraState('not_found');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.updateCameraState('error', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Platform-Specific Considerations
|
||||||
|
|
||||||
|
1. **iOS Implementation**
|
||||||
|
- Camera permissions in Info.plist
|
||||||
|
- Privacy descriptions
|
||||||
|
- Camera usage description
|
||||||
|
- Background camera access
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- ios/App/App/Info.plist -->
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>We need access to your camera to scan QR codes</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>We need access to save scanned QR codes</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Android Implementation**
|
||||||
|
- Camera permissions in AndroidManifest.xml
|
||||||
|
- Runtime permission handling
|
||||||
|
- Camera features declaration
|
||||||
|
- Hardware feature requirements
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- android/app/src/main/AndroidManifest.xml -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Platform-Specific Features**
|
||||||
|
- iOS: Camera orientation handling
|
||||||
|
- Android: Camera resolution optimization
|
||||||
|
- Both: Battery usage optimization
|
||||||
|
- Both: Memory management
|
||||||
|
|
||||||
|
```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<void> {
|
||||||
|
// Stop scanning
|
||||||
|
await this.stopScan();
|
||||||
|
|
||||||
|
// Clear any stored camera data
|
||||||
|
this.currentLensFacing = LensFacing.Back;
|
||||||
|
|
||||||
|
// Remove listeners
|
||||||
|
this.listenerHandles.forEach(handle => handle());
|
||||||
|
this.listenerHandles = [];
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
this.isScanning = false;
|
||||||
|
this.updateCameraState('off');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Testing on Capacitor
|
||||||
|
|
||||||
|
1. **Device Testing**
|
||||||
|
- Test on multiple iOS devices
|
||||||
|
- Test on multiple Android devices
|
||||||
|
- Test different camera configurations
|
||||||
|
- Test with different screen sizes
|
||||||
|
- Test with different OS versions
|
||||||
|
|
||||||
|
2. **Camera Testing**
|
||||||
|
- Test front camera switching
|
||||||
|
- Test back camera switching
|
||||||
|
- Test camera permissions
|
||||||
|
- Test camera in use scenarios
|
||||||
|
- Test low light conditions
|
||||||
|
- Test different QR code sizes
|
||||||
|
- Test different QR code distances
|
||||||
|
|
||||||
|
3. **Performance Testing**
|
||||||
|
- Battery usage monitoring
|
||||||
|
- Memory usage monitoring
|
||||||
|
- Camera switching speed
|
||||||
|
- QR code detection speed
|
||||||
|
- App responsiveness
|
||||||
|
- Background/foreground transitions
|
||||||
@@ -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
|
|
||||||
275
package-lock.json
generated
275
package-lock.json
generated
@@ -6465,9 +6465,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ipld/dag-pb/node_modules/multiformats": {
|
"node_modules/@ipld/dag-pb/node_modules/multiformats": {
|
||||||
"version": "13.3.3",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz",
|
||||||
"integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ=="
|
"integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w=="
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
@@ -7610,9 +7610,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
|
||||||
"integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
|
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -7623,9 +7623,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
|
||||||
"integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
|
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -7636,9 +7636,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
|
||||||
"integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
|
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -7649,9 +7649,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
|
||||||
"integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
|
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -7662,9 +7662,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
|
||||||
"integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
|
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -7675,9 +7675,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
|
||||||
"integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
|
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -7688,9 +7688,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
|
||||||
"integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
|
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -7701,9 +7701,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
|
||||||
"integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
|
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -7714,9 +7714,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
|
||||||
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
|
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -7727,9 +7727,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
|
||||||
"integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
|
"integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -7740,9 +7740,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
|
||||||
"integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
|
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -7753,9 +7753,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
|
||||||
"integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
|
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -7766,9 +7766,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
|
||||||
"integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
|
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -7779,9 +7779,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
|
||||||
"integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
|
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -7792,9 +7792,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
|
||||||
"integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
|
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -7805,9 +7805,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
|
||||||
"integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
|
"integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -7818,9 +7818,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz",
|
||||||
"integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
|
"integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -7831,9 +7831,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
|
||||||
"integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
|
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -7844,9 +7844,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
|
||||||
"integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
|
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -7857,9 +7857,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
|
||||||
"integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
|
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8737,9 +8737,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/leaflet": {
|
"node_modules/@types/leaflet": {
|
||||||
"version": "1.9.17",
|
"version": "1.9.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.18.tgz",
|
||||||
"integrity": "sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==",
|
"integrity": "sha512-ht2vsoPjezor5Pmzi5hdsA7F++v5UGq9OlUduWHmMZiuQGIpJ2WS5+Gg9HaAA79gNh1AIPtCqhzejcIZ3lPzXQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
@@ -8764,9 +8764,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.47",
|
"version": "20.17.48",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.47.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.48.tgz",
|
||||||
"integrity": "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==",
|
"integrity": "sha512-KpSfKOHPsiSC4IkZeu2LsusFwExAIVGkhG1KkbaBMLwau0uMhj0fCrvyg9ddM2sAvd+gtiBJLir4LAw1MNMIaw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
@@ -9461,9 +9461,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@veramo/did-provider-peer/node_modules/did-jwt-vc": {
|
"node_modules/@veramo/did-provider-peer/node_modules/did-jwt-vc": {
|
||||||
"version": "4.0.12",
|
"version": "4.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/did-jwt-vc/-/did-jwt-vc-4.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/did-jwt-vc/-/did-jwt-vc-4.0.13.tgz",
|
||||||
"integrity": "sha512-xhQ8tY6tanrgzkhKmoSt3A/XkInufMo73qSJU1cXWxfYpMpYYmldvaxvJm2nqMjCly276ajP6LNeXgkYg9elRw==",
|
"integrity": "sha512-T1IUneS7Rgpao8dOeZy7dMUvAvcLLn7T8YlWRk/8HsEpaVLDx5NrjRfbfDJU8FL8CI8aBIAhoDnPQO3PNV+BWg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"did-jwt": "^8.0.0",
|
"did-jwt": "^8.0.0",
|
||||||
"did-resolver": "^4.1.0"
|
"did-resolver": "^4.1.0"
|
||||||
@@ -9486,9 +9486,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@veramo/did-provider-peer/node_modules/multiformats": {
|
"node_modules/@veramo/did-provider-peer/node_modules/multiformats": {
|
||||||
"version": "13.3.3",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz",
|
||||||
"integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ=="
|
"integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w=="
|
||||||
},
|
},
|
||||||
"node_modules/@veramo/did-resolver": {
|
"node_modules/@veramo/did-resolver": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
@@ -13901,9 +13901,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.154",
|
"version": "1.5.155",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.154.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
|
||||||
"integrity": "sha512-G4VCFAyKbp1QJ+sWdXYIRYsPGvlV5sDACfCmoMFog3rjm1syLhI41WXm/swZypwCIWIm4IFLWzHY14joWMQ5Fw==",
|
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/elementtree": {
|
"node_modules/elementtree": {
|
||||||
@@ -14568,9 +14568,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ethers": {
|
"node_modules/ethers": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.1.tgz",
|
||||||
"integrity": "sha512-KgHwltNSMdbrGWEyKkM0Rt2s+u1nDH/5BVDQakLinzGEJi4bWindBzZSCC4gKsbZjwDTI6ex/8suR9Ihbmz4IQ==",
|
"integrity": "sha512-JnFiPFi3sK2Z6y7jZ3qrafDMwiXmU+6cNZ0M+kPq+mTy9skqEzwqAdFW3nb/em2xjlIVXX6Lz8ID6i3LmS4+fQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -14653,9 +14653,9 @@
|
|||||||
"integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="
|
"integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="
|
||||||
},
|
},
|
||||||
"node_modules/ethr-did": {
|
"node_modules/ethr-did": {
|
||||||
"version": "3.0.34",
|
"version": "3.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.35.tgz",
|
||||||
"integrity": "sha512-0NloieyCPi6iRebLRufFns00sRZJ46GB+Oc/thu3hqIc/7rOUQjNEQmUbSTo2OTEIW3FOIuaAVo2eh58HQ9SwA==",
|
"integrity": "sha512-vWTGIcdnzyTeahNw25P4eQEMo6gVQEVEg0Kit8spDPB6neUAk5HaJXfxG9i8gKPJBOgyVNkMQ/aPOgVhnSig3w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"did-jwt": "^8.0.0",
|
"did-jwt": "^8.0.0",
|
||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
@@ -21089,9 +21089,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/papaparse": {
|
"node_modules/papaparse": {
|
||||||
"version": "5.5.2",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
|
||||||
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA=="
|
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="
|
||||||
},
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -21750,9 +21750,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/protons-runtime/node_modules/multiformats": {
|
"node_modules/protons-runtime/node_modules/multiformats": {
|
||||||
"version": "13.3.3",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz",
|
||||||
"integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ=="
|
"integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w=="
|
||||||
},
|
},
|
||||||
"node_modules/protons-runtime/node_modules/uint8arrays": {
|
"node_modules/protons-runtime/node_modules/uint8arrays": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
@@ -23241,9 +23241,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.40.2",
|
"version": "4.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
|
||||||
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
|
"integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.7"
|
"@types/estree": "1.0.7"
|
||||||
@@ -23256,26 +23256,26 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.40.2",
|
"@rollup/rollup-android-arm-eabi": "4.41.0",
|
||||||
"@rollup/rollup-android-arm64": "4.40.2",
|
"@rollup/rollup-android-arm64": "4.41.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.40.2",
|
"@rollup/rollup-darwin-arm64": "4.41.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.40.2",
|
"@rollup/rollup-darwin-x64": "4.41.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.40.2",
|
"@rollup/rollup-freebsd-arm64": "4.41.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.40.2",
|
"@rollup/rollup-freebsd-x64": "4.41.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.41.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.40.2",
|
"@rollup/rollup-linux-arm-musleabihf": "4.41.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.40.2",
|
"@rollup/rollup-linux-arm64-gnu": "4.41.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.40.2",
|
"@rollup/rollup-linux-arm64-musl": "4.41.0",
|
||||||
"@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
|
"@rollup/rollup-linux-loongarch64-gnu": "4.41.0",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.41.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.40.2",
|
"@rollup/rollup-linux-riscv64-gnu": "4.41.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.40.2",
|
"@rollup/rollup-linux-riscv64-musl": "4.41.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.40.2",
|
"@rollup/rollup-linux-s390x-gnu": "4.41.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.40.2",
|
"@rollup/rollup-linux-x64-gnu": "4.41.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.40.2",
|
"@rollup/rollup-linux-x64-musl": "4.41.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.40.2",
|
"@rollup/rollup-win32-arm64-msvc": "4.41.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.40.2",
|
"@rollup/rollup-win32-ia32-msvc": "4.41.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.40.2",
|
"@rollup/rollup-win32-x64-msvc": "4.41.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25050,13 +25050,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/synckit": {
|
"node_modules/synckit": {
|
||||||
"version": "0.11.5",
|
"version": "0.11.6",
|
||||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.5.tgz",
|
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.6.tgz",
|
||||||
"integrity": "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==",
|
"integrity": "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pkgr/core": "^0.2.4",
|
"@pkgr/core": "^0.2.4"
|
||||||
"tslib": "^2.8.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
@@ -25065,12 +25064,6 @@
|
|||||||
"url": "https://opencollective.com/synckit"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
@@ -25267,13 +25260,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.39.1",
|
"version": "5.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
||||||
"integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==",
|
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.14.0",
|
||||||
"commander": "^2.20.0",
|
"commander": "^2.20.0",
|
||||||
"source-map-support": "~0.5.20"
|
"source-map-support": "~0.5.20"
|
||||||
},
|
},
|
||||||
@@ -25989,9 +25982,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uint8-varint/node_modules/multiformats": {
|
"node_modules/uint8-varint/node_modules/multiformats": {
|
||||||
"version": "13.3.3",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz",
|
||||||
"integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ=="
|
"integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w=="
|
||||||
},
|
},
|
||||||
"node_modules/uint8-varint/node_modules/uint8arrays": {
|
"node_modules/uint8-varint/node_modules/uint8arrays": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
@@ -26010,9 +26003,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uint8arraylist/node_modules/multiformats": {
|
"node_modules/uint8arraylist/node_modules/multiformats": {
|
||||||
"version": "13.3.3",
|
"version": "13.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.5.tgz",
|
||||||
"integrity": "sha512-TlaFCzs3NHNzMpwiGwRYehnnhHlZcWfptygFekshlb9xCyO09GfN+9881+VBENCdRnKOeqmMxDCbupNecV8xRQ=="
|
"integrity": "sha512-dXsVGtaekmpKMHUngnXkPpXnJU9h8ee2+P85kTETViXcDkQjkWLrEkj/b5pJ23ZhvBlicr9eq3B9IJOa28R70w=="
|
||||||
},
|
},
|
||||||
"node_modules/uint8arraylist/node_modules/uint8arrays": {
|
"node_modules/uint8arraylist/node_modules/uint8arrays": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
@@ -27445,15 +27438,15 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.7.1",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||||
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
|
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
@@ -27601,9 +27594,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.24.4",
|
"version": "3.25.7",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.7.tgz",
|
||||||
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
|
"integrity": "sha512-YGdT1cVRmKkOg6Sq7vY7IkxdphySKnXhaUmFI4r4FcuFVNgpCb9tZfNwXbT6BPjD5oz0nubFsoo9pIqKrDcCvg==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
#
|
#
|
||||||
# Prerequisites: openssl, jq
|
# 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
|
# Generate a key and extract the public part
|
||||||
@@ -1,77 +1,181 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||||
<div class="dialog relative">
|
<div class="dialog relative">
|
||||||
<div class="text-lg text-center font-light relative z-50">
|
<div class="text-lg text-center font-bold relative">
|
||||||
|
<h1 id="ViewHeading" class="text-center font-bold">
|
||||||
|
<span v-if="uploading">Uploading Image…</span>
|
||||||
|
<span v-else-if="blob">Crop Image</span>
|
||||||
|
<span v-else-if="showCameraPreview">Upload Image</span>
|
||||||
|
<span v-else>Add Photo</span>
|
||||||
|
</h1>
|
||||||
<div
|
<div
|
||||||
id="ViewHeading"
|
class="text-2xl text-center px-1 py-0.5 leading-none absolute -right-1 top-0"
|
||||||
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
|
|
||||||
>
|
|
||||||
Add Photo
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
|
||||||
@click="close()"
|
@click="close()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="mt-4">
|
||||||
<div class="text-center mt-8">
|
<template v-if="isRegistered">
|
||||||
<template v-if="isRegistered">
|
<div v-if="!blob">
|
||||||
<div>
|
<div
|
||||||
<font-awesome
|
class="border-b border-dashed border-slate-300 text-orange-400 mb-4 font-bold text-sm"
|
||||||
icon="camera"
|
>
|
||||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
|
||||||
@click="openPhotoDialog()"
|
Take a photo with your camera
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showCameraPreview"
|
||||||
|
class="camera-preview relative flex bg-black overflow-hidden mb-4"
|
||||||
|
>
|
||||||
|
<div class="camera-container w-full h-full relative">
|
||||||
|
<video
|
||||||
|
ref="videoElement"
|
||||||
|
class="camera-video w-full h-full object-cover"
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
muted
|
||||||
|
></video>
|
||||||
|
<button
|
||||||
|
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
|
@click="capturePhoto"
|
||||||
|
>
|
||||||
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-4 font-bold text-sm"
|
||||||
|
>
|
||||||
|
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
|
||||||
|
OR choose a file from your device
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="w-full file:text-center file:bg-gradient-to-b file:from-slate-400 file:to-slate-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:rounded-md file:border-none file:cursor-pointer file:me-2"
|
||||||
|
@change="uploadImageFile"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div
|
||||||
<input type="file" @change="uploadImageFile" />
|
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-4 font-bold text-sm"
|
||||||
</div>
|
>
|
||||||
<div class="mt-4">
|
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
|
||||||
<span class="mt-2">
|
OR paste an image URL
|
||||||
... or paste a URL:
|
|
||||||
<input v-model="imageUrl" type="text" class="border-2" />
|
|
||||||
</span>
|
|
||||||
<span class="ml-2">
|
|
||||||
<font-awesome
|
|
||||||
v-if="imageUrl"
|
|
||||||
icon="check"
|
|
||||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md cursor-pointer"
|
|
||||||
@click="acceptUrl"
|
|
||||||
/>
|
|
||||||
<!-- so that there's no shifting when it becomes visible -->
|
|
||||||
<font-awesome
|
|
||||||
v-else
|
|
||||||
icon="check"
|
|
||||||
class="text-white bg-white px-2 py-2"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="flex items-center gap-2 mt-4">
|
||||||
<template v-else>
|
<input
|
||||||
<div class="text-center text-lg text-slate-500 py-12">Register to Upload a Photo</div>
|
v-model="imageUrl"
|
||||||
</template>
|
type="text"
|
||||||
</div>
|
class="block w-full rounded border border-slate-400 px-4 py-2"
|
||||||
|
placeholder="https://example.com/image.jpg"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="imageUrl"
|
||||||
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-md cursor-pointer"
|
||||||
|
@click="acceptUrl"
|
||||||
|
>
|
||||||
|
<font-awesome icon="check" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="uploading" class="flex justify-center">
|
||||||
|
<font-awesome
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin fa-3x text-center block px-12 py-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="crop">
|
||||||
|
<VuePictureCropper
|
||||||
|
:box-style="{
|
||||||
|
backgroundColor: '#f8f8f8',
|
||||||
|
margin: 'auto',
|
||||||
|
}"
|
||||||
|
:img="createBlobURL(blob)"
|
||||||
|
:options="{
|
||||||
|
viewMode: 1,
|
||||||
|
dragMode: 'crop',
|
||||||
|
aspectRatio: 1 / 1,
|
||||||
|
}"
|
||||||
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img
|
||||||
|
:src="createBlobURL(blob)"
|
||||||
|
class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'grid gap-2 mt-2',
|
||||||
|
showRetry ? 'grid-cols-2' : 'grid-cols-1',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
|
||||||
|
@click="uploadImage"
|
||||||
|
>
|
||||||
|
<span>Upload</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showRetry"
|
||||||
|
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
|
||||||
|
@click="retryImage"
|
||||||
|
>
|
||||||
|
<span>Retry</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
id="noticeBeforeUpload"
|
||||||
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3"
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<p class="mb-2">
|
||||||
|
Before you can upload a photo, a friend needs to register you.
|
||||||
|
</p>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contact-qr' }"
|
||||||
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Share Your Info
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PhotoDialog ref="photoDialog" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||||
import PhotoDialog from "../components/PhotoDialog.vue";
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
|
import { accessToken } from "../libs/crypto";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
|
|
||||||
const inputImageFileNameRef = ref<Blob>();
|
const inputImageFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { PhotoDialog },
|
components: { VuePictureCropper },
|
||||||
props: {
|
props: {
|
||||||
isRegistered: {
|
isRegistered: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -82,38 +186,97 @@ const inputImageFileNameRef = ref<Blob>();
|
|||||||
export default class ImageMethodDialog extends Vue {
|
export default class ImageMethodDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
claimType: string;
|
/** Active DID for user authentication */
|
||||||
|
activeDid = "";
|
||||||
|
|
||||||
|
/** Current image blob being processed */
|
||||||
|
blob?: Blob;
|
||||||
|
|
||||||
|
/** Type of claim for the image */
|
||||||
|
claimType: string = "";
|
||||||
|
|
||||||
|
/** Whether to show cropping interface */
|
||||||
crop: boolean = false;
|
crop: boolean = false;
|
||||||
|
|
||||||
|
/** Name of the selected file */
|
||||||
|
fileName?: string;
|
||||||
|
|
||||||
|
/** Callback function to set image URL after upload */
|
||||||
imageCallback: (imageUrl?: string) => void = () => {};
|
imageCallback: (imageUrl?: string) => void = () => {};
|
||||||
|
|
||||||
|
/** URL for image input */
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
|
|
||||||
|
/** Whether to show retry button */
|
||||||
|
showRetry = true;
|
||||||
|
|
||||||
|
/** Upload progress state */
|
||||||
|
uploading = false;
|
||||||
|
|
||||||
|
/** Dialog visibility state */
|
||||||
visible = false;
|
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;
|
||||||
|
|
||||||
|
private platformCapabilities = this.platformService.getCapabilities();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook: Initializes component and retrieves user settings
|
||||||
|
* @throws {Error} When settings retrieval fails
|
||||||
|
*/
|
||||||
|
async mounted() {
|
||||||
|
console.log("ImageMethodDialog mounted");
|
||||||
|
try {
|
||||||
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
|
this.activeDid = settings.activeDid || "";
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error("Error retrieving settings from database:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "There was an error retrieving your settings.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook: Cleans up camera stream when component is destroyed
|
||||||
|
*/
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopCameraPreview();
|
||||||
|
}
|
||||||
|
|
||||||
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
||||||
this.claimType = claimType;
|
this.claimType = claimType;
|
||||||
this.crop = !!crop;
|
this.crop = !!crop;
|
||||||
this.imageCallback = setImageFn;
|
this.imageCallback = setImageFn;
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
|
||||||
|
|
||||||
openPhotoDialog(blob?: Blob, fileName?: string) {
|
// Start camera preview immediately if not on mobile
|
||||||
this.visible = false;
|
if (!this.platformCapabilities.isMobile) {
|
||||||
|
this.startCameraPreview();
|
||||||
(this.$refs.photoDialog as PhotoDialog).open(
|
}
|
||||||
this.imageCallback,
|
|
||||||
this.claimType,
|
|
||||||
this.crop,
|
|
||||||
blob,
|
|
||||||
fileName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImageFile(event: Event) {
|
async uploadImageFile(event: Event) {
|
||||||
this.visible = false;
|
const target = event.target as HTMLInputElement;
|
||||||
|
if (!target.files) return;
|
||||||
|
|
||||||
inputImageFileNameRef.value = event.target.files[0];
|
inputImageFileNameRef.value = target.files[0];
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/File
|
|
||||||
// ... plus it has a `type` property from my testing
|
|
||||||
const file = inputImageFileNameRef.value;
|
const file = inputImageFileNameRef.value;
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -123,7 +286,9 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
const blob = new Blob([new Uint8Array(data)], {
|
const blob = new Blob([new Uint8Array(data)], {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
});
|
});
|
||||||
this.openPhotoDialog(blob, file.name as string);
|
this.blob = blob;
|
||||||
|
this.fileName = file.name;
|
||||||
|
this.showRetry = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(file as Blob);
|
reader.readAsArrayBuffer(file as Blob);
|
||||||
@@ -131,21 +296,16 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async acceptUrl() {
|
async acceptUrl() {
|
||||||
this.visible = false;
|
|
||||||
if (this.crop) {
|
if (this.crop) {
|
||||||
try {
|
try {
|
||||||
const urlBlobResponse: Blob = await axios.get(this.imageUrl as string, {
|
const urlBlobResponse = await axios.get(this.imageUrl as string, {
|
||||||
responseType: "blob", // This ensures the data is returned as a Blob
|
responseType: "blob",
|
||||||
});
|
});
|
||||||
const fullUrl = new URL(this.imageUrl as string);
|
const fullUrl = new URL(this.imageUrl as string);
|
||||||
const fileName = fullUrl.pathname.split("/").pop() as string;
|
const fileName = fullUrl.pathname.split("/").pop() as string;
|
||||||
(this.$refs.photoDialog as PhotoDialog).open(
|
this.blob = urlBlobResponse.data as Blob;
|
||||||
this.imageCallback,
|
this.fileName = fileName;
|
||||||
this.claimType,
|
this.showRetry = false;
|
||||||
this.crop,
|
|
||||||
urlBlobResponse.data as Blob,
|
|
||||||
fileName,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -159,11 +319,219 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.imageCallback(this.imageUrl);
|
this.imageCallback(this.imageUrl);
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
this.stopCameraPreview();
|
||||||
|
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
||||||
|
if (bottomNav) {
|
||||||
|
bottomNav.style.display = "";
|
||||||
|
}
|
||||||
|
this.blob = undefined;
|
||||||
|
this.showCameraPreview = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startCameraPreview() {
|
||||||
|
logger.debug("startCameraPreview called");
|
||||||
|
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||||
|
logger.debug("Platform capabilities:", this.platformCapabilities);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Starting camera preview for desktop browser");
|
||||||
|
try {
|
||||||
|
this.showCameraPreview = true;
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { facingMode: "environment" },
|
||||||
|
});
|
||||||
|
this.cameraStream = stream;
|
||||||
|
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
const videoElement = this.$refs.videoElement as HTMLVideoElement;
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.srcObject = stream;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
videoElement.onloadedmetadata = () => {
|
||||||
|
videoElement.play().then(() => {
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCameraPreview() {
|
||||||
|
if (this.cameraStream) {
|
||||||
|
this.cameraStream.getTracks().forEach((track) => track.stop());
|
||||||
|
this.cameraStream = null;
|
||||||
|
}
|
||||||
|
this.showCameraPreview = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.showRetry = true;
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBlobURL(blob: Blob): string {
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retryImage() {
|
||||||
|
this.blob = undefined;
|
||||||
|
if (!this.platformCapabilities.isMobile) {
|
||||||
|
await this.startCameraPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadImage() {
|
||||||
|
this.uploading = true;
|
||||||
|
|
||||||
|
if (this.crop) {
|
||||||
|
this.blob = (await cropper?.getBlob()) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await accessToken(this.activeDid);
|
||||||
|
const headers = {
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
const formData = new FormData();
|
||||||
|
if (!this.blob) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "There was an error finding the picture. Please try again.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
this.uploading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||||
|
formData.append("claimType", this.claimType);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.location.hostname === "localhost" &&
|
||||||
|
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||||
|
) {
|
||||||
|
logger.log(
|
||||||
|
"Using shared image API server, so only users on that server can play with images.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const response = await axios.post(
|
||||||
|
DEFAULT_IMAGE_API_SERVER + "/image",
|
||||||
|
formData,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
this.uploading = false;
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
this.imageCallback(response.data.url as string);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
let errorMessage = "There was an error saving the picture.";
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const data = error.response?.data;
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
errorMessage = "Authentication failed. Please try logging in again.";
|
||||||
|
} else if (status === 413) {
|
||||||
|
errorMessage = "Image file is too large. Please try a smaller image.";
|
||||||
|
} else if (status === 415) {
|
||||||
|
errorMessage =
|
||||||
|
"Unsupported image format. Please try a different image.";
|
||||||
|
} else if (status && status >= 500) {
|
||||||
|
errorMessage = "Server error. Please try again later.";
|
||||||
|
} else if (data?.message) {
|
||||||
|
errorMessage = data.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
this.uploading = false;
|
||||||
|
this.blob = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -189,5 +557,9 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,15 +15,16 @@ PhotoDialog.vue */
|
|||||||
<div class="text-lg text-center font-light relative z-50">
|
<div class="text-lg text-center font-light relative z-50">
|
||||||
<div
|
<div
|
||||||
id="ViewHeading"
|
id="ViewHeading"
|
||||||
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
|
class="text-center font-bold absolute top-0 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none"
|
||||||
>
|
>
|
||||||
<span v-if="uploading"> Uploading... </span>
|
<span v-if="uploading"> Uploading... </span>
|
||||||
<span v-else-if="blob"> Look Good? </span>
|
<span v-else-if="blob"> Look Good? </span>
|
||||||
|
<span v-else-if="showCameraPreview"> Take Photo </span>
|
||||||
<span v-else> Say "Cheese"! </span>
|
<span v-else> Say "Cheese"! </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
class="text-lg text-center px-2 py-2 leading-none absolute right-0 top-0 text-white cursor-pointer"
|
||||||
@click="close()"
|
@click="close()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||||
@@ -47,7 +48,7 @@ PhotoDialog.vue */
|
|||||||
:options="{
|
:options="{
|
||||||
viewMode: 1,
|
viewMode: 1,
|
||||||
dragMode: 'crop',
|
dragMode: 'crop',
|
||||||
aspectRatio: 9 / 9,
|
aspectRatio: 1 / 1,
|
||||||
}"
|
}"
|
||||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
/>
|
/>
|
||||||
@@ -60,32 +61,45 @@ PhotoDialog.vue */
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
|
<div class="grid grid-cols-2 gap-2 mt-2">
|
||||||
<button
|
<button
|
||||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
|
||||||
@click="uploadImage"
|
@click="uploadImage"
|
||||||
>
|
>
|
||||||
<span>Upload</span>
|
<span>Upload</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="showRetry"
|
|
||||||
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
|
v-if="showRetry"
|
||||||
|
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
|
||||||
@click="retryImage"
|
@click="retryImage"
|
||||||
>
|
>
|
||||||
<span>Retry</span>
|
<span>Retry</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="showCameraPreview" class="camera-preview">
|
||||||
|
<div class="camera-container">
|
||||||
|
<video
|
||||||
|
ref="videoElement"
|
||||||
|
class="camera-video"
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
muted
|
||||||
|
></video>
|
||||||
|
<button
|
||||||
|
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
|
@click="capturePhoto"
|
||||||
|
>
|
||||||
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
||||||
<button
|
<button
|
||||||
v-if="isRegistered"
|
v-if="isRegistered"
|
||||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||||
@click="takePhoto"
|
@click="startCameraPreview"
|
||||||
>
|
>
|
||||||
<font-awesome icon="camera" class="w-[1em]" />
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
</button>
|
</button>
|
||||||
@@ -142,20 +156,29 @@ export default class PhotoDialog extends Vue {
|
|||||||
/** Dialog visibility state */
|
/** Dialog visibility state */
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
|
/** Whether to show camera preview */
|
||||||
|
showCameraPreview = false;
|
||||||
|
|
||||||
|
/** Camera stream reference */
|
||||||
|
private cameraStream: MediaStream | null = null;
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
|
private platformCapabilities = this.platformService.getCapabilities();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle hook: Initializes component and retrieves user settings
|
* Lifecycle hook: Initializes component and retrieves user settings
|
||||||
* @throws {Error} When settings retrieval fails
|
* @throws {Error} When settings retrieval fails
|
||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
logger.log("PhotoDialog mounted");
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
logger.log("isRegistered:", this.isRegistered);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error("Error retrieving settings from database:", error);
|
logger.error("Error retrieving settings from database:", error);
|
||||||
this.$notify(
|
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
|
* Opens the photo dialog with specified configuration
|
||||||
* @param setImageFn - Callback function to handle image URL after upload
|
* @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 blob - Optional existing image blob
|
||||||
* @param inputFileName - Optional filename for the image
|
* @param inputFileName - Optional filename for the image
|
||||||
*/
|
*/
|
||||||
open(
|
async open(
|
||||||
setImageFn: (arg: string) => void,
|
setImageFn: (arg: string) => void,
|
||||||
claimType: string,
|
claimType: string,
|
||||||
crop?: boolean,
|
crop?: boolean,
|
||||||
@@ -204,6 +234,10 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
this.fileName = undefined;
|
this.fileName = undefined;
|
||||||
this.showRetry = true;
|
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
|
* Closes the photo dialog and resets state
|
||||||
*/
|
*/
|
||||||
close() {
|
close() {
|
||||||
|
logger.debug(
|
||||||
|
"Dialog closing, current showCameraPreview:",
|
||||||
|
this.showCameraPreview,
|
||||||
|
);
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
this.stopCameraPreview();
|
||||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
||||||
if (bottomNav) {
|
if (bottomNav) {
|
||||||
bottomNav.style.display = "";
|
bottomNav.style.display = "";
|
||||||
@@ -219,6 +258,154 @@ export default class PhotoDialog extends Vue {
|
|||||||
this.blob = undefined;
|
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
|
* Captures a photo using device camera
|
||||||
* @throws {Error} When camera access fails
|
* @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() {
|
async retryImage() {
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
|
if (!this.platformCapabilities.isMobile) {
|
||||||
|
await this.startCameraPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -422,5 +612,43 @@ export default class PhotoDialog extends Vue {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 700px;
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,106 @@
|
|||||||
/**
|
/**
|
||||||
* @file Deep Link Interface Definitions
|
* @file Deep Link Type Definitions and Validation Schemas
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
*
|
*
|
||||||
* Defines the core interfaces for the deep linking system.
|
* This file defines the type system and validation schemas for deep linking in the TimeSafari app.
|
||||||
* These interfaces are used across the deep linking implementation
|
* It uses Zod for runtime validation while providing TypeScript types for compile-time checking.
|
||||||
* to ensure type safety and consistent error handling.
|
*
|
||||||
|
* 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 {
|
export interface DeepLinkError extends Error {
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
21
src/interfaces/give.ts
Normal file
21
src/interfaces/give.ts
Normal file
@@ -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;
|
||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
faHandHoldingDollar,
|
faHandHoldingDollar,
|
||||||
faHandHoldingHeart,
|
faHandHoldingHeart,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
|
faImage,
|
||||||
faImagePortrait,
|
faImagePortrait,
|
||||||
faLeftRight,
|
faLeftRight,
|
||||||
faLightbulb,
|
faLightbulb,
|
||||||
@@ -135,6 +136,7 @@ library.add(
|
|||||||
faHandHoldingDollar,
|
faHandHoldingDollar,
|
||||||
faHandHoldingHeart,
|
faHandHoldingHeart,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
|
faImage,
|
||||||
faImagePortrait,
|
faImagePortrait,
|
||||||
faLeftRight,
|
faLeftRight,
|
||||||
faLightbulb,
|
faLightbulb,
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import { App } from "./lib/capacitor/app";
|
import { App } from "./libs/capacitor/app";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from "./services/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import axios from "axios";
|
|||||||
import VueAxios from "vue-axios";
|
import VueAxios from "vue-axios";
|
||||||
import Notifications from "notiwind";
|
import Notifications from "notiwind";
|
||||||
import "./assets/styles/tailwind.css";
|
import "./assets/styles/tailwind.css";
|
||||||
import { FontAwesomeIcon } from "./lib/fontawesome";
|
import { FontAwesomeIcon } from "./libs/fontawesome";
|
||||||
import Camera from "simple-vue-camera";
|
import Camera from "simple-vue-camera";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
|
import {
|
||||||
|
QRScannerService,
|
||||||
|
ScanListener,
|
||||||
|
QRScannerOptions,
|
||||||
|
CameraState,
|
||||||
|
CameraStateListener,
|
||||||
|
} from "./types";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import jsQR from "jsqr";
|
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 TARGET_FPS = 15; // Target 15 FPS for scanning
|
||||||
private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames
|
private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames
|
||||||
private lastFrameTime = 0;
|
private lastFrameTime = 0;
|
||||||
|
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
||||||
|
private currentState: CameraState = "off";
|
||||||
|
private currentStateMessage?: string;
|
||||||
|
|
||||||
constructor(private options?: QRScannerOptions) {
|
constructor(private options?: QRScannerOptions) {
|
||||||
// Generate a short random ID for this scanner instance
|
// 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<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
this.updateCameraState("initializing", "Checking camera permissions...");
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||||
);
|
);
|
||||||
@@ -55,7 +97,9 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
`[WebInlineQRScanner:${this.id}] Permission state:`,
|
`[WebInlineQRScanner:${this.id}] Permission state:`,
|
||||||
permissions.state,
|
permissions.state,
|
||||||
);
|
);
|
||||||
return permissions.state === "granted";
|
const granted = permissions.state === "granted";
|
||||||
|
this.updateCameraState(granted ? "ready" : "permission_denied");
|
||||||
|
return granted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Error checking camera permissions:`,
|
`[WebInlineQRScanner:${this.id}] Error checking camera permissions:`,
|
||||||
@@ -64,12 +108,17 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
this.updateCameraState("error", "Error checking camera permissions");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestPermissions(): Promise<boolean> {
|
async requestPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
this.updateCameraState(
|
||||||
|
"initializing",
|
||||||
|
"Requesting camera permissions...",
|
||||||
|
);
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||||
);
|
);
|
||||||
@@ -107,9 +156,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.error(
|
this.updateCameraState("ready", "Camera permissions granted");
|
||||||
`[WebInlineQRScanner:${this.id}] Camera stream obtained successfully`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Stop the test stream immediately
|
// Stop the test stream immediately
|
||||||
stream.getTracks().forEach((track) => {
|
stream.getTracks().forEach((track) => {
|
||||||
@@ -124,25 +171,19 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const wrappedError =
|
const wrappedError =
|
||||||
error instanceof Error ? error : new Error(String(error));
|
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 (
|
if (
|
||||||
wrappedError.name === "NotFoundError" ||
|
wrappedError.name === "NotFoundError" ||
|
||||||
wrappedError.name === "DevicesNotFoundError"
|
wrappedError.name === "DevicesNotFoundError"
|
||||||
) {
|
) {
|
||||||
|
this.updateCameraState("not_found", "No camera found on this device");
|
||||||
throw new Error("No camera found on this device");
|
throw new Error("No camera found on this device");
|
||||||
} else if (
|
} else if (
|
||||||
wrappedError.name === "NotAllowedError" ||
|
wrappedError.name === "NotAllowedError" ||
|
||||||
wrappedError.name === "PermissionDeniedError"
|
wrappedError.name === "PermissionDeniedError"
|
||||||
) {
|
) {
|
||||||
|
this.updateCameraState("permission_denied", "Camera access denied");
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Camera access denied. Please grant camera permission and try again",
|
"Camera access denied. Please grant camera permission and try again",
|
||||||
);
|
);
|
||||||
@@ -150,8 +191,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
wrappedError.name === "NotReadableError" ||
|
wrappedError.name === "NotReadableError" ||
|
||||||
wrappedError.name === "TrackStartError"
|
wrappedError.name === "TrackStartError"
|
||||||
) {
|
) {
|
||||||
|
this.updateCameraState(
|
||||||
|
"in_use",
|
||||||
|
"Camera is in use by another application",
|
||||||
|
);
|
||||||
throw new Error("Camera is in use by another application");
|
throw new Error("Camera is in use by another application");
|
||||||
} else {
|
} else {
|
||||||
|
this.updateCameraState("error", wrappedError.message);
|
||||||
throw new Error(`Camera error: ${wrappedError.message}`);
|
throw new Error(`Camera error: ${wrappedError.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,6 +436,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
this.scanAttempts = 0;
|
this.scanAttempts = 0;
|
||||||
this.lastScanTime = Date.now();
|
this.lastScanTime = Date.now();
|
||||||
|
this.updateCameraState("initializing", "Starting camera...");
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
|
||||||
|
|
||||||
// Get camera stream
|
// 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:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||||
tracks: this.stream.getTracks().map((t) => ({
|
tracks: this.stream.getTracks().map((t) => ({
|
||||||
kind: t.kind,
|
kind: t.kind,
|
||||||
@@ -431,11 +480,20 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
const wrappedError =
|
const wrappedError =
|
||||||
error instanceof Error ? error : new Error(String(error));
|
error instanceof Error ? error : new Error(String(error));
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Error starting scan:`, {
|
|
||||||
error: wrappedError.message,
|
// Update state based on error type
|
||||||
stack: wrappedError.stack,
|
if (
|
||||||
name: wrappedError.name,
|
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) {
|
if (this.scanListener?.onError) {
|
||||||
this.scanListener.onError(wrappedError);
|
this.scanListener.onError(wrappedError);
|
||||||
}
|
}
|
||||||
@@ -492,14 +550,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const wrappedError =
|
logger.error(
|
||||||
error instanceof Error ? error : new Error(String(error));
|
`[WebInlineQRScanner:${this.id}] Error stopping scan:`,
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, {
|
error,
|
||||||
error: wrappedError.message,
|
);
|
||||||
stack: wrappedError.stack,
|
this.updateCameraState("error", "Error stopping camera");
|
||||||
name: wrappedError.name,
|
throw error;
|
||||||
});
|
|
||||||
throw wrappedError;
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||||
@@ -541,10 +597,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, {
|
logger.error(
|
||||||
error: error instanceof Error ? error.message : String(error),
|
`[WebInlineQRScanner:${this.id}] Error during cleanup:`,
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
error,
|
||||||
});
|
);
|
||||||
|
this.updateCameraState("error", "Error during cleanup");
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ export interface QRScannerOptions {
|
|||||||
playSound?: boolean;
|
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
|
* Interface for QR scanner service implementations
|
||||||
*/
|
*/
|
||||||
@@ -44,6 +58,12 @@ export interface QRScannerService {
|
|||||||
/** Add a listener for scan events */
|
/** Add a listener for scan events */
|
||||||
addListener(listener: ScanListener): void;
|
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 */
|
/** Clean up scanner resources */
|
||||||
cleanup(): Promise<void>;
|
cleanup(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* Architecture:
|
* Architecture:
|
||||||
* 1. DeepLinkHandler class encapsulates all deep link processing logic
|
* 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
|
* 3. Provides consistent error handling and logging
|
||||||
* 4. Maps validated parameters to Vue router calls
|
* 4. Maps validated parameters to Vue router calls
|
||||||
*
|
*
|
||||||
@@ -51,7 +51,7 @@ import {
|
|||||||
baseUrlSchema,
|
baseUrlSchema,
|
||||||
routeSchema,
|
routeSchema,
|
||||||
DeepLinkRoute,
|
DeepLinkRoute,
|
||||||
} from "../types/deepLinks";
|
} from "../interfaces/deepLinks";
|
||||||
import { logConsoleAndDb } from "../db";
|
import { logConsoleAndDb } from "../db";
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async takePicture(): Promise<ImageResult> {
|
async takePicture(): Promise<ImageResult> {
|
||||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
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 on mobile, use file input with capture attribute (existing behavior)
|
||||||
if (isMobile || !hasGetUserMedia) {
|
if (isMobile || !hasGetUserMedia) {
|
||||||
@@ -113,107 +115,121 @@ export class WebPlatformService implements PlatformService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Desktop: Use getUserMedia for webcam capture
|
// Desktop: Use getUserMedia for webcam capture
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let stream: MediaStream | null = null;
|
let stream: MediaStream | null = null;
|
||||||
let video: HTMLVideoElement | null = null;
|
let video: HTMLVideoElement | null = null;
|
||||||
let captureButton: HTMLButtonElement | null = null;
|
let captureButton: HTMLButtonElement | null = null;
|
||||||
let overlay: HTMLDivElement | null = null;
|
let overlay: HTMLDivElement | null = null;
|
||||||
let cleanup = () => {
|
const cleanup = () => {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
stream.getTracks().forEach((track) => track.stop());
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
}
|
}
|
||||||
if (video && video.parentNode) video.parentNode.removeChild(video);
|
if (video && video.parentNode) video.parentNode.removeChild(video);
|
||||||
if (captureButton && captureButton.parentNode) captureButton.parentNode.removeChild(captureButton);
|
if (captureButton && captureButton.parentNode)
|
||||||
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
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");
|
// Move async operations inside Promise body
|
||||||
video.autoplay = true;
|
navigator.mediaDevices
|
||||||
video.playsInline = true;
|
.getUserMedia({
|
||||||
video.style.maxWidth = "90vw";
|
video: { facingMode: "user" },
|
||||||
video.style.maxHeight = "70vh";
|
})
|
||||||
video.srcObject = stream;
|
.then((mediaStream) => {
|
||||||
overlay.appendChild(video);
|
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");
|
video = document.createElement("video");
|
||||||
captureButton.textContent = "Capture Photo";
|
video.autoplay = true;
|
||||||
captureButton.style.marginTop = "2rem";
|
video.playsInline = true;
|
||||||
captureButton.style.padding = "1rem 2rem";
|
video.style.maxWidth = "90vw";
|
||||||
captureButton.style.fontSize = "1.2rem";
|
video.style.maxHeight = "70vh";
|
||||||
captureButton.style.background = "#2563eb";
|
video.srcObject = stream;
|
||||||
captureButton.style.color = "white";
|
overlay.appendChild(video);
|
||||||
captureButton.style.border = "none";
|
|
||||||
captureButton.style.borderRadius = "0.5rem";
|
|
||||||
captureButton.style.cursor = "pointer";
|
|
||||||
overlay.appendChild(captureButton);
|
|
||||||
|
|
||||||
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 () => {
|
document.body.appendChild(overlay);
|
||||||
try {
|
|
||||||
// Create a canvas to capture the frame
|
captureButton.onclick = () => {
|
||||||
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();
|
|
||||||
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 {
|
try {
|
||||||
const blob = await this.processImageFile(file);
|
// Create a canvas to capture the frame
|
||||||
resolve({
|
const canvas = document.createElement("canvas");
|
||||||
blob,
|
canvas.width = video!.videoWidth;
|
||||||
fileName: file.name || "photo.jpg",
|
canvas.height = video!.videoHeight;
|
||||||
});
|
const ctx = canvas.getContext("2d");
|
||||||
} catch (error) {
|
ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height);
|
||||||
logger.error("Error processing fallback image:", error);
|
canvas.toBlob(
|
||||||
reject(new Error("Failed to process fallback image"));
|
(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();
|
||||||
|
reject(err);
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
reject(new Error("No image selected"));
|
})
|
||||||
}
|
.catch((error) => {
|
||||||
};
|
cleanup();
|
||||||
input.click();
|
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("No image selected"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]>;
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,12 @@
|
|||||||
<TopMessage />
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<main id="Content" class="p-6 pb-24 max-w-3xl mx-auto" role="main" aria-label="Account Profile">
|
<main
|
||||||
|
id="Content"
|
||||||
|
class="p-6 pb-24 max-w-3xl mx-auto"
|
||||||
|
role="main"
|
||||||
|
aria-label="Account Profile"
|
||||||
|
>
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||||
Your Identity
|
Your Identity
|
||||||
@@ -78,31 +83,28 @@
|
|||||||
:icon-size="96"
|
:icon-size="96"
|
||||||
:profile-image-url="profileImageUrl"
|
:profile-image-url="profileImageUrl"
|
||||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||||
@click="showLargeIdenticonUrl = profileImageUrl"
|
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="View profile image in large size"
|
aria-label="View profile image in large size"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@click="showLargeIdenticonUrl = profileImageUrl"
|
||||||
/>
|
/>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="trash-can"
|
icon="trash-can"
|
||||||
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
|
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
|
||||||
@click="confirmDeleteImage"
|
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="Delete profile image"
|
aria-label="Delete profile image"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@click="confirmDeleteImage"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<div v-else class="text-center">
|
<div v-else class="text-center">
|
||||||
<template v-if="isRegistered">
|
<template v-if="isRegistered">
|
||||||
<div class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" @click="openImageDialog()">
|
<div
|
||||||
<font-awesome
|
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
icon="user"
|
@click="openImageDialog()"
|
||||||
class="fa-fw"
|
>
|
||||||
/>
|
<font-awesome icon="user" class="fa-fw" />
|
||||||
<font-awesome
|
<font-awesome icon="camera" class="fa-fw" />
|
||||||
icon="camera"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -124,7 +126,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<ImageMethodDialog ref="imageMethodDialog" :isRegistered="isRegistered" />
|
<ImageMethodDialog
|
||||||
|
ref="imageMethodDialog"
|
||||||
|
:is-registered="isRegistered"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="flex justify-center text-center text-sm leading-tight mb-1">
|
<div class="flex justify-center text-center text-sm leading-tight mb-1">
|
||||||
@@ -171,14 +176,20 @@
|
|||||||
<code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
|
<code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
|
||||||
<button
|
<button
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
|
aria-label="Copy DID to clipboard"
|
||||||
@click="
|
@click="
|
||||||
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
||||||
"
|
"
|
||||||
aria-label="Copy DID to clipboard"
|
|
||||||
>
|
>
|
||||||
<font-awesome icon="copy" class="text-slate-400 fa-fw" aria-hidden="true"></font-awesome>
|
<font-awesome
|
||||||
|
icon="copy"
|
||||||
|
class="text-slate-400 fa-fw"
|
||||||
|
aria-hidden="true"
|
||||||
|
></font-awesome>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="showDidCopy" role="status" aria-live="polite">Copied</span>
|
<span v-show="showDidCopy" role="status" aria-live="polite"
|
||||||
|
>Copied</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-blue-500 text-sm font-bold">
|
<div class="text-blue-500 text-sm font-bold">
|
||||||
@@ -201,8 +212,8 @@
|
|||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Before you can publicly announce a new project or time
|
Before you can publicly announce a new project or time commitment, a
|
||||||
commitment, a friend needs to register you.
|
friend needs to register you.
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
:to="{ name: 'contact-qr' }"
|
||||||
@@ -224,19 +235,22 @@
|
|||||||
Reminder Notification
|
Reminder Notification
|
||||||
<button
|
<button
|
||||||
class="text-slate-400 fa-fw cursor-pointer"
|
class="text-slate-400 fa-fw cursor-pointer"
|
||||||
@click.stop="showReminderNotificationInfo"
|
|
||||||
aria-label="Learn more about reminder notifications"
|
aria-label="Learn more about reminder notifications"
|
||||||
|
@click.stop="showReminderNotificationInfo"
|
||||||
>
|
>
|
||||||
<font-awesome icon="question-circle" aria-hidden="true"></font-awesome>
|
<font-awesome
|
||||||
|
icon="question-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
></font-awesome>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative ml-2 cursor-pointer"
|
class="relative ml-2 cursor-pointer"
|
||||||
@click="showReminderNotificationChoice()"
|
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="notifyingReminder"
|
:aria-checked="notifyingReminder"
|
||||||
aria-label="Toggle reminder notifications"
|
aria-label="Toggle reminder notifications"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@click="showReminderNotificationChoice()"
|
||||||
>
|
>
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
|
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
|
||||||
@@ -297,7 +311,9 @@
|
|||||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||||
aria-labelledby="searchLocationHeading"
|
aria-labelledby="searchLocationHeading"
|
||||||
>
|
>
|
||||||
<h2 id="searchLocationHeading" class="mb-2 font-bold">Location for Searches</h2>
|
<h2 id="searchLocationHeading" class="mb-2 font-bold">
|
||||||
|
Location for Searches
|
||||||
|
</h2>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'search-area' }"
|
:to="{ name: 'search-area' }"
|
||||||
class="block w-full text-center bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
class="block w-full text-center bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
@@ -316,8 +332,8 @@
|
|||||||
Public Profile
|
Public Profile
|
||||||
<button
|
<button
|
||||||
class="text-slate-400 fa-fw cursor-pointer"
|
class="text-slate-400 fa-fw cursor-pointer"
|
||||||
@click="showProfileInfo"
|
|
||||||
aria-label="Learn more about public profile"
|
aria-label="Learn more about public profile"
|
||||||
|
@click="showProfileInfo"
|
||||||
>
|
>
|
||||||
<font-awesome icon="circle-info" aria-hidden="true"></font-awesome>
|
<font-awesome icon="circle-info" aria-hidden="true"></font-awesome>
|
||||||
</button>
|
</button>
|
||||||
@@ -408,9 +424,18 @@
|
|||||||
>
|
>
|
||||||
<h2 id="usageLimitsHeading" class="mb-2 font-bold">Usage Limits</h2>
|
<h2 id="usageLimitsHeading" class="mb-2 font-bold">Usage Limits</h2>
|
||||||
<!-- show spinner if loading limits -->
|
<!-- show spinner if loading limits -->
|
||||||
<div v-if="loadingLimits" class="text-center" role="status" aria-live="polite">
|
<div
|
||||||
|
v-if="loadingLimits"
|
||||||
|
class="text-center"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
Checking…
|
Checking…
|
||||||
<font-awesome icon="spinner" class="fa-spin" aria-hidden="true"></font-awesome>
|
<font-awesome
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin"
|
||||||
|
aria-hidden="true"
|
||||||
|
></font-awesome>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 text-center">
|
<div class="mb-4 text-center">
|
||||||
{{ limitsMessage }}
|
{{ limitsMessage }}
|
||||||
@@ -468,9 +493,13 @@
|
|||||||
class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
|
class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
|
||||||
@click="showAdvanced = !showAdvanced"
|
@click="showAdvanced = !showAdvanced"
|
||||||
>
|
>
|
||||||
{{ showAdvanced ? 'Hide Advanced Settings' : 'Show Advanced Settings' }}
|
{{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}
|
||||||
</h3>
|
</h3>
|
||||||
<section v-if="showAdvanced || showGeneralAdvanced" id="sectionAdvanced" aria-labelledby="advancedHeading">
|
<section
|
||||||
|
v-if="showAdvanced || showGeneralAdvanced"
|
||||||
|
id="sectionAdvanced"
|
||||||
|
aria-labelledby="advancedHeading"
|
||||||
|
>
|
||||||
<h2 id="advancedHeading" class="sr-only">Advanced Settings</h2>
|
<h2 id="advancedHeading" class="sr-only">Advanced Settings</h2>
|
||||||
<p class="text-rose-600 mb-8">
|
<p class="text-rose-600 mb-8">
|
||||||
Beware: the features here can be confusing and even change data in ways
|
Beware: the features here can be confusing and even change data in ways
|
||||||
@@ -642,8 +671,14 @@
|
|||||||
|
|
||||||
<div id="sectionClaimServer">
|
<div id="sectionClaimServer">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
|
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
|
||||||
<div class="px-4 py-4" role="group" aria-labelledby="claimServerHeading">
|
<div
|
||||||
<h3 id="claimServerHeading" class="sr-only">Claim Server Configuration</h3>
|
class="px-4 py-4"
|
||||||
|
role="group"
|
||||||
|
aria-labelledby="claimServerHeading"
|
||||||
|
>
|
||||||
|
<h3 id="claimServerHeading" class="sr-only">
|
||||||
|
Claim Server Configuration
|
||||||
|
</h3>
|
||||||
<label for="apiServerInput" class="sr-only">API Server URL</label>
|
<label for="apiServerInput" class="sr-only">API Server URL</label>
|
||||||
<input
|
<input
|
||||||
id="apiServerInput"
|
id="apiServerInput"
|
||||||
@@ -653,18 +688,15 @@
|
|||||||
aria-describedby="apiServerDescription"
|
aria-describedby="apiServerDescription"
|
||||||
placeholder="Enter API server URL"
|
placeholder="Enter API server URL"
|
||||||
/>
|
/>
|
||||||
<div
|
<div id="apiServerDescription" class="sr-only" role="tooltip">
|
||||||
id="apiServerDescription"
|
Enter the URL for the claim server. You can use the buttons below to
|
||||||
class="sr-only"
|
quickly set common server URLs.
|
||||||
role="tooltip"
|
|
||||||
>
|
|
||||||
Enter the URL for the claim server. You can use the buttons below to quickly set common server URLs.
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="apiServerInput != apiServer"
|
v-if="apiServerInput != apiServer"
|
||||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||||
@click="onClickSaveApiServer()"
|
|
||||||
aria-label="Save API server URL"
|
aria-label="Save API server URL"
|
||||||
|
@click="onClickSaveApiServer()"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="floppy-disk"
|
icon="floppy-disk"
|
||||||
@@ -676,22 +708,22 @@
|
|||||||
<div class="mt-2" role="group" aria-label="Quick server selection">
|
<div class="mt-2" role="group" aria-label="Quick server selection">
|
||||||
<button
|
<button
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
|
||||||
aria-label="Use production server URL"
|
aria-label="Use production server URL"
|
||||||
|
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
||||||
>
|
>
|
||||||
Use Prod
|
Use Prod
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
|
||||||
aria-label="Use test server URL"
|
aria-label="Use test server URL"
|
||||||
|
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
||||||
>
|
>
|
||||||
Use Test
|
Use Test
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
|
||||||
aria-label="Use local server URL"
|
aria-label="Use local server URL"
|
||||||
|
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
||||||
>
|
>
|
||||||
Use Local
|
Use Local
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 my-4"
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 my-4"
|
||||||
>
|
>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<b>Note:</b> your identity currently does <b>not</b> include a name.
|
<b>Note:</b> your identity currently does <b>not</b> include a name.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10"
|
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isInitializing"
|
v-if="cameraState === 'initializing'"
|
||||||
class="flex items-center justify-center space-x-2"
|
class="flex items-center justify-center space-x-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -112,10 +112,10 @@
|
|||||||
3.042 1.135 5.824 3 7.938l3-2.647z"
|
3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ initializationStatus }}</span>
|
<span>{{ cameraStateMessage || "Initializing camera..." }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-else-if="isScanning"
|
v-else-if="cameraState === 'active'"
|
||||||
class="flex items-center justify-center space-x-2"
|
class="flex items-center justify-center space-x-2"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -125,8 +125,19 @@
|
|||||||
</p>
|
</p>
|
||||||
<p v-else-if="error" class="text-red-400">Error: {{ error }}</p>
|
<p v-else-if="error" class="text-red-400">Error: {{ error }}</p>
|
||||||
<p v-else class="flex items-center justify-center space-x-2">
|
<p v-else class="flex items-center justify-center space-x-2">
|
||||||
<span class="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
|
<span
|
||||||
<span>Ready to scan</span>
|
:class="{
|
||||||
|
'inline-block w-2 h-2 rounded-full': true,
|
||||||
|
'bg-green-500': cameraState === 'ready',
|
||||||
|
'bg-yellow-500': cameraState === 'in_use',
|
||||||
|
'bg-red-500':
|
||||||
|
cameraState === 'error' ||
|
||||||
|
cameraState === 'permission_denied' ||
|
||||||
|
cameraState === 'not_found',
|
||||||
|
'bg-blue-500': cameraState === 'off',
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span>{{ cameraStateMessage || "Ready to scan" }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,6 +213,7 @@ import { retrieveAccountMetadata } from "../libs/util";
|
|||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||||
|
import { CameraState } from "@/services/QRScanner/types";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -239,7 +251,8 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
initializationStatus = "Initializing camera...";
|
initializationStatus = "Initializing camera...";
|
||||||
useQRReader = __USE_QR_READER__;
|
useQRReader = __USE_QR_READER__;
|
||||||
preferredCamera: "user" | "environment" = "environment";
|
preferredCamera: "user" | "environment" = "environment";
|
||||||
cameraStatus = "Initializing";
|
cameraState: CameraState = "off";
|
||||||
|
cameraStateMessage?: string;
|
||||||
|
|
||||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||||
|
|
||||||
@@ -303,19 +316,71 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
try {
|
try {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
this.isInitializing = true;
|
|
||||||
this.initializationStatus = "Initializing camera...";
|
|
||||||
this.lastScannedValue = "";
|
this.lastScannedValue = "";
|
||||||
this.lastScanTime = 0;
|
this.lastScanTime = 0;
|
||||||
|
|
||||||
const scanner = QRScannerFactory.getInstance();
|
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
|
// Check if scanning is supported first
|
||||||
if (!(await scanner.isSupported())) {
|
if (!(await scanner.isSupported())) {
|
||||||
this.error =
|
this.error =
|
||||||
"Camera access requires HTTPS. Please use a secure connection.";
|
"Camera access requires HTTPS. Please use a secure connection.";
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
this.isInitializing = false;
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -328,40 +393,11 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
return;
|
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
|
// Start scanning
|
||||||
await scanner.startScan();
|
await scanner.startScan();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error instanceof Error ? error.message : String(error);
|
this.error = error instanceof Error ? error.message : String(error);
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
this.isInitializing = false;
|
|
||||||
logger.error("Error starting scan:", {
|
logger.error("Error starting scan:", {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
@@ -828,12 +864,12 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
try {
|
try {
|
||||||
await promise;
|
await promise;
|
||||||
this.isInitializing = false;
|
this.isInitializing = false;
|
||||||
this.cameraStatus = "Ready";
|
this.cameraState = "ready";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const wrappedError =
|
const wrappedError =
|
||||||
error instanceof Error ? error : new Error(String(error));
|
error instanceof Error ? error : new Error(String(error));
|
||||||
this.error = wrappedError.message;
|
this.error = wrappedError.message;
|
||||||
this.cameraStatus = "Error";
|
this.cameraState = "error";
|
||||||
this.isInitializing = false;
|
this.isInitializing = false;
|
||||||
logger.error("Error during QR scanner initialization:", {
|
logger.error("Error during QR scanner initialization:", {
|
||||||
error: wrappedError.message,
|
error: wrappedError.message,
|
||||||
@@ -843,17 +879,17 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCameraOn(): void {
|
onCameraOn(): void {
|
||||||
this.cameraStatus = "Active";
|
this.cameraState = "active";
|
||||||
this.isInitializing = false;
|
this.isInitializing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onCameraOff(): void {
|
onCameraOff(): void {
|
||||||
this.cameraStatus = "Off";
|
this.cameraState = "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
onDetect(result: unknown): void {
|
onDetect(result: unknown): void {
|
||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
this.cameraStatus = "Detecting";
|
this.cameraState = "detecting";
|
||||||
try {
|
try {
|
||||||
let rawValue: string | undefined;
|
let rawValue: string | undefined;
|
||||||
if (
|
if (
|
||||||
@@ -874,7 +910,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
} finally {
|
} finally {
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
this.cameraStatus = "Active";
|
this.cameraState = "active";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,12 +933,12 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const wrappedError =
|
const wrappedError =
|
||||||
error instanceof Error ? error : new Error(String(error));
|
error instanceof Error ? error : new Error(String(error));
|
||||||
this.error = wrappedError.message;
|
this.error = wrappedError.message;
|
||||||
this.cameraStatus = "Error";
|
this.cameraState = "error";
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error: Error): void {
|
onError(error: Error): void {
|
||||||
this.error = error.message;
|
this.error = error.message;
|
||||||
this.cameraStatus = "Error";
|
this.cameraState = "error";
|
||||||
logger.error("QR code scan error:", {
|
logger.error("QR code scan error:", {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { VALID_DEEP_LINK_ROUTES } from "../types/deepLinks";
|
import { VALID_DEEP_LINK_ROUTES } from "../interfaces/deepLinks";
|
||||||
import { logConsoleAndDb } from "../db";
|
import { logConsoleAndDb } from "../db";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user