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

---
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