forked from trent_larson/crowd-funder-for-time-pwa
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
This commit is contained in:
@@ -133,289 +133,61 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement other interface methods...
|
||||
}
|
||||
```
|
||||
|
||||
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> {
|
||||
async requestPermissions() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Implement other interface methods...
|
||||
}
|
||||
```
|
||||
|
||||
6. **Create Dialog Component**
|
||||
```vue
|
||||
<!-- QRScannerDialog.vue -->
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div class="dialog relative">
|
||||
<!-- Dialog content -->
|
||||
<div v-if="useQRReader">
|
||||
<qrcode-stream
|
||||
class="w-full max-w-lg mx-auto"
|
||||
@detect="onScanDetect"
|
||||
@error="onScanError"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Mobile camera button -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@Component({
|
||||
components: { QrcodeStream }
|
||||
})
|
||||
export default class QRScannerDialog extends Vue {
|
||||
// Implementation...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### Web
|
||||
- Uses MediaDevices API
|
||||
- Requires HTTPS for camera access
|
||||
- Handles browser compatibility
|
||||
- Manages memory and resources
|
||||
- Provides fallback UI
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Unit Tests**
|
||||
- Test factory pattern
|
||||
- Test platform detection
|
||||
- Test error handling
|
||||
- Test cleanup procedures
|
||||
|
||||
2. **Integration Tests**
|
||||
- Test permission flows
|
||||
- Test camera access
|
||||
- Test QR code detection
|
||||
- Test cross-platform behavior
|
||||
|
||||
3. **E2E Tests**
|
||||
- Test full scanning workflow
|
||||
- Test UI feedback
|
||||
- Test error scenarios
|
||||
- Test platform differences
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
1. **Permission Handling**
|
||||
- Always check permissions first
|
||||
- Provide clear user feedback
|
||||
- Handle denial gracefully
|
||||
- Implement retry logic
|
||||
|
||||
2. **Resource Management**
|
||||
- Clean up after scanning
|
||||
- Handle component unmounting
|
||||
- Release camera resources
|
||||
- Clear event listeners
|
||||
|
||||
3. **Error Handling**
|
||||
- Log errors appropriately
|
||||
- Provide user feedback
|
||||
- Implement fallbacks
|
||||
- Handle edge cases
|
||||
|
||||
4. **Performance**
|
||||
- Optimize camera preview
|
||||
- Handle memory usage
|
||||
- Manage battery impact
|
||||
- Consider device capabilities
|
||||
|
||||
# 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();
|
||||
const { camera } = await BarcodeScanner.requestPermissions();
|
||||
return camera === 'granted';
|
||||
} catch (error) {
|
||||
logger.error('Error checking camera permissions:', error);
|
||||
logger.error('Error requesting camera permissions:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Implement other interface methods...
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -426,6 +198,7 @@ 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({
|
||||
@@ -438,7 +211,61 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement other interface methods...
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -448,29 +275,125 @@ export class WebDialogQRScanner implements QRScannerService {
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div class="dialog relative">
|
||||
<!-- Dialog content -->
|
||||
<div v-if="useQRReader">
|
||||
<qrcode-stream
|
||||
class="w-full max-w-lg mx-auto"
|
||||
@detect="onScanDetect"
|
||||
@error="onScanError"
|
||||
/>
|
||||
<div class="dialog-header">
|
||||
<h2>Scan QR Code</h2>
|
||||
<button @click="onClose" class="close-button">×</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- Mobile camera button -->
|
||||
<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">
|
||||
@Component({
|
||||
components: { QrcodeStream }
|
||||
})
|
||||
export default class QRScannerDialog extends Vue {
|
||||
// Implementation...
|
||||
}
|
||||
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
|
||||
@@ -513,6 +436,7 @@ onUnmounted(() => {
|
||||
- Supports both iOS and Android
|
||||
- Uses back camera by default
|
||||
- Handles device rotation
|
||||
- Provides native UI for scanning
|
||||
|
||||
### Web
|
||||
- Uses MediaDevices API
|
||||
@@ -520,6 +444,7 @@ onUnmounted(() => {
|
||||
- Handles browser compatibility
|
||||
- Manages memory and resources
|
||||
- Provides fallback UI
|
||||
- Uses vue-qrcode-reader for web scanning
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -528,41 +453,74 @@ onUnmounted(() => {
|
||||
- Test platform detection
|
||||
- Test error handling
|
||||
- Test cleanup procedures
|
||||
- Test permission flows
|
||||
|
||||
2. **Integration Tests**
|
||||
- Test permission flows
|
||||
- Test camera access
|
||||
- Test QR code detection
|
||||
- Test cross-platform behavior
|
||||
- Test UI components
|
||||
- Test error scenarios
|
||||
|
||||
3. **E2E Tests**
|
||||
- Test full scanning workflow
|
||||
- Test UI feedback
|
||||
- Test error scenarios
|
||||
- Test platform differences
|
||||
- Test complete scanning flow
|
||||
- Test permission handling
|
||||
- Test cross-platform compatibility
|
||||
- Test error recovery
|
||||
- Test cleanup procedures
|
||||
|
||||
## Common Issues and Solutions
|
||||
## Best Practices
|
||||
|
||||
1. **Permission Handling**
|
||||
- Always check permissions first
|
||||
- Provide clear user feedback
|
||||
- Handle denial gracefully
|
||||
- Implement retry logic
|
||||
1. **Error Handling**
|
||||
- Always handle permission errors gracefully
|
||||
- Provide clear error messages to users
|
||||
- Implement proper cleanup on errors
|
||||
- Log errors for debugging
|
||||
|
||||
2. **Resource Management**
|
||||
- Clean up after scanning
|
||||
- Handle component unmounting
|
||||
- Release camera resources
|
||||
- Clear event listeners
|
||||
2. **Performance**
|
||||
- Clean up resources when not in use
|
||||
- Handle device rotation properly
|
||||
- Optimize camera usage
|
||||
- Manage memory efficiently
|
||||
|
||||
3. **Error Handling**
|
||||
- Log errors appropriately
|
||||
- Provide user feedback
|
||||
- Implement fallbacks
|
||||
- Handle edge cases
|
||||
3. **Security**
|
||||
- Request minimum required permissions
|
||||
- Handle sensitive data securely
|
||||
- Validate scanned data
|
||||
- Implement proper cleanup
|
||||
|
||||
4. **Performance**
|
||||
- Optimize camera preview
|
||||
- Handle memory usage
|
||||
- Manage battery impact
|
||||
- Consider device capabilities
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user