docs: consolidate QR code implementation documentation

Merge multiple QR code documentation files into a single comprehensive guide
that accurately reflects the current implementation. The consolidated guide:

- Combines information from qr-code-implementation-guide.mdc,
  qr-code-handling-rule.mdc, and camera-implementation.md
- Clarifies the relationship between ContactQRScanView and ContactQRScanShowView
- Streamlines build configuration documentation
- Adds detailed sections on error handling, security, and best practices
- Improves organization and readability of implementation details
- Removes redundant information while preserving critical details

This change improves documentation maintainability and provides a single
source of truth for QR code implementation details.
This commit is contained in:
Matt Raymer
2025-05-19 06:28:46 -04:00
parent 16818b460d
commit 8d2ccaa063
7 changed files with 302 additions and 1391 deletions

View File

@@ -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;
}

View File

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