Files
crowd-funder-from-jason/.cursor/rules/qr-code-implementation-guide.mdc
Matthew Raymer a8812714a3 fix(qr): improve QR scanner implementation and error handling
- Implement robust QR scanner factory with platform detection
- Add proper camera permissions to Android manifest
- Improve error handling and logging across scanner implementations
- Add continuous scanning mode for Capacitor/MLKit scanner
- Enhance UI feedback during scanning process
- Fix build configuration for proper platform detection
- Clean up resources properly in scanner components
- Add TypeScript improvements and error wrapping

The changes include:
- Adding CAMERA permission to AndroidManifest.xml
- Setting proper build flags (__IS_MOBILE__, __USE_QR_READER__)
- Implementing continuous scanning mode for better UX
- Adding proper cleanup of scanner resources
- Improving error handling and type safety
- Enhancing UI with loading states and error messages
2025-04-22 10:00:37 +00:00

527 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
description:
globs:
alwaysApply: true
---
# QR Code Implementation Guide
## Directory Structure
```
src/
├── components/
│ └── QRScanner/
│ ├── types.ts
│ ├── factory.ts
│ ├── CapacitorScanner.ts
│ ├── WebDialogScanner.ts
│ └── QRScannerDialog.vue
├── services/
│ └── QRScanner/
│ ├── types.ts
│ ├── QRScannerFactory.ts
│ ├── CapacitorQRScanner.ts
│ └── WebDialogQRScanner.ts
```
## Core Interfaces
```typescript
// types.ts
export interface ScanListener {
onScan: (result: string) => void;
onError?: (error: Error) => void;
}
export interface QRScannerService {
checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>;
startScan(): Promise<void>;
stopScan(): Promise<void>;
addListener(listener: ScanListener): void;
cleanup(): Promise<void>;
}
```
## Configuration Files
### 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'] : [],
}
}
});
```
### 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 vue-qrcode-reader
```
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;
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');
}
}
return this.instance;
}
static async cleanup() {
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>> = [];
async checkPermissions() {
try {
const { camera } = await BarcodeScanner.checkPermissions();
return camera === 'granted';
} catch (error) {
logger.error('Error checking camera permissions:', error);
return false;
}
}
async requestPermissions() {
try {
const { camera } = await BarcodeScanner.requestPermissions();
return camera === 'granted';
} catch (error) {
logger.error('Error requesting camera permissions:', error);
return false;
}
}
async isSupported() {
return Capacitor.isNativePlatform();
}
async startScan() {
if (this.isScanning) return;
this.isScanning = true;
try {
await BarcodeScanner.startScan();
} catch (error) {
this.isScanning = false;
throw error;
}
}
async stopScan() {
if (!this.isScanning) return;
this.isScanning = false;
try {
await BarcodeScanner.stopScan();
} catch (error) {
logger.error('Error stopping scan:', error);
}
}
addListener(listener: ScanListener) {
this.scanListener = listener;
const handle = BarcodeScanner.addListener('barcodeScanned', (result) => {
if (this.scanListener) {
this.scanListener.onScan(result.barcode);
}
});
this.listenerHandles.push(handle.remove);
}
async cleanup() {
await this.stopScan();
for (const handle of this.listenerHandles) {
await handle();
}
this.listenerHandles = [];
this.scanListener = null;
}
}
```
5. **Implement Web Scanner**
```typescript
// WebDialogQRScanner.ts
export class WebDialogQRScanner implements QRScannerService {
private dialogInstance: App | null = null;
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
private scanListener: ScanListener | null = null;
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: true });
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() {
if (this.dialogInstance) return;
const container = document.createElement('div');
document.body.appendChild(container);
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();
}
});
this.dialogComponent = this.dialogInstance.mount(container) as InstanceType<typeof QRScannerDialog>;
}
async stopScan() {
await this.cleanup();
}
addListener(listener: ScanListener) {
this.scanListener = listener;
}
async cleanup() {
if (this.dialogInstance) {
this.dialogInstance.unmount();
this.dialogInstance = null;
this.dialogComponent = null;
}
}
}
```
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');
}
}
scanner.addListener({
onScan: (result) => {
console.log('Scanned:', result);
},
onError: (error) => {
console.error('Scan error:', error);
}
});
await scanner.startScan();
}
// Cleanup when done
onUnmounted(() => {
QRScannerFactory.cleanup();
});
```
## 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