forked from jsnbuchanan/crowd-funder-for-time-pwa
Merge branch 'qrcode-reboot' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into qrcode-reboot
This commit is contained in:
@@ -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
|
|
||||||
284
doc/qr-code-implementation-guide.md
Normal file
284
doc/qr-code-implementation-guide.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# QR Code Implementation Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the QR code scanning and generation implementation in the TimeSafari application. The system uses a platform-agnostic design with specific implementations for web and mobile platforms.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── services/
|
||||||
|
│ └── QRScanner/
|
||||||
|
│ ├── types.ts # Core interfaces and types
|
||||||
|
│ ├── QRScannerFactory.ts # Factory for creating scanner instances
|
||||||
|
│ ├── CapacitorQRScanner.ts # Mobile implementation using MLKit
|
||||||
|
│ ├── WebInlineQRScanner.ts # Web implementation using MediaDevices API
|
||||||
|
│ └── interfaces.ts # Additional interfaces
|
||||||
|
├── components/
|
||||||
|
│ └── QRScanner/
|
||||||
|
│ └── QRScannerDialog.vue # Shared UI component
|
||||||
|
└── views/
|
||||||
|
├── ContactQRScanView.vue # Dedicated scanning view
|
||||||
|
└── ContactQRScanShowView.vue # Combined QR display and scanning view
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **Factory Pattern**
|
||||||
|
- `QRScannerFactory` - Creates appropriate scanner instance based on platform
|
||||||
|
- Common interface `QRScannerService` implemented by all scanners
|
||||||
|
- Platform detection via Capacitor and build flags
|
||||||
|
|
||||||
|
2. **Platform-Specific Implementations**
|
||||||
|
- `CapacitorQRScanner` - Native mobile implementation using MLKit
|
||||||
|
- `WebInlineQRScanner` - Web browser implementation using MediaDevices API
|
||||||
|
- `QRScannerDialog.vue` - Shared UI component
|
||||||
|
|
||||||
|
3. **View Components**
|
||||||
|
- `ContactQRScanView` - Dedicated view for scanning QR codes
|
||||||
|
- `ContactQRScanShowView` - Combined view for displaying and scanning QR codes
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Core Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface QRScannerService {
|
||||||
|
checkPermissions(): Promise<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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Implementations
|
||||||
|
|
||||||
|
#### Mobile (Capacitor)
|
||||||
|
- Uses `@capacitor-mlkit/barcode-scanning`
|
||||||
|
- Native camera access through platform APIs
|
||||||
|
- Optimized for mobile performance
|
||||||
|
- Supports both iOS and Android
|
||||||
|
- Real-time QR code detection
|
||||||
|
- Back camera preferred for scanning
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
```typescript
|
||||||
|
// capacitor.config.ts
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
plugins: {
|
||||||
|
MLKitBarcodeScanner: {
|
||||||
|
formats: ['QR_CODE'],
|
||||||
|
detectorSize: 1.0,
|
||||||
|
lensFacing: 'back',
|
||||||
|
googleBarcodeScannerModuleInstallState: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web
|
||||||
|
- Uses browser's MediaDevices API
|
||||||
|
- Vue.js components for UI
|
||||||
|
- EventEmitter for stream management
|
||||||
|
- Browser-based camera access
|
||||||
|
- Inline camera preview
|
||||||
|
- Responsive design
|
||||||
|
- Cross-browser compatibility
|
||||||
|
|
||||||
|
### View Components
|
||||||
|
|
||||||
|
#### ContactQRScanView
|
||||||
|
- Dedicated view for scanning QR codes
|
||||||
|
- Full-screen camera interface
|
||||||
|
- Simple UI focused on scanning
|
||||||
|
- Used primarily on native platforms
|
||||||
|
- Streamlined scanning experience
|
||||||
|
|
||||||
|
#### ContactQRScanShowView
|
||||||
|
- Combined view for QR code display and scanning
|
||||||
|
- Shows user's own QR code
|
||||||
|
- Handles user registration status
|
||||||
|
- Provides options to copy contact information
|
||||||
|
- Platform-specific scanning implementation:
|
||||||
|
- Native: Button to navigate to ContactQRScanView
|
||||||
|
- Web: Built-in scanning functionality
|
||||||
|
|
||||||
|
### QR Code Workflow
|
||||||
|
|
||||||
|
1. **Initiation**
|
||||||
|
- User selects "Scan QR Code" option
|
||||||
|
- Platform-specific scanner is initialized
|
||||||
|
- Camera permissions are verified
|
||||||
|
- Appropriate scanner component is loaded
|
||||||
|
|
||||||
|
2. **Platform-Specific Implementation**
|
||||||
|
- Web: Uses `qrcode-stream` for real-time scanning
|
||||||
|
- Native: Uses `@capacitor-mlkit/barcode-scanning`
|
||||||
|
|
||||||
|
3. **Scanning Process**
|
||||||
|
- Camera stream initialization
|
||||||
|
- Real-time frame analysis
|
||||||
|
- QR code detection and decoding
|
||||||
|
- Validation of QR code format
|
||||||
|
- Processing of contact information
|
||||||
|
|
||||||
|
4. **Contact Processing**
|
||||||
|
- Decryption of contact data
|
||||||
|
- Validation of user information
|
||||||
|
- Verification of timestamp
|
||||||
|
- Check for duplicate contacts
|
||||||
|
- Processing of shared data
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
### Common Vite Configuration
|
||||||
|
```typescript
|
||||||
|
// vite.config.common.mts
|
||||||
|
export async function createBuildConfig(mode: string) {
|
||||||
|
const isCapacitor = mode === "capacitor";
|
||||||
|
|
||||||
|
return defineConfig({
|
||||||
|
define: {
|
||||||
|
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||||
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative),
|
||||||
|
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||||
|
__USE_QR_READER__: JSON.stringify(!isCapacitor)
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: [
|
||||||
|
'@capacitor-mlkit/barcode-scanning',
|
||||||
|
'vue-qrcode-reader'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Builds
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build:web": "vite build --config vite.config.web.mts",
|
||||||
|
"build:capacitor": "vite build --config vite.config.capacitor.mts",
|
||||||
|
"build:all": "npm run build:web && npm run build:capacitor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Scenarios
|
||||||
|
1. No camera found
|
||||||
|
2. Permission denied
|
||||||
|
3. Camera in use by another application
|
||||||
|
4. HTTPS required
|
||||||
|
5. Browser compatibility issues
|
||||||
|
6. Invalid QR code format
|
||||||
|
7. Expired QR codes
|
||||||
|
8. Duplicate contact attempts
|
||||||
|
9. Network connectivity issues
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
- User-friendly error messages
|
||||||
|
- Troubleshooting tips
|
||||||
|
- Clear instructions for resolution
|
||||||
|
- Platform-specific guidance
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### QR Code Security
|
||||||
|
- Encryption of contact data
|
||||||
|
- Timestamp validation
|
||||||
|
- Version checking
|
||||||
|
- User verification
|
||||||
|
- Rate limiting for scans
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- Secure transmission of contact data
|
||||||
|
- Validation of QR code authenticity
|
||||||
|
- Prevention of duplicate scans
|
||||||
|
- Protection against malicious codes
|
||||||
|
- Secure storage of contact information
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Camera Access
|
||||||
|
1. Always check for camera availability
|
||||||
|
2. Request permissions explicitly
|
||||||
|
3. Handle all error conditions
|
||||||
|
4. Provide clear user feedback
|
||||||
|
5. Implement proper cleanup
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
1. Optimize camera resolution
|
||||||
|
2. Implement proper resource cleanup
|
||||||
|
3. Handle camera switching efficiently
|
||||||
|
4. Manage memory usage
|
||||||
|
5. Battery usage optimization
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
1. Clear visual feedback
|
||||||
|
2. Camera preview
|
||||||
|
3. Scanning status indicators
|
||||||
|
4. Error messages
|
||||||
|
5. Success confirmations
|
||||||
|
6. Intuitive camera controls
|
||||||
|
7. Smooth camera switching
|
||||||
|
8. Responsive UI feedback
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Scenarios
|
||||||
|
1. Permission handling
|
||||||
|
2. Camera switching
|
||||||
|
3. Error conditions
|
||||||
|
4. Platform compatibility
|
||||||
|
5. Performance metrics
|
||||||
|
6. QR code detection
|
||||||
|
7. Contact processing
|
||||||
|
8. Security validation
|
||||||
|
|
||||||
|
### Test Environment
|
||||||
|
- Multiple browsers
|
||||||
|
- iOS and Android devices
|
||||||
|
- Various network conditions
|
||||||
|
- Different camera configurations
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Key Packages
|
||||||
|
- `@capacitor-mlkit/barcode-scanning`
|
||||||
|
- `qrcode-stream`
|
||||||
|
- `vue-qrcode-reader`
|
||||||
|
- Platform-specific camera APIs
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Regular Updates
|
||||||
|
- Keep dependencies updated
|
||||||
|
- Monitor platform changes
|
||||||
|
- Update documentation
|
||||||
|
- Review security patches
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
- Track memory usage
|
||||||
|
- Monitor camera performance
|
||||||
|
- Check error rates
|
||||||
|
- Analyze user feedback
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
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 +21,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 +46,35 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateCameraState(state: CameraState, message?: string) {
|
||||||
|
this.currentState = state;
|
||||||
|
this.currentStateMessage = message;
|
||||||
|
this.cameraStateListeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener.onStateChange(state, message);
|
||||||
|
logger.info(`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`, {
|
||||||
|
state,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[WebInlineQRScanner:${this.id}] Error in camera state listener:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addCameraStateListener(listener: CameraStateListener): void {
|
||||||
|
this.cameraStateListeners.add(listener);
|
||||||
|
// Immediately notify the new listener of current state
|
||||||
|
listener.onStateChange(this.currentState, this.currentStateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCameraStateListener(listener: CameraStateListener): void {
|
||||||
|
this.cameraStateListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
async checkPermissions(): Promise<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 +85,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 +96,14 @@ 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 +141,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) => {
|
||||||
@@ -122,36 +154,20 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} 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 === "DevicesNotFoundError") {
|
||||||
wrappedError.name === "NotFoundError" ||
|
this.updateCameraState('not_found', 'No camera found on this device');
|
||||||
wrappedError.name === "DevicesNotFoundError"
|
|
||||||
) {
|
|
||||||
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 === "PermissionDeniedError") {
|
||||||
wrappedError.name === "NotAllowedError" ||
|
this.updateCameraState('permission_denied', 'Camera access denied');
|
||||||
wrappedError.name === "PermissionDeniedError"
|
throw new Error("Camera access denied. Please grant camera permission and try again");
|
||||||
) {
|
} else if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") {
|
||||||
throw new Error(
|
this.updateCameraState('in_use', 'Camera is in use by another application');
|
||||||
"Camera access denied. Please grant camera permission and try again",
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
wrappedError.name === "NotReadableError" ||
|
|
||||||
wrappedError.name === "TrackStartError"
|
|
||||||
) {
|
|
||||||
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 +406,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 +421,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,
|
||||||
@@ -429,13 +448,15 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.scanQRCode();
|
this.scanQRCode();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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:`, {
|
// Update state based on error type
|
||||||
error: wrappedError.message,
|
if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") {
|
||||||
stack: wrappedError.stack,
|
this.updateCameraState('in_use', 'Camera is in use by another application');
|
||||||
name: wrappedError.name,
|
} else {
|
||||||
});
|
this.updateCameraState('error', wrappedError.message);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.scanListener?.onError) {
|
if (this.scanListener?.onError) {
|
||||||
this.scanListener.onError(wrappedError);
|
this.scanListener.onError(wrappedError);
|
||||||
}
|
}
|
||||||
@@ -492,14 +513,9 @@ 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(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, error);
|
||||||
error instanceof Error ? error : new Error(String(error));
|
this.updateCameraState('error', 'Error stopping camera');
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, {
|
throw error;
|
||||||
error: wrappedError.message,
|
|
||||||
stack: wrappedError.stack,
|
|
||||||
name: wrappedError.name,
|
|
||||||
});
|
|
||||||
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 +557,9 @@ 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(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, error);
|
||||||
error: error instanceof Error ? error.message : String(error),
|
this.updateCameraState('error', 'Error during cleanup');
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,14 @@
|
|||||||
</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 :class="{
|
||||||
<span>Ready to scan</span>
|
'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 +208,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 +246,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 +311,70 @@ 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 +387,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 +858,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 +873,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 +904,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 +927,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,
|
||||||
|
|||||||
Reference in New Issue
Block a user