forked from jsnbuchanan/crowd-funder-for-time-pwa
feature(qrcode): reboot qrcode reader
This commit is contained in:
568
.cursor/rules/qr-code-implementation-guide.mdc
Normal file
568
.cursor/rules/qr-code-implementation-guide.mdc
Normal file
@@ -0,0 +1,568 @@
|
||||
---
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
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();
|
||||
return camera === 'granted';
|
||||
} catch (error) {
|
||||
logger.error('Error checking camera permissions:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
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
|
||||
Reference in New Issue
Block a user