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.
526 lines
11 KiB
526 lines
11 KiB
---
|
|
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
|
|
|