forked from jsnbuchanan/crowd-funder-for-time-pwa
- 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
527 lines
11 KiB
Plaintext
527 lines
11 KiB
Plaintext
---
|
||
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
|