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...
|
async requestPermissions() {
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
const permissions = await navigator.permissions.query({
|
const { camera } = await BarcodeScanner.requestPermissions();
|
||||||
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';
|
return camera === 'granted';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking camera permissions:', error);
|
logger.error('Error requesting camera permissions:', error);
|
||||||
return false;
|
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 dialogInstance: App | null = null;
|
||||||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
|
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
|
||||||
private scanListener: ScanListener | null = null;
|
private scanListener: ScanListener | null = null;
|
||||||
|
|
||||||
async checkPermissions(): Promise<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const permissions = await navigator.permissions.query({
|
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>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||||
<div class="dialog relative">
|
<div class="dialog relative">
|
||||||
<!-- Dialog content -->
|
<div class="dialog-header">
|
||||||
<div v-if="useQRReader">
|
<h2>Scan QR Code</h2>
|
||||||
<qrcode-stream
|
<button @click="onClose" class="close-button">×</button>
|
||||||
class="w-full max-w-lg mx-auto"
|
|
||||||
@detect="onScanDetect"
|
|
||||||
@error="onScanError"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div class="dialog-content">
|
||||||
<!-- Mobile camera button -->
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@Component({
|
import { defineComponent } from 'vue';
|
||||||
components: { QrcodeStream }
|
import { QrcodeStream } from 'vue-qrcode-reader';
|
||||||
})
|
|
||||||
export default class QRScannerDialog extends Vue {
|
export default defineComponent({
|
||||||
// Implementation...
|
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>
|
</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
|
## Usage Example
|
||||||
@@ -513,6 +436,7 @@ onUnmounted(() => {
|
|||||||
- Supports both iOS and Android
|
- Supports both iOS and Android
|
||||||
- Uses back camera by default
|
- Uses back camera by default
|
||||||
- Handles device rotation
|
- Handles device rotation
|
||||||
|
- Provides native UI for scanning
|
||||||
|
|
||||||
### Web
|
### Web
|
||||||
- Uses MediaDevices API
|
- Uses MediaDevices API
|
||||||
@@ -520,6 +444,7 @@ onUnmounted(() => {
|
|||||||
- Handles browser compatibility
|
- Handles browser compatibility
|
||||||
- Manages memory and resources
|
- Manages memory and resources
|
||||||
- Provides fallback UI
|
- Provides fallback UI
|
||||||
|
- Uses vue-qrcode-reader for web scanning
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -528,41 +453,74 @@ onUnmounted(() => {
|
|||||||
- Test platform detection
|
- Test platform detection
|
||||||
- Test error handling
|
- Test error handling
|
||||||
- Test cleanup procedures
|
- Test cleanup procedures
|
||||||
|
- Test permission flows
|
||||||
|
|
||||||
2. **Integration Tests**
|
2. **Integration Tests**
|
||||||
- Test permission flows
|
|
||||||
- Test camera access
|
- Test camera access
|
||||||
- Test QR code detection
|
- Test QR code detection
|
||||||
- Test cross-platform behavior
|
- Test cross-platform behavior
|
||||||
|
- Test UI components
|
||||||
|
- Test error scenarios
|
||||||
|
|
||||||
3. **E2E Tests**
|
3. **E2E Tests**
|
||||||
- Test full scanning workflow
|
- Test complete scanning flow
|
||||||
- Test UI feedback
|
- Test permission handling
|
||||||
- Test error scenarios
|
- Test cross-platform compatibility
|
||||||
- Test platform differences
|
- Test error recovery
|
||||||
|
- Test cleanup procedures
|
||||||
|
|
||||||
## Common Issues and Solutions
|
## Best Practices
|
||||||
|
|
||||||
1. **Permission Handling**
|
1. **Error Handling**
|
||||||
- Always check permissions first
|
- Always handle permission errors gracefully
|
||||||
- Provide clear user feedback
|
- Provide clear error messages to users
|
||||||
- Handle denial gracefully
|
- Implement proper cleanup on errors
|
||||||
- Implement retry logic
|
- Log errors for debugging
|
||||||
|
|
||||||
2. **Resource Management**
|
2. **Performance**
|
||||||
- Clean up after scanning
|
- Clean up resources when not in use
|
||||||
- Handle component unmounting
|
- Handle device rotation properly
|
||||||
- Release camera resources
|
- Optimize camera usage
|
||||||
- Clear event listeners
|
- Manage memory efficiently
|
||||||
|
|
||||||
3. **Error Handling**
|
3. **Security**
|
||||||
- Log errors appropriately
|
- Request minimum required permissions
|
||||||
- Provide user feedback
|
- Handle sensitive data securely
|
||||||
- Implement fallbacks
|
- Validate scanned data
|
||||||
- Handle edge cases
|
- Implement proper cleanup
|
||||||
|
|
||||||
4. **Performance**
|
4. **User Experience**
|
||||||
- Optimize camera preview
|
- Provide clear feedback
|
||||||
- Handle memory usage
|
- Handle edge cases gracefully
|
||||||
- Manage battery impact
|
- Support both platforms seamlessly
|
||||||
- Consider device capabilities
|
- 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
|
||||||
|
|||||||
Binary file not shown.
@@ -41,4 +41,6 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -23,11 +23,12 @@
|
|||||||
"clean:electron": "rimraf dist-electron",
|
"clean:electron": "rimraf dist-electron",
|
||||||
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
||||||
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
||||||
"build:capacitor": "vite build --config vite.config.capacitor.mts",
|
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||||
"build:web": "vite build --config vite.config.web.mts",
|
"build:web": "vite build --config vite.config.web.mts",
|
||||||
"electron:dev": "npm run build && electron dist-electron",
|
"electron:dev": "npm run build && electron dist-electron",
|
||||||
"electron:start": "electron dist-electron",
|
"electron:start": "electron dist-electron",
|
||||||
"build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||||
|
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
||||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||||
|
|||||||
@@ -80,15 +80,21 @@ import { QRScannerOptions } from "@/services/QRScanner/types";
|
|||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
|
||||||
|
interface ScanProps {
|
||||||
|
onScan: (result: string) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
options?: QRScannerOptions;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QrcodeStream,
|
QrcodeStream,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class QRScannerDialog extends Vue {
|
export default class QRScannerDialog extends Vue {
|
||||||
@Prop({ type: Function, required: true }) onScan!: (result: string) => void;
|
@Prop({ type: Function, required: true }) onScan!: ScanProps['onScan'];
|
||||||
@Prop({ type: Function }) onError?: (error: Error) => void;
|
@Prop({ type: Function }) onError?: ScanProps['onError'];
|
||||||
@Prop({ type: Object }) options?: QRScannerOptions;
|
@Prop({ type: Object }) options?: ScanProps['options'];
|
||||||
|
|
||||||
visible = true;
|
visible = true;
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
@@ -126,11 +132,12 @@ export default class QRScannerDialog extends Vue {
|
|||||||
await promise;
|
await promise;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error instanceof Error ? error.message : String(error);
|
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
this.error = wrappedError.message;
|
||||||
if (this.onError) {
|
if (this.onError) {
|
||||||
this.onError(error instanceof Error ? error : new Error(String(error)));
|
this.onError(wrappedError);
|
||||||
}
|
}
|
||||||
logger.error("Error initializing QR scanner:", error);
|
logger.error("Error initializing QR scanner:", wrappedError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +146,12 @@ export default class QRScannerDialog extends Vue {
|
|||||||
this.onScan(result);
|
this.onScan(result);
|
||||||
this.close();
|
this.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error instanceof Error ? error.message : String(error);
|
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
this.error = wrappedError.message;
|
||||||
if (this.onError) {
|
if (this.onError) {
|
||||||
this.onError(error instanceof Error ? error : new Error(String(error)));
|
this.onError(wrappedError);
|
||||||
}
|
}
|
||||||
logger.error("Error handling QR scan result:", error);
|
logger.error("Error handling QR scan result:", wrappedError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ import { logger } from "@/utils/logger";
|
|||||||
export class CapacitorQRScanner implements QRScannerService {
|
export class CapacitorQRScanner implements QRScannerService {
|
||||||
private scanListener: ScanListener | null = null;
|
private scanListener: ScanListener | null = null;
|
||||||
private isScanning = false;
|
private isScanning = false;
|
||||||
|
private listenerHandles: Array<() => Promise<void>> = [];
|
||||||
|
|
||||||
async checkPermissions(): Promise<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const { camera } = await BarcodeScanner.checkPermissions();
|
const { camera } = await BarcodeScanner.checkPermissions();
|
||||||
return camera === "granted";
|
return camera === "granted";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error checking camera permissions:", error);
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error checking camera permissions:", wrappedError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +35,9 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
const { camera } = await BarcodeScanner.requestPermissions();
|
const { camera } = await BarcodeScanner.requestPermissions();
|
||||||
return camera === "granted";
|
return camera === "granted";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error requesting camera permissions:", error);
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error requesting camera permissions:", wrappedError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +47,9 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
const { supported } = await BarcodeScanner.isSupported();
|
const { supported } = await BarcodeScanner.isSupported();
|
||||||
return supported;
|
return supported;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error checking scanner support:", error);
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error checking scanner support:", wrappedError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,17 +86,23 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
logger.log("Scanner options:", scanOptions);
|
logger.log("Scanner options:", scanOptions);
|
||||||
const result = await BarcodeScanner.scan(scanOptions);
|
|
||||||
logger.log("Scan result:", result);
|
// Add listener for barcode scans
|
||||||
|
const handle = await BarcodeScanner.addListener('barcodeScanned', (result) => {
|
||||||
if (result.barcodes.length > 0) {
|
if (this.scanListener) {
|
||||||
this.scanListener?.onScan(result.barcodes[0].rawValue);
|
this.scanListener.onScan(result.barcode.rawValue);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
this.listenerHandles.push(handle.remove);
|
||||||
|
|
||||||
|
// Start continuous scanning
|
||||||
|
await BarcodeScanner.startScan(scanOptions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error during QR scan:", error);
|
const wrappedError =
|
||||||
this.scanListener?.onError?.(error as Error);
|
error instanceof Error ? error : new Error(String(error));
|
||||||
} finally {
|
logger.error("Error during QR scan:", wrappedError);
|
||||||
this.isScanning = false;
|
this.scanListener?.onError?.(wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +113,14 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await BarcodeScanner.stopScan();
|
await BarcodeScanner.stopScan();
|
||||||
this.isScanning = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error stopping QR scan:", error);
|
const wrappedError =
|
||||||
this.scanListener?.onError?.(error as Error);
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error stopping QR scan:", wrappedError);
|
||||||
|
this.scanListener?.onError?.(wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
|
} finally {
|
||||||
|
this.isScanning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +129,19 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
await this.stopScan();
|
try {
|
||||||
this.scanListener = null;
|
await this.stopScan();
|
||||||
|
for (const handle of this.listenerHandles) {
|
||||||
|
await handle();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error during cleanup:", wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
|
} finally {
|
||||||
|
this.listenerHandles = [];
|
||||||
|
this.scanListener = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,15 @@ export class QRScannerFactory {
|
|||||||
private static instance: QRScannerService | null = null;
|
private static instance: QRScannerService | null = null;
|
||||||
|
|
||||||
private static isNativePlatform(): boolean {
|
private static isNativePlatform(): boolean {
|
||||||
|
// Debug logging for build flags
|
||||||
|
logger.log("Build flags:", {
|
||||||
|
IS_MOBILE: typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : 'undefined',
|
||||||
|
USE_QR_READER: typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : 'undefined',
|
||||||
|
VITE_PLATFORM: process.env.VITE_PLATFORM,
|
||||||
|
});
|
||||||
|
|
||||||
const capacitorNative = Capacitor.isNativePlatform();
|
const capacitorNative = Capacitor.isNativePlatform();
|
||||||
const isMobile = __IS_MOBILE__;
|
const isMobile = typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : capacitorNative;
|
||||||
const platform = Capacitor.getPlatform();
|
const platform = Capacitor.getPlatform();
|
||||||
|
|
||||||
logger.log("Platform detection:", {
|
logger.log("Platform detection:", {
|
||||||
@@ -22,12 +29,16 @@ export class QRScannerFactory {
|
|||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force native scanner on Android/iOS
|
// Always use native scanner on Android/iOS
|
||||||
if (platform === "android" || platform === "ios") {
|
if (platform === "android" || platform === "ios") {
|
||||||
|
logger.log("Using native scanner due to platform:", platform);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return capacitorNative || isMobile;
|
// For other platforms, use native if available
|
||||||
|
const useNative = capacitorNative || isMobile;
|
||||||
|
logger.log("Platform decision:", { useNative, reason: useNative ? "capacitorNative/isMobile" : "web" });
|
||||||
|
return useNative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,19 +51,24 @@ export class QRScannerFactory {
|
|||||||
`Creating QR scanner for platform: ${isNative ? "native" : "web"}`,
|
`Creating QR scanner for platform: ${isNative ? "native" : "web"}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNative) {
|
try {
|
||||||
logger.log("Using native MLKit scanner");
|
if (isNative) {
|
||||||
this.instance = new CapacitorQRScanner();
|
logger.log("Using native MLKit scanner");
|
||||||
} else if (__USE_QR_READER__) {
|
this.instance = new CapacitorQRScanner();
|
||||||
logger.log("Using web QR scanner");
|
} else if (typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : !isNative) {
|
||||||
this.instance = new WebDialogQRScanner();
|
logger.log("Using web QR scanner");
|
||||||
} else {
|
this.instance = new WebDialogQRScanner();
|
||||||
throw new Error(
|
} else {
|
||||||
"No QR scanner implementation available for this platform",
|
throw new Error(
|
||||||
);
|
"No QR scanner implementation available for this platform",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error creating QR scanner:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.instance!; // We know it's not null here
|
return this.instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,8 +76,13 @@ export class QRScannerFactory {
|
|||||||
*/
|
*/
|
||||||
static async cleanup(): Promise<void> {
|
static async cleanup(): Promise<void> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
await this.instance.cleanup();
|
try {
|
||||||
this.instance = null;
|
await this.instance.cleanup();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error cleaning up QR scanner:", error);
|
||||||
|
} finally {
|
||||||
|
this.instance = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
|
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
|
||||||
private scanListener: ScanListener | null = null;
|
private scanListener: ScanListener | null = null;
|
||||||
private isScanning = false;
|
private isScanning = false;
|
||||||
|
private container: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(private options?: QRScannerOptions) {}
|
constructor(private options?: QRScannerOptions) {}
|
||||||
|
|
||||||
@@ -18,7 +19,9 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
});
|
});
|
||||||
return permissions.state === "granted";
|
return permissions.state === "granted";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error checking camera permissions:", error);
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error checking camera permissions:", wrappedError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +32,9 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
stream.getTracks().forEach((track) => track.stop());
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error requesting camera permissions:", error);
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error requesting camera permissions:", wrappedError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,8 +52,8 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
|
|
||||||
// Create and mount dialog component
|
// Create and mount dialog component
|
||||||
const container = document.createElement("div");
|
this.container = document.createElement("div");
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(this.container);
|
||||||
|
|
||||||
this.dialogInstance = createApp(QRScannerDialog, {
|
this.dialogInstance = createApp(QRScannerDialog, {
|
||||||
onScan: (result: string) => {
|
onScan: (result: string) => {
|
||||||
@@ -64,16 +69,18 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
options: this.options,
|
options: this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dialogComponent = this.dialogInstance.mount(container).$refs
|
this.dialogComponent = this.dialogInstance.mount(this.container).$refs
|
||||||
.dialog as InstanceType<typeof QRScannerDialog>;
|
.dialog as InstanceType<typeof QRScannerDialog>;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
if (this.scanListener?.onError) {
|
if (this.scanListener?.onError) {
|
||||||
this.scanListener.onError(
|
this.scanListener.onError(wrappedError);
|
||||||
error instanceof Error ? error : new Error(String(error)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
logger.error("Error starting scan:", error);
|
logger.error("Error starting scan:", wrappedError);
|
||||||
|
this.cleanupContainer();
|
||||||
|
throw wrappedError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,9 +96,14 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
if (this.dialogInstance) {
|
if (this.dialogInstance) {
|
||||||
this.dialogInstance.unmount();
|
this.dialogInstance.unmount();
|
||||||
}
|
}
|
||||||
this.isScanning = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error stopping scan:", error);
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error stopping scan:", wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
|
} finally {
|
||||||
|
this.isScanning = false;
|
||||||
|
this.cleanupContainer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +111,26 @@ export class WebDialogQRScanner implements QRScannerService {
|
|||||||
this.scanListener = listener;
|
this.scanListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cleanupContainer(): void {
|
||||||
|
if (this.container && this.container.parentNode) {
|
||||||
|
this.container.parentNode.removeChild(this.container);
|
||||||
|
}
|
||||||
|
this.container = null;
|
||||||
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
await this.stopScan();
|
try {
|
||||||
this.dialogComponent = null;
|
await this.stopScan();
|
||||||
this.dialogInstance = null;
|
} catch (error) {
|
||||||
this.scanListener = null;
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error("Error during cleanup:", wrappedError);
|
||||||
|
throw wrappedError;
|
||||||
|
} finally {
|
||||||
|
this.dialogComponent = null;
|
||||||
|
this.dialogInstance = null;
|
||||||
|
this.scanListener = null;
|
||||||
|
this.cleanupContainer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<span
|
<span
|
||||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
|
||||||
@click="() => $refs.userNameDialog.open((name) => (givenName = name))"
|
@click="openUserNameDialog"
|
||||||
>
|
>
|
||||||
click here to set it for them.
|
click here to set it for them.
|
||||||
</span>
|
</span>
|
||||||
@@ -77,8 +77,19 @@
|
|||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
|
<h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
|
||||||
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
<div v-if="isScanning" class="relative aspect-square">
|
||||||
<span>
|
<div class="absolute inset-0 border-2 border-blue-500 opacity-50 pointer-events-none"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 rounded-md mt-4"
|
||||||
|
@click="startScanning"
|
||||||
|
>
|
||||||
|
Start Scanning
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span v-if="error" class="text-red-500 block mt-2">{{ error }}</span>
|
||||||
|
<span v-else class="block mt-2">
|
||||||
If you do not see a scanning camera window here, check your camera
|
If you do not see a scanning camera window here, check your camera
|
||||||
permissions.
|
permissions.
|
||||||
</span>
|
</span>
|
||||||
@@ -90,7 +101,6 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { QrcodeStream } from "vue-qrcode-reader";
|
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
@@ -110,9 +120,10 @@ import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
|||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QrcodeStream,
|
|
||||||
QRCodeVue3,
|
QRCodeVue3,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
@@ -128,6 +139,8 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
hideRegisterPromptOnNewContact = false;
|
hideRegisterPromptOnNewContact = false;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
|
isScanning = false;
|
||||||
|
error: string | null = null;
|
||||||
|
|
||||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||||
|
|
||||||
@@ -150,12 +163,55 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
account,
|
account,
|
||||||
!!settings.isRegistered,
|
!!settings.isRegistered,
|
||||||
name,
|
name,
|
||||||
settings.profileImageUrl,
|
settings.profileImageUrl || "",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startScanning() {
|
||||||
|
try {
|
||||||
|
this.error = null;
|
||||||
|
this.isScanning = true;
|
||||||
|
|
||||||
|
const scanner = QRScannerFactory.getInstance();
|
||||||
|
|
||||||
|
// Check permissions first
|
||||||
|
if (!(await scanner.checkPermissions())) {
|
||||||
|
const granted = await scanner.requestPermissions();
|
||||||
|
if (!granted) {
|
||||||
|
this.error = "Camera permission denied";
|
||||||
|
this.isScanning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scan listener
|
||||||
|
scanner.addListener({
|
||||||
|
onScan: this.onScanDetect,
|
||||||
|
onError: this.onScanError
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start scanning
|
||||||
|
await scanner.startScan();
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error instanceof Error ? error.message : String(error);
|
||||||
|
this.isScanning = false;
|
||||||
|
logger.error("Error starting scan:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopScanning() {
|
||||||
|
try {
|
||||||
|
const scanner = QRScannerFactory.getInstance();
|
||||||
|
await scanner.stopScan();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error stopping scan:", error);
|
||||||
|
} finally {
|
||||||
|
this.isScanning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
danger(message: string, title: string = "Error", timeout = 5000) {
|
danger(message: string, title: string = "Error", timeout = 5000) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -169,49 +225,37 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Handle QR code scan result
|
||||||
* @param content is the result of a QR scan, an array with one item with a rawValue property
|
|
||||||
*/
|
*/
|
||||||
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
|
async onScanDetect(result: string) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
try {
|
||||||
async onScanDetect(content: any) {
|
|
||||||
const url = content[0]?.rawValue;
|
|
||||||
if (url) {
|
|
||||||
let newContact: Contact;
|
let newContact: Contact;
|
||||||
try {
|
const jwt = getContactJwtFromJwtUrl(result);
|
||||||
const jwt = getContactJwtFromJwtUrl(url);
|
if (!jwt) {
|
||||||
if (!jwt) {
|
this.$notify(
|
||||||
this.$notify(
|
{
|
||||||
{
|
group: "alert",
|
||||||
group: "alert",
|
type: "danger",
|
||||||
type: "danger",
|
title: "No Contact Info",
|
||||||
title: "No Contact Info",
|
text: "The contact info could not be parsed.",
|
||||||
text: "The contact info could not be parsed.",
|
},
|
||||||
},
|
3000,
|
||||||
3000,
|
);
|
||||||
);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const { payload } = decodeEndorserJwt(jwt);
|
||||||
const { payload } = decodeEndorserJwt(jwt);
|
newContact = {
|
||||||
newContact = {
|
did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49
|
||||||
did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49
|
name: payload.own.name,
|
||||||
name: payload.own.name,
|
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
||||||
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
profileImageUrl: payload.own.profileImageUrl,
|
||||||
profileImageUrl: payload.own.profileImageUrl,
|
};
|
||||||
publicKeyBase64: payload.own.publicEncKey,
|
if (!newContact.did) {
|
||||||
registered: payload.own.registered,
|
this.danger("There is no DID.", "Incomplete Contact");
|
||||||
};
|
return;
|
||||||
if (!newContact.did) {
|
}
|
||||||
this.danger("There is no DID.", "Incomplete Contact");
|
if (!isDid(newContact.did)) {
|
||||||
return;
|
this.danger("The DID must begin with 'did:'", "Invalid DID");
|
||||||
}
|
|
||||||
if (!isDid(newContact.did)) {
|
|
||||||
this.danger("The DID must begin with 'did:'", "Invalid DID");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Error parsing QR info:", e);
|
|
||||||
this.danger("Could not parse the QR info.", "Read Error");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +291,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Register",
|
title: "Register",
|
||||||
text: "Do you want to register them?",
|
text: "Do you want to register them?",
|
||||||
onCancel: async (stopAsking: boolean) => {
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
@@ -255,7 +299,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNo: async (stopAsking: boolean) => {
|
onNo: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
@@ -285,16 +329,12 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.$notify(
|
// Stop scanning after successful scan
|
||||||
{
|
await this.stopScanning();
|
||||||
group: "alert",
|
} catch (error) {
|
||||||
type: "danger",
|
this.error = error instanceof Error ? error.message : String(error);
|
||||||
title: "Invalid Contact QR Code",
|
logger.error("Error processing scan result:", error);
|
||||||
text: "No QR code detected with contact information.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,8 +404,8 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
let userMessage = "There was an error.";
|
let userMessage = "There was an error.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
if (serverError.response?.data?.error?.message) {
|
if (serverError.response?.data && typeof serverError.response.data === 'object' && 'message' in serverError.response.data) {
|
||||||
userMessage = serverError.response.data.error.message;
|
userMessage = (serverError.response.data as {message: string}).message;
|
||||||
} else if (serverError.message) {
|
} else if (serverError.message) {
|
||||||
userMessage = serverError.message; // Info for the user
|
userMessage = serverError.message; // Info for the user
|
||||||
} else {
|
} else {
|
||||||
@@ -387,18 +427,9 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
onScanError(error: Error) {
|
||||||
onScanError(error: any) {
|
this.error = error.message;
|
||||||
logger.error("Scan was invalid:", error);
|
logger.error("Scan error:", error);
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Scan",
|
|
||||||
text: "The scan was invalid.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyUrlToClipboard() {
|
onCopyUrlToClipboard() {
|
||||||
@@ -435,5 +466,22 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openUserNameDialog() {
|
||||||
|
(this.$refs.userNameDialog as any).open((name: string) => {
|
||||||
|
this.givenName = name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
// Clean up scanner when component is destroyed
|
||||||
|
QRScannerFactory.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.aspect-square {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export async function createBuildConfig(mode: string) {
|
|||||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
|
||||||
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
||||||
|
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||||
|
__USE_QR_READER__: JSON.stringify(!isCapacitor),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user