You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
533 lines
13 KiB
533 lines
13 KiB
---
|
|
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
|
|
|