forked from trent_larson/crowd-funder-for-time-pwa
chore: updates for qr code reader rules, linting, and cleanup
This commit is contained in:
@@ -9,15 +9,33 @@ alwaysApply: true
|
||||
|
||||
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) | PyWebView (Desktop) |
|
||||
|---------|-----------|-------------------|-------------------|-------------------|
|
||||
| QR Code Scanning | vue-qrcode-reader | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented |
|
||||
| QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented |
|
||||
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented | Not Implemented |
|
||||
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs | PyWebView Python Bridge |
|
||||
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented | Not Implemented |
|
||||
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks | process.env checks |
|
||||
|
||||
## 2. Build Configuration Structure
|
||||
## 2. Project Structure
|
||||
|
||||
### 2.1 Entry Points
|
||||
### 2.1 Core Directories
|
||||
```
|
||||
src/
|
||||
├── components/ # Vue components
|
||||
├── services/ # Platform services and business logic
|
||||
├── views/ # Page components
|
||||
├── router/ # Vue router configuration
|
||||
├── types/ # TypeScript type definitions
|
||||
├── utils/ # Utility functions
|
||||
├── lib/ # Core libraries
|
||||
├── platforms/ # Platform-specific implementations
|
||||
├── electron/ # Electron-specific code
|
||||
├── constants/ # Application constants
|
||||
├── db/ # Database related code
|
||||
├── interfaces/ # TypeScript interfaces
|
||||
└── assets/ # Static assets
|
||||
```
|
||||
|
||||
### 2.2 Entry Points
|
||||
```
|
||||
src/
|
||||
├── main.ts # Base entry
|
||||
@@ -28,19 +46,35 @@ src/
|
||||
└── main.web.ts # Web/PWA entry
|
||||
```
|
||||
|
||||
### 2.2 Build Configurations
|
||||
### 2.3 Build Configurations
|
||||
```
|
||||
root/
|
||||
├── vite.config.common.mts # Shared config
|
||||
├── vite.config.capacitor.mts # Mobile build
|
||||
├── vite.config.electron.mts # Electron build
|
||||
├── vite.config.pywebview.mts # PyWebView build
|
||||
└── vite.config.web.mts # Web/PWA build
|
||||
├── vite.config.web.mts # Web/PWA build
|
||||
└── vite.config.utils.mts # Build utilities
|
||||
```
|
||||
|
||||
## 3. Platform Service Architecture
|
||||
## 3. Service Architecture
|
||||
|
||||
### 3.1 Service Factory Pattern
|
||||
### 3.1 Service Organization
|
||||
```
|
||||
services/
|
||||
├── QRScanner/ # QR code scanning service
|
||||
│ ├── WebInlineQRScanner.ts
|
||||
│ └── interfaces.ts
|
||||
├── platforms/ # Platform-specific services
|
||||
│ ├── WebPlatformService.ts
|
||||
│ ├── CapacitorPlatformService.ts
|
||||
│ ├── ElectronPlatformService.ts
|
||||
│ └── PyWebViewPlatformService.ts
|
||||
└── factory/ # Service factories
|
||||
└── PlatformServiceFactory.ts
|
||||
```
|
||||
|
||||
### 3.2 Service Factory Pattern
|
||||
```typescript
|
||||
// PlatformServiceFactory.ts
|
||||
export class PlatformServiceFactory {
|
||||
@@ -56,40 +90,34 @@ export class PlatformServiceFactory {
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Platform-Specific Implementations
|
||||
```
|
||||
services/platforms/
|
||||
├── WebPlatformService.ts
|
||||
├── CapacitorPlatformService.ts
|
||||
├── ElectronPlatformService.ts
|
||||
└── PyWebViewPlatformService.ts
|
||||
```
|
||||
|
||||
## 4. Feature Implementation Guidelines
|
||||
|
||||
### 4.1 QR Code Scanning
|
||||
|
||||
1. **Factory Pattern**
|
||||
1. **Service Interface**
|
||||
```typescript
|
||||
export class QRScannerFactory {
|
||||
static getInstance(): QRScannerService {
|
||||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
|
||||
return new CapacitorQRScanner();
|
||||
} else if (__USE_QR_READER__) {
|
||||
return new WebDialogQRScanner();
|
||||
}
|
||||
throw new Error("No QR scanner implementation available");
|
||||
}
|
||||
interface QRScannerService {
|
||||
checkPermissions(): Promise<boolean>;
|
||||
requestPermissions(): Promise<boolean>;
|
||||
isSupported(): Promise<boolean>;
|
||||
startScan(): Promise<void>;
|
||||
stopScan(): Promise<void>;
|
||||
addListener(listener: ScanListener): void;
|
||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Platform-Specific Implementation**
|
||||
```typescript
|
||||
// Example for Capacitor
|
||||
export class CapacitorQRScanner implements QRScannerService {
|
||||
async startScan(options?: QRScannerOptions): Promise<void> {
|
||||
// Platform-specific implementation
|
||||
}
|
||||
// WebInlineQRScanner.ts
|
||||
export class WebInlineQRScanner implements QRScannerService {
|
||||
private scanListener: ScanListener | null = null;
|
||||
private isScanning = false;
|
||||
private stream: MediaStream | null = null;
|
||||
private events = new EventEmitter();
|
||||
|
||||
// Implementation of interface methods
|
||||
}
|
||||
```
|
||||
|
||||
@@ -202,6 +230,8 @@ if (process.env.VITE_PLATFORM === 'capacitor') {
|
||||
- Use platform-specific directories for unique implementations
|
||||
- Share common code through service interfaces
|
||||
- Implement feature detection before using platform capabilities
|
||||
- Keep platform-specific code isolated in dedicated directories
|
||||
- Use TypeScript interfaces for cross-platform compatibility
|
||||
|
||||
### 8.2 Platform Detection
|
||||
```typescript
|
||||
@@ -219,6 +249,7 @@ if (capabilities.hasCamera) {
|
||||
3. Use factory pattern for instantiation
|
||||
4. Implement graceful fallbacks
|
||||
5. Add comprehensive error handling
|
||||
6. Use dependency injection for better testability
|
||||
|
||||
## 9. Dependency Management
|
||||
|
||||
@@ -228,7 +259,7 @@ if (capabilities.hasCamera) {
|
||||
"dependencies": {
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"electron": "^33.2.1",
|
||||
"vue-qrcode-reader": "^5.5.3"
|
||||
"vue": "^3.4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -253,8 +284,9 @@ async checkPermissions(): Promise<boolean> {
|
||||
```
|
||||
|
||||
### 10.2 Data Storage
|
||||
- Use platform-appropriate storage mechanisms
|
||||
- Implement encryption for sensitive data
|
||||
- Handle permissions appropriately
|
||||
- Use secure storage mechanisms for sensitive data
|
||||
- Implement proper encryption for stored data
|
||||
- Follow platform-specific security guidelines
|
||||
- Regular security audits and updates
|
||||
|
||||
This document should be updated as new features are added or platform-specific implementations change. Regular reviews ensure it remains current with the codebase.
|
||||
|
||||
@@ -14,11 +14,11 @@ The QR code scanning functionality follows a platform-agnostic design using a fa
|
||||
1. **Factory Pattern**
|
||||
- `QRScannerFactory` - Creates appropriate scanner instance based on platform
|
||||
- Common interface `QRScannerService` implemented by all scanners
|
||||
- Platform detection via Vite config flags: `__USE_QR_READER__` and `__IS_MOBILE__`
|
||||
- Platform detection via Capacitor and build flags
|
||||
|
||||
2. **Platform-Specific Implementations**
|
||||
- `CapacitorQRScanner` - Native mobile implementation
|
||||
- `WebDialogQRScanner` - Web browser implementation
|
||||
- `CapacitorQRScanner` - Native mobile implementation using MLKit
|
||||
- `WebInlineQRScanner` - Web browser implementation using MediaDevices API
|
||||
- `QRScannerDialog.vue` - Shared UI component
|
||||
|
||||
## Mobile Implementation (Capacitor)
|
||||
@@ -54,13 +54,13 @@ MLKitBarcodeScanner: {
|
||||
## Web Implementation
|
||||
|
||||
### Technology Stack
|
||||
- Uses `vue-qrcode-reader` library
|
||||
- Browser's MediaDevices API
|
||||
- Vue.js dialog component
|
||||
- Uses browser's MediaDevices API
|
||||
- Vue.js components for UI
|
||||
- EventEmitter for stream management
|
||||
|
||||
### Key Features
|
||||
- Browser-based camera access
|
||||
- Fallback UI for unsupported browsers
|
||||
- Inline camera preview
|
||||
- Responsive design
|
||||
- Cross-browser compatibility
|
||||
- Progressive enhancement
|
||||
@@ -97,9 +97,10 @@ MLKitBarcodeScanner: {
|
||||
|
||||
### Platform Detection
|
||||
```typescript
|
||||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
|
||||
const isNative = QRScannerFactory.isNativePlatform();
|
||||
if (isNative) {
|
||||
// Use native scanner
|
||||
} else if (__USE_QR_READER__) {
|
||||
} else {
|
||||
// Use web scanner
|
||||
}
|
||||
```
|
||||
@@ -112,6 +113,9 @@ await scanner.startScan();
|
||||
scanner.addListener({
|
||||
onScan: (result) => {
|
||||
// Handle scan result
|
||||
},
|
||||
onError: (error) => {
|
||||
// Handle error
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -145,4 +149,29 @@ scanner.addListener({
|
||||
2. Verify permission flows
|
||||
3. Check error handling
|
||||
4. Validate cleanup
|
||||
5. Verify cross-platform behavior
|
||||
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;
|
||||
}
|
||||
@@ -9,19 +9,16 @@ alwaysApply: true
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ └── QRScanner/
|
||||
│ ├── types.ts
|
||||
│ ├── factory.ts
|
||||
│ ├── CapacitorScanner.ts
|
||||
│ ├── WebDialogScanner.ts
|
||||
│ └── QRScannerDialog.vue
|
||||
├── services/
|
||||
│ └── QRScanner/
|
||||
│ ├── types.ts
|
||||
│ ├── QRScannerFactory.ts
|
||||
│ ├── CapacitorQRScanner.ts
|
||||
│ └── WebDialogQRScanner.ts
|
||||
│ ├── 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
|
||||
@@ -33,13 +30,20 @@ export interface ScanListener {
|
||||
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(): Promise<void>;
|
||||
startScan(options?: QRScannerOptions): Promise<void>;
|
||||
stopScan(): Promise<void>;
|
||||
addListener(listener: ScanListener): void;
|
||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
```
|
||||
@@ -48,18 +52,17 @@ export interface QRScannerService {
|
||||
|
||||
### Vite Configuration
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__USE_QR_READER__: JSON.stringify(!isMobile),
|
||||
__IS_MOBILE__: JSON.stringify(isMobile),
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: isMobile ? ['vue-qrcode-reader'] : [],
|
||||
// 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
|
||||
@@ -81,7 +84,7 @@ const config: CapacitorConfig = {
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
npm install @capacitor-mlkit/barcode-scanning vue-qrcode-reader
|
||||
npm install @capacitor-mlkit/barcode-scanning
|
||||
```
|
||||
|
||||
2. **Create Core Types**
|
||||
@@ -93,20 +96,34 @@ Create the interface files as shown above.
|
||||
export class QRScannerFactory {
|
||||
private static instance: QRScannerService | null = null;
|
||||
|
||||
static getInstance(): QRScannerService {
|
||||
if (!this.instance) {
|
||||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
|
||||
this.instance = new CapacitorQRScanner();
|
||||
} else if (__USE_QR_READER__) {
|
||||
this.instance = new WebDialogQRScanner();
|
||||
} else {
|
||||
throw new Error('No QR scanner implementation available');
|
||||
}
|
||||
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;
|
||||
}
|
||||
return this.instance;
|
||||
|
||||
// For other platforms, use native if available
|
||||
return capacitorNative || isMobile;
|
||||
}
|
||||
|
||||
static async cleanup() {
|
||||
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;
|
||||
@@ -122,65 +139,104 @@ 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() {
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
try {
|
||||
const { camera } = await BarcodeScanner.checkPermissions();
|
||||
return camera === 'granted';
|
||||
return camera === "granted";
|
||||
} catch (error) {
|
||||
logger.error('Error checking camera permissions:', error);
|
||||
logger.error("Error checking camera permissions:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermissions() {
|
||||
async requestPermissions(): Promise<boolean> {
|
||||
try {
|
||||
if (await this.checkPermissions()) {
|
||||
return true;
|
||||
}
|
||||
const { camera } = await BarcodeScanner.requestPermissions();
|
||||
return camera === 'granted';
|
||||
return camera === "granted";
|
||||
} catch (error) {
|
||||
logger.error('Error requesting camera permissions:', error);
|
||||
logger.error("Error requesting camera permissions:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async isSupported() {
|
||||
return Capacitor.isNativePlatform();
|
||||
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() {
|
||||
async startScan(options?: QRScannerOptions): Promise<void> {
|
||||
if (this.isScanning) return;
|
||||
this.isScanning = true;
|
||||
if (this.cleanupPromise) {
|
||||
await this.cleanupPromise;
|
||||
}
|
||||
|
||||
try {
|
||||
await BarcodeScanner.startScan();
|
||||
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() {
|
||||
async stopScan(): Promise<void> {
|
||||
if (!this.isScanning) return;
|
||||
this.isScanning = false;
|
||||
|
||||
try {
|
||||
await BarcodeScanner.stopScan();
|
||||
} catch (error) {
|
||||
logger.error('Error stopping scan:', error);
|
||||
logger.error("Error stopping scan:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
addListener(listener: ScanListener) {
|
||||
addListener(listener: ScanListener): void {
|
||||
this.scanListener = listener;
|
||||
const handle = BarcodeScanner.addListener('barcodeScanned', (result) => {
|
||||
if (this.scanListener) {
|
||||
this.scanListener.onScan(result.barcode);
|
||||
}
|
||||
});
|
||||
this.listenerHandles.push(handle.remove);
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
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();
|
||||
@@ -193,31 +249,40 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
|
||||
5. **Implement Web Scanner**
|
||||
```typescript
|
||||
// WebDialogQRScanner.ts
|
||||
export class WebDialogQRScanner implements QRScannerService {
|
||||
private dialogInstance: App | null = null;
|
||||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
|
||||
// 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
|
||||
name: "camera" as PermissionName,
|
||||
});
|
||||
return permissions.state === 'granted';
|
||||
return permissions.state === "granted";
|
||||
} catch (error) {
|
||||
logger.error('Error checking camera permissions:', error);
|
||||
logger.error("Error checking camera permissions:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermissions(): Promise<boolean> {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
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);
|
||||
logger.error("Error requesting camera permissions:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -226,208 +291,150 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
|
||||
}
|
||||
|
||||
async startScan() {
|
||||
if (this.dialogInstance) return;
|
||||
async startScan(): Promise<void> {
|
||||
if (this.isScanning) return;
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this.dialogInstance = createApp(QRScannerDialog, {
|
||||
onScan: (result: string) => {
|
||||
if (this.scanListener) {
|
||||
this.scanListener.onScan(result);
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
if (this.scanListener?.onError) {
|
||||
this.scanListener.onError(error);
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
this.cleanup();
|
||||
async stopScan(): Promise<void> {
|
||||
if (!this.isScanning) return;
|
||||
|
||||
try {
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.dialogComponent = this.dialogInstance.mount(container) as InstanceType<typeof QRScannerDialog>;
|
||||
this.events.emit("stream", null);
|
||||
} catch (error) {
|
||||
logger.error("Error stopping scan:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
async stopScan() {
|
||||
await this.cleanup();
|
||||
}
|
||||
|
||||
addListener(listener: ScanListener) {
|
||||
addListener(listener: ScanListener): void {
|
||||
this.scanListener = listener;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.dialogInstance) {
|
||||
this.dialogInstance.unmount();
|
||||
this.dialogInstance = null;
|
||||
this.dialogComponent = null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Create Dialog Component**
|
||||
```vue
|
||||
<!-- QRScannerDialog.vue -->
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div class="dialog relative">
|
||||
<div class="dialog-header">
|
||||
<h2>Scan QR Code</h2>
|
||||
<button @click="onClose" class="close-button">×</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div v-if="useQRReader">
|
||||
<qrcode-stream
|
||||
class="w-full max-w-lg mx-auto"
|
||||
@detect="onScanDetect"
|
||||
@error="onScanError"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button @click="startMobileScan" class="scan-button">
|
||||
Start Camera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { QrcodeStream } from 'vue-qrcode-reader';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QRScannerDialog',
|
||||
components: { QrcodeStream },
|
||||
props: {
|
||||
onScan: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: true,
|
||||
useQRReader: __USE_QR_READER__
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onScanDetect(promisedResult: Promise<string>) {
|
||||
promisedResult
|
||||
.then(result => this.onScan(result))
|
||||
.catch(error => this.onError(error));
|
||||
},
|
||||
onScanError(error: Error) {
|
||||
this.onError(error);
|
||||
},
|
||||
async startMobileScan() {
|
||||
try {
|
||||
const scanner = QRScannerFactory.getInstance();
|
||||
await scanner.startScan();
|
||||
} catch (error) {
|
||||
this.onError(error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scan-button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
// In your component
|
||||
async function scanQRCode() {
|
||||
const scanner = QRScannerFactory.getInstance();
|
||||
|
||||
if (!(await scanner.checkPermissions())) {
|
||||
const granted = await scanner.requestPermissions();
|
||||
if (!granted) {
|
||||
throw new Error('Camera permission denied');
|
||||
// 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();
|
||||
}
|
||||
|
||||
scanner.addListener({
|
||||
onScan: (result) => {
|
||||
console.log('Scanned:', result);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Scan error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
await scanner.startScan();
|
||||
}
|
||||
|
||||
// Cleanup when done
|
||||
onUnmounted(() => {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user