Browse Source

chore: updates for qr code reader rules, linting, and cleanup

pull/133/head
Matt Raymer 1 month ago
parent
commit
74b9caa94f
  1. 102
      .cursor/rules/architectural_decision_record.mdc
  2. 47
      .cursor/rules/qr-code-handling-rule.mdc
  3. 433
      .cursor/rules/qr-code-implementation-guide.mdc
  4. 447
      package-lock.json
  5. 722
      src/components/QRScanner/QRScannerDialog.vue
  6. 15
      src/services/QRScanner/QRScannerService.ts
  7. 263
      src/services/QRScanner/WebDialogQRScanner.ts
  8. 15
      src/views/ContactQRScanShowView.vue

102
.cursor/rules/architectural_decision_record.mdc

@ -9,15 +9,33 @@ alwaysApply: true
| Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) | PyWebView (Desktop) | | Feature | Web (PWA) | Capacitor (Mobile) | Electron (Desktop) | PyWebView (Desktop) |
|---------|-----------|-------------------|-------------------|-------------------| |---------|-----------|-------------------|-------------------|-------------------|
| QR Code Scanning | vue-qrcode-reader | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented | | QR Code Scanning | WebInlineQRScanner | @capacitor-mlkit/barcode-scanning | Not Implemented | Not Implemented |
| Deep Linking | URL Parameters | App URL Open Events | Not Implemented | Not Implemented | | Deep Linking | URL Parameters | App URL Open Events | Not Implemented | Not Implemented |
| File System | Limited (Browser API) | Capacitor Filesystem | Electron fs | PyWebView Python Bridge | | File System | Limited (Browser API) | Capacitor Filesystem | Electron fs | PyWebView Python Bridge |
| Camera Access | MediaDevices API | Capacitor Camera | Not Implemented | Not Implemented | | Camera Access | MediaDevices API | Capacitor Camera | Not Implemented | Not Implemented |
| Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks | process.env checks | | Platform Detection | Web APIs | Capacitor.isNativePlatform() | process.env checks | process.env checks |
## 2. Build Configuration Structure ## 2. Project Structure
### 2.1 Entry Points ### 2.1 Core Directories
```
src/
├── components/ # Vue components
├── services/ # Platform services and business logic
├── views/ # Page components
├── router/ # Vue router configuration
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── lib/ # Core libraries
├── platforms/ # Platform-specific implementations
├── electron/ # Electron-specific code
├── constants/ # Application constants
├── db/ # Database related code
├── interfaces/ # TypeScript interfaces
└── assets/ # Static assets
```
### 2.2 Entry Points
``` ```
src/ src/
├── main.ts # Base entry ├── main.ts # Base entry
@ -28,19 +46,35 @@ src/
└── main.web.ts # Web/PWA entry └── main.web.ts # Web/PWA entry
``` ```
### 2.2 Build Configurations ### 2.3 Build Configurations
``` ```
root/ root/
├── vite.config.common.mts # Shared config ├── vite.config.common.mts # Shared config
├── vite.config.capacitor.mts # Mobile build ├── vite.config.capacitor.mts # Mobile build
├── vite.config.electron.mts # Electron build ├── vite.config.electron.mts # Electron build
├── vite.config.pywebview.mts # PyWebView build ├── vite.config.pywebview.mts # PyWebView build
└── vite.config.web.mts # Web/PWA build ├── vite.config.web.mts # Web/PWA build
└── vite.config.utils.mts # Build utilities
``` ```
## 3. Platform Service Architecture ## 3. Service Architecture
### 3.1 Service Organization
```
services/
├── QRScanner/ # QR code scanning service
│ ├── WebInlineQRScanner.ts
│ └── interfaces.ts
├── platforms/ # Platform-specific services
│ ├── WebPlatformService.ts
│ ├── CapacitorPlatformService.ts
│ ├── ElectronPlatformService.ts
│ └── PyWebViewPlatformService.ts
└── factory/ # Service factories
└── PlatformServiceFactory.ts
```
### 3.1 Service Factory Pattern ### 3.2 Service Factory Pattern
```typescript ```typescript
// PlatformServiceFactory.ts // PlatformServiceFactory.ts
export class PlatformServiceFactory { export class PlatformServiceFactory {
@ -56,40 +90,34 @@ export class PlatformServiceFactory {
} }
``` ```
### 3.2 Platform-Specific Implementations
```
services/platforms/
├── WebPlatformService.ts
├── CapacitorPlatformService.ts
├── ElectronPlatformService.ts
└── PyWebViewPlatformService.ts
```
## 4. Feature Implementation Guidelines ## 4. Feature Implementation Guidelines
### 4.1 QR Code Scanning ### 4.1 QR Code Scanning
1. **Factory Pattern** 1. **Service Interface**
```typescript ```typescript
export class QRScannerFactory { interface QRScannerService {
static getInstance(): QRScannerService { checkPermissions(): Promise<boolean>;
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { requestPermissions(): Promise<boolean>;
return new CapacitorQRScanner(); isSupported(): Promise<boolean>;
} else if (__USE_QR_READER__) { startScan(): Promise<void>;
return new WebDialogQRScanner(); stopScan(): Promise<void>;
} addListener(listener: ScanListener): void;
throw new Error("No QR scanner implementation available"); onStream(callback: (stream: MediaStream | null) => void): void;
} cleanup(): Promise<void>;
} }
``` ```
2. **Platform-Specific Implementation** 2. **Platform-Specific Implementation**
```typescript ```typescript
// Example for Capacitor // WebInlineQRScanner.ts
export class CapacitorQRScanner implements QRScannerService { export class WebInlineQRScanner implements QRScannerService {
async startScan(options?: QRScannerOptions): Promise<void> { private scanListener: ScanListener | null = null;
// Platform-specific implementation private isScanning = false;
} private stream: MediaStream | null = null;
private events = new EventEmitter();
// Implementation of interface methods
} }
``` ```
@ -202,6 +230,8 @@ if (process.env.VITE_PLATFORM === 'capacitor') {
- Use platform-specific directories for unique implementations - Use platform-specific directories for unique implementations
- Share common code through service interfaces - Share common code through service interfaces
- Implement feature detection before using platform capabilities - Implement feature detection before using platform capabilities
- Keep platform-specific code isolated in dedicated directories
- Use TypeScript interfaces for cross-platform compatibility
### 8.2 Platform Detection ### 8.2 Platform Detection
```typescript ```typescript
@ -219,6 +249,7 @@ if (capabilities.hasCamera) {
3. Use factory pattern for instantiation 3. Use factory pattern for instantiation
4. Implement graceful fallbacks 4. Implement graceful fallbacks
5. Add comprehensive error handling 5. Add comprehensive error handling
6. Use dependency injection for better testability
## 9. Dependency Management ## 9. Dependency Management
@ -228,7 +259,7 @@ if (capabilities.hasCamera) {
"dependencies": { "dependencies": {
"@capacitor/core": "^6.2.0", "@capacitor/core": "^6.2.0",
"electron": "^33.2.1", "electron": "^33.2.1",
"vue-qrcode-reader": "^5.5.3" "vue": "^3.4.0"
} }
} }
``` ```
@ -253,8 +284,9 @@ async checkPermissions(): Promise<boolean> {
``` ```
### 10.2 Data Storage ### 10.2 Data Storage
- Use platform-appropriate storage mechanisms - Use secure storage mechanisms for sensitive data
- Implement encryption for sensitive data - Implement proper encryption for stored data
- Handle permissions appropriately - Follow platform-specific security guidelines
- Regular security audits and updates
This document should be updated as new features are added or platform-specific implementations change. Regular reviews ensure it remains current with the codebase. This document should be updated as new features are added or platform-specific implementations change. Regular reviews ensure it remains current with the codebase.

47
.cursor/rules/qr-code-handling-rule.mdc

@ -14,11 +14,11 @@ The QR code scanning functionality follows a platform-agnostic design using a fa
1. **Factory Pattern** 1. **Factory Pattern**
- `QRScannerFactory` - Creates appropriate scanner instance based on platform - `QRScannerFactory` - Creates appropriate scanner instance based on platform
- Common interface `QRScannerService` implemented by all scanners - Common interface `QRScannerService` implemented by all scanners
- Platform detection via Vite config flags: `__USE_QR_READER__` and `__IS_MOBILE__` - Platform detection via Capacitor and build flags
2. **Platform-Specific Implementations** 2. **Platform-Specific Implementations**
- `CapacitorQRScanner` - Native mobile implementation - `CapacitorQRScanner` - Native mobile implementation using MLKit
- `WebDialogQRScanner` - Web browser implementation - `WebInlineQRScanner` - Web browser implementation using MediaDevices API
- `QRScannerDialog.vue` - Shared UI component - `QRScannerDialog.vue` - Shared UI component
## Mobile Implementation (Capacitor) ## Mobile Implementation (Capacitor)
@ -54,13 +54,13 @@ MLKitBarcodeScanner: {
## Web Implementation ## Web Implementation
### Technology Stack ### Technology Stack
- Uses `vue-qrcode-reader` library - Uses browser's MediaDevices API
- Browser's MediaDevices API - Vue.js components for UI
- Vue.js dialog component - EventEmitter for stream management
### Key Features ### Key Features
- Browser-based camera access - Browser-based camera access
- Fallback UI for unsupported browsers - Inline camera preview
- Responsive design - Responsive design
- Cross-browser compatibility - Cross-browser compatibility
- Progressive enhancement - Progressive enhancement
@ -97,9 +97,10 @@ MLKitBarcodeScanner: {
### Platform Detection ### Platform Detection
```typescript ```typescript
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { const isNative = QRScannerFactory.isNativePlatform();
if (isNative) {
// Use native scanner // Use native scanner
} else if (__USE_QR_READER__) { } else {
// Use web scanner // Use web scanner
} }
``` ```
@ -112,6 +113,9 @@ await scanner.startScan();
scanner.addListener({ scanner.addListener({
onScan: (result) => { onScan: (result) => {
// Handle scan result // Handle scan result
},
onError: (error) => {
// Handle error
} }
}); });
``` ```
@ -146,3 +150,28 @@ scanner.addListener({
3. Check error handling 3. Check error handling
4. Validate cleanup 4. Validate cleanup
5. Verify cross-platform behavior 5. Verify cross-platform behavior
## Service Interface
```typescript
interface QRScannerService {
checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>;
startScan(options?: QRScannerOptions): Promise<void>;
stopScan(): Promise<void>;
addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise<void>;
}
interface ScanListener {
onScan: (result: string) => void;
onError?: (error: Error) => void;
}
interface QRScannerOptions {
camera?: "front" | "back";
showPreview?: boolean;
playSound?: boolean;
}

433
.cursor/rules/qr-code-implementation-guide.mdc

@ -9,19 +9,16 @@ alwaysApply: true
``` ```
src/ src/
├── components/
│ └── QRScanner/
│ ├── types.ts
│ ├── factory.ts
│ ├── CapacitorScanner.ts
│ ├── WebDialogScanner.ts
│ └── QRScannerDialog.vue
├── services/ ├── services/
│ └── QRScanner/ │ └── QRScanner/
│ ├── types.ts │ ├── types.ts # Core interfaces and types
│ ├── QRScannerFactory.ts │ ├── QRScannerFactory.ts # Factory for creating scanner instances
│ ├── CapacitorQRScanner.ts │ ├── CapacitorQRScanner.ts # Mobile implementation using MLKit
│ └── WebDialogQRScanner.ts │ ├── WebInlineQRScanner.ts # Web implementation using MediaDevices API
│ └── interfaces.ts # Additional interfaces
├── components/
│ └── QRScanner/
│ └── QRScannerDialog.vue # Shared UI component
``` ```
## Core Interfaces ## Core Interfaces
@ -33,13 +30,20 @@ export interface ScanListener {
onError?: (error: Error) => void; onError?: (error: Error) => void;
} }
export interface QRScannerOptions {
camera?: "front" | "back";
showPreview?: boolean;
playSound?: boolean;
}
export interface QRScannerService { export interface QRScannerService {
checkPermissions(): Promise<boolean>; checkPermissions(): Promise<boolean>;
requestPermissions(): Promise<boolean>; requestPermissions(): Promise<boolean>;
isSupported(): Promise<boolean>; isSupported(): Promise<boolean>;
startScan(): Promise<void>; startScan(options?: QRScannerOptions): Promise<void>;
stopScan(): Promise<void>; stopScan(): Promise<void>;
addListener(listener: ScanListener): void; addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise<void>; cleanup(): Promise<void>;
} }
``` ```
@ -48,18 +52,17 @@ export interface QRScannerService {
### Vite Configuration ### Vite Configuration
```typescript ```typescript
// vite.config.ts // vite.config.common.mts
export default defineConfig({ export function createBuildConfig(mode: string) {
return {
define: { define: {
__USE_QR_READER__: JSON.stringify(!isMobile), 'process.env.VITE_PLATFORM': JSON.stringify(mode),
__IS_MOBILE__: JSON.stringify(isMobile), 'process.env.VITE_PWA_ENABLED': JSON.stringify(!isNative),
}, __IS_MOBILE__: JSON.stringify(isCapacitor),
build: { __USE_QR_READER__: JSON.stringify(!isCapacitor)
rollupOptions: {
external: isMobile ? ['vue-qrcode-reader'] : [],
} }
};
} }
});
``` ```
### Capacitor Configuration ### Capacitor Configuration
@ -81,7 +84,7 @@ const config: CapacitorConfig = {
1. **Install Dependencies** 1. **Install Dependencies**
```bash ```bash
npm install @capacitor-mlkit/barcode-scanning vue-qrcode-reader npm install @capacitor-mlkit/barcode-scanning
``` ```
2. **Create Core Types** 2. **Create Core Types**
@ -93,20 +96,34 @@ Create the interface files as shown above.
export class QRScannerFactory { export class QRScannerFactory {
private static instance: QRScannerService | null = null; private static instance: QRScannerService | null = null;
private static isNativePlatform(): boolean {
const capacitorNative = Capacitor.isNativePlatform();
const isMobile = typeof __IS_MOBILE__ !== "undefined" ? __IS_MOBILE__ : capacitorNative;
const platform = Capacitor.getPlatform();
// Always use native scanner on Android/iOS
if (platform === "android" || platform === "ios") {
return true;
}
// For other platforms, use native if available
return capacitorNative || isMobile;
}
static getInstance(): QRScannerService { static getInstance(): QRScannerService {
if (!this.instance) { if (!this.instance) {
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { const isNative = this.isNativePlatform();
if (isNative) {
this.instance = new CapacitorQRScanner(); this.instance = new CapacitorQRScanner();
} else if (__USE_QR_READER__) {
this.instance = new WebDialogQRScanner();
} else { } else {
throw new Error('No QR scanner implementation available'); this.instance = new WebInlineQRScanner();
} }
} }
return this.instance; return this.instance!;
} }
static async cleanup() { static async cleanup(): Promise<void> {
if (this.instance) { if (this.instance) {
await this.instance.cleanup(); await this.instance.cleanup();
this.instance = null; this.instance = null;
@ -122,65 +139,104 @@ 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>> = []; private listenerHandles: Array<() => Promise<void>> = [];
private cleanupPromise: Promise<void> | null = null;
async checkPermissions() { 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); logger.error("Error checking camera permissions:", error);
return false; return false;
} }
} }
async requestPermissions() { async requestPermissions(): Promise<boolean> {
try { try {
if (await this.checkPermissions()) {
return true;
}
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); logger.error("Error requesting camera permissions:", error);
return false; return false;
} }
} }
async isSupported() { async isSupported(): Promise<boolean> {
return Capacitor.isNativePlatform(); try {
const { supported } = await BarcodeScanner.isSupported();
return supported;
} catch (error) {
logger.error("Error checking scanner support:", error);
return false;
}
} }
async startScan() { async startScan(options?: QRScannerOptions): Promise<void> {
if (this.isScanning) return; if (this.isScanning) return;
this.isScanning = true; if (this.cleanupPromise) {
await this.cleanupPromise;
}
try { try {
await BarcodeScanner.startScan(); if (!(await this.checkPermissions())) {
const granted = await this.requestPermissions();
if (!granted) {
throw new Error("Camera permission denied");
}
}
if (!(await this.isSupported())) {
throw new Error("QR scanning not supported on this device");
}
this.isScanning = true;
const scanOptions: StartScanOptions = {
formats: [BarcodeFormat.QrCode],
lensFacing: options?.camera === "front" ? LensFacing.Front : LensFacing.Back,
};
const handle = await BarcodeScanner.addListener("barcodeScanned", (result) => {
if (this.scanListener && result.barcode?.rawValue) {
this.scanListener.onScan(result.barcode.rawValue);
}
});
this.listenerHandles.push(handle.remove);
await BarcodeScanner.startScan(scanOptions);
} catch (error) { } catch (error) {
this.isScanning = false; this.isScanning = false;
await this.cleanup();
this.scanListener?.onError?.(error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }
async stopScan() { async stopScan(): Promise<void> {
if (!this.isScanning) return; if (!this.isScanning) return;
this.isScanning = false; this.isScanning = false;
try { try {
await BarcodeScanner.stopScan(); await BarcodeScanner.stopScan();
} catch (error) { } catch (error) {
logger.error('Error stopping scan:', error); logger.error("Error stopping scan:", error);
throw error;
} }
} }
addListener(listener: ScanListener) { addListener(listener: ScanListener): void {
this.scanListener = listener; this.scanListener = listener;
const handle = BarcodeScanner.addListener('barcodeScanned', (result) => {
if (this.scanListener) {
this.scanListener.onScan(result.barcode);
} }
});
this.listenerHandles.push(handle.remove); onStream(callback: (stream: MediaStream | null) => void): void {
// No-op for native scanner
callback(null);
} }
async cleanup() { async cleanup(): Promise<void> {
await this.stopScan(); await this.stopScan();
for (const handle of this.listenerHandles) { for (const handle of this.listenerHandles) {
await handle(); await handle();
@ -193,31 +249,40 @@ export class CapacitorQRScanner implements QRScannerService {
5. **Implement Web Scanner** 5. **Implement Web Scanner**
```typescript ```typescript
// WebDialogQRScanner.ts // WebInlineQRScanner.ts
export class WebDialogQRScanner implements QRScannerService { export class WebInlineQRScanner implements QRScannerService {
private dialogInstance: App | null = null;
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
private scanListener: ScanListener | null = null; private scanListener: ScanListener | null = null;
private isScanning = false;
private stream: MediaStream | null = null;
private events = new EventEmitter();
constructor(private options?: QRScannerOptions) {}
async checkPermissions(): Promise<boolean> { async checkPermissions(): Promise<boolean> {
try { try {
const permissions = await navigator.permissions.query({ const permissions = await navigator.permissions.query({
name: 'camera' as PermissionName name: "camera" as PermissionName,
}); });
return permissions.state === 'granted'; return permissions.state === "granted";
} catch (error) { } catch (error) {
logger.error('Error checking camera permissions:', error); logger.error("Error checking camera permissions:", error);
return false; return false;
} }
} }
async requestPermissions(): Promise<boolean> { async requestPermissions(): Promise<boolean> {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true }); const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
},
});
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); logger.error("Error requesting camera permissions:", error);
return false; return false;
} }
} }
@ -226,183 +291,75 @@ export class WebDialogQRScanner implements QRScannerService {
return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices; return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
} }
async startScan() { async startScan(): Promise<void> {
if (this.dialogInstance) return; if (this.isScanning) return;
const container = document.createElement('div');
document.body.appendChild(container);
this.dialogInstance = createApp(QRScannerDialog, { try {
onScan: (result: string) => { this.isScanning = true;
if (this.scanListener) { this.stream = await navigator.mediaDevices.getUserMedia({
this.scanListener.onScan(result); video: {
} facingMode: "environment",
}, width: { ideal: 1280 },
onError: (error: Error) => { height: { ideal: 720 },
if (this.scanListener?.onError) {
this.scanListener.onError(error);
}
}, },
onClose: () => {
this.cleanup();
}
}); });
this.events.emit("stream", this.stream);
this.dialogComponent = this.dialogInstance.mount(container) as InstanceType<typeof QRScannerDialog>; } catch (error) {
} this.isScanning = false;
const wrappedError = error instanceof Error ? error : new Error(String(error));
async stopScan() { this.scanListener?.onError?.(wrappedError);
await this.cleanup(); throw wrappedError;
}
addListener(listener: ScanListener) {
this.scanListener = listener;
}
async cleanup() {
if (this.dialogInstance) {
this.dialogInstance.unmount();
this.dialogInstance = null;
this.dialogComponent = null;
}
} }
} }
```
6. **Create Dialog Component** async stopScan(): Promise<void> {
```vue if (!this.isScanning) return;
<!-- 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 { try {
const scanner = QRScannerFactory.getInstance(); if (this.stream) {
await scanner.startScan(); this.stream.getTracks().forEach(track => track.stop());
} catch (error) { this.stream = null;
this.onError(error as Error);
}
} }
this.events.emit("stream", null);
} catch (error) {
logger.error("Error stopping scan:", error);
throw error;
} finally {
this.isScanning = false;
} }
});
</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 { addListener(listener: ScanListener): void {
background: white; this.scanListener = listener;
border-radius: 8px;
padding: 20px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
} }
.dialog-header { onStream(callback: (stream: MediaStream | null) => void): void {
display: flex; this.events.on("stream", callback);
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
} }
.close-button { async cleanup(): Promise<void> {
background: none; try {
border: none; await this.stopScan();
font-size: 24px; this.events.removeAllListeners();
cursor: pointer; } catch (error) {
logger.error("Error during cleanup:", error);
} }
.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
```typescript ```typescript
// In your component // Example usage in a Vue component
async function scanQRCode() { import { QRScannerFactory } from '@/services/QRScanner/QRScannerFactory';
export default defineComponent({
async mounted() {
const scanner = QRScannerFactory.getInstance(); const scanner = QRScannerFactory.getInstance();
try {
// Check and request permissions
if (!(await scanner.checkPermissions())) { if (!(await scanner.checkPermissions())) {
const granted = await scanner.requestPermissions(); const granted = await scanner.requestPermissions();
if (!granted) { if (!granted) {
@ -410,24 +367,74 @@ async function scanQRCode() {
} }
} }
// Add scan listener
scanner.addListener({ scanner.addListener({
onScan: (result) => { onScan: (result) => {
console.log('Scanned:', result); console.log('QR Code scanned:', result);
}, },
onError: (error) => { onError: (error) => {
console.error('Scan error:', error); console.error('Scan error:', error);
} }
}); });
await scanner.startScan(); // Start scanning
await scanner.startScan({
camera: 'back',
showPreview: true
});
// Handle stream for preview
scanner.onStream((stream) => {
if (stream) {
// Update video element with stream
this.videoElement.srcObject = stream;
}
});
} catch (error) {
console.error('Failed to start scanner:', error);
} }
},
// Cleanup when done async beforeUnmount() {
onUnmounted(() => { // Clean up scanner
QRScannerFactory.cleanup(); await QRScannerFactory.cleanup();
}
}); });
``` ```
## Best Practices
1. **Error Handling**
- Always implement error handlers in scan listeners
- Handle permission denials gracefully
- Provide user feedback for errors
- Clean up resources on errors
2. **Resource Management**
- Always call cleanup when done
- Stop camera streams properly
- Remove event listeners
- Handle component unmounting
3. **Performance**
- Use appropriate camera resolution
- Clean up resources promptly
- Handle platform-specific optimizations
- Monitor memory usage
4. **Security**
- Require HTTPS for web implementation
- Validate scanned data
- Handle permissions properly
- Sanitize user input
5. **Testing**
- Test on multiple devices
- Verify permission flows
- Check error scenarios
- Validate cleanup
- Test cross-platform behavior
## Platform-Specific Notes ## Platform-Specific Notes
### Mobile (Capacitor) ### Mobile (Capacitor)

447
package-lock.json

@ -193,9 +193,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.27.1", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
"integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
"devOptional": true, "devOptional": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -269,12 +269,12 @@
} }
}, },
"node_modules/@babel/helper-compilation-targets": { "node_modules/@babel/helper-compilation-targets": {
"version": "7.27.1", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.27.1", "@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0", "browserslist": "^4.24.0",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
@ -638,9 +638,9 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.27.1", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
"dependencies": { "dependencies": {
"@babel/types": "^7.27.1" "@babel/types": "^7.27.1"
}, },
@ -1636,13 +1636,14 @@
} }
}, },
"node_modules/@babel/plugin-transform-object-rest-spread": { "node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.27.1", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz",
"integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.27.1",
"@babel/plugin-transform-parameters": "^7.27.1" "@babel/plugin-transform-parameters": "^7.27.1"
}, },
"engines": { "engines": {
@ -2100,13 +2101,13 @@
} }
}, },
"node_modules/@babel/preset-env": { "node_modules/@babel/preset-env": {
"version": "7.27.1", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz",
"integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.27.1", "@babel/compat-data": "^7.27.2",
"@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1",
@ -2148,7 +2149,7 @@
"@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.27.2",
"@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-object-super": "^7.27.1",
"@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1",
@ -2256,13 +2257,13 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.1", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.1", "@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1" "@babel/types": "^7.27.1"
}, },
"engines": { "engines": {
@ -4810,26 +4811,26 @@
} }
}, },
"node_modules/@expo/cli": { "node_modules/@expo/cli": {
"version": "0.24.11", "version": "0.24.12",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.11.tgz", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.12.tgz",
"integrity": "sha512-bQtXdonOgg2OgPjHd7D5IkiPObKyiLs+HVM2A1VFV1pOT/8kc2kF/I4lN/Y5uce03FC8v0VRv6rKrDQPlTVWlg==", "integrity": "sha512-MHCIq5jE6uWG26z7SQjUGxXrggxrooKqaGLTz2Vktr5NPkqRc0HBRKi3Rzd4zH5Y902/p18itTQBgvYOrMHt/g==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@0no-co/graphql.web": "^1.0.8", "@0no-co/graphql.web": "^1.0.8",
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/code-signing-certificates": "^0.0.5", "@expo/code-signing-certificates": "^0.0.5",
"@expo/config": "~11.0.8", "@expo/config": "~11.0.9",
"@expo/config-plugins": "~10.0.2", "@expo/config-plugins": "~10.0.2",
"@expo/devcert": "^1.1.2", "@expo/devcert": "^1.1.2",
"@expo/env": "~1.0.5", "@expo/env": "~1.0.5",
"@expo/image-utils": "^0.7.4", "@expo/image-utils": "^0.7.4",
"@expo/json-file": "^9.1.4", "@expo/json-file": "^9.1.4",
"@expo/metro-config": "~0.20.13", "@expo/metro-config": "~0.20.14",
"@expo/osascript": "^2.2.4", "@expo/osascript": "^2.2.4",
"@expo/package-manager": "^1.8.4", "@expo/package-manager": "^1.8.4",
"@expo/plist": "^0.3.4", "@expo/plist": "^0.3.4",
"@expo/prebuild-config": "^9.0.5", "@expo/prebuild-config": "^9.0.6",
"@expo/spawn-async": "^1.7.2", "@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1", "@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0", "@expo/xcpretty": "^4.3.0",
@ -5215,15 +5216,15 @@
} }
}, },
"node_modules/@expo/config": { "node_modules/@expo/config": {
"version": "11.0.8", "version": "11.0.9",
"resolved": "https://registry.npmjs.org/@expo/config/-/config-11.0.8.tgz", "resolved": "https://registry.npmjs.org/@expo/config/-/config-11.0.9.tgz",
"integrity": "sha512-udLrpW4SvXUwF+ntJ0RzEjRbFoSS7Tr/rMrvhfISHWGbcZ09+c+QkI0O8y1sEBWQDpI/IlC9REPqGm5b7HweDw==", "integrity": "sha512-Rm2nnuwvPFBPmK0qlzx1DyGFcDq1KgahvdnYRuCYGDwOxUrf+cqYnj/K7cHijC1sBpp8fw550NVKMoLCsOodjw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "~7.10.4", "@babel/code-frame": "~7.10.4",
"@expo/config-plugins": "~10.0.2", "@expo/config-plugins": "~10.0.2",
"@expo/config-types": "^53.0.3", "@expo/config-types": "^53.0.4",
"@expo/json-file": "^9.1.4", "@expo/json-file": "^9.1.4",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"getenv": "^1.0.0", "getenv": "^1.0.0",
@ -5366,9 +5367,9 @@
} }
}, },
"node_modules/@expo/config-types": { "node_modules/@expo/config-types": {
"version": "53.0.3", "version": "53.0.4",
"resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-53.0.3.tgz", "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-53.0.4.tgz",
"integrity": "sha512-V1e6CiM4TXtGxG/W2Msjp/QOx/vikLo5IUGMvEMjgAglBfGYx3PXfqsUb5aZDt6kqA3bDDwFuZoS5vNm/SYwSg==", "integrity": "sha512-0s+9vFx83WIToEr0Iwy4CcmiUXa5BgwBmEjylBB2eojX5XAMm9mJvw9KpjAb8m7zq2G0Q6bRbeufkzgbipuNQg==",
"optional": true, "optional": true,
"peer": true "peer": true
}, },
@ -5617,9 +5618,9 @@
} }
}, },
"node_modules/@expo/metro-config": { "node_modules/@expo/metro-config": {
"version": "0.20.13", "version": "0.20.14",
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.13.tgz", "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.14.tgz",
"integrity": "sha512-yyhyBBX2HaqFpuGq8r73d9eB1nJeUWDrNDrPANWuXNwfM/fd5pCT1GXmlRe4CWPQ4dPOlYnBIyrEn5c2FI5J4w==", "integrity": "sha512-tYDDubuZycK+NX00XN7BMu73kBur/evOPcKfxc+UBeFfgN2EifOITtdwSUDdRsbtJ2OnXwMY1HfRUG3Lq3l4cw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -5627,7 +5628,7 @@
"@babel/generator": "^7.20.5", "@babel/generator": "^7.20.5",
"@babel/parser": "^7.20.0", "@babel/parser": "^7.20.0",
"@babel/types": "^7.20.0", "@babel/types": "^7.20.0",
"@expo/config": "~11.0.8", "@expo/config": "~11.0.9",
"@expo/env": "~1.0.5", "@expo/env": "~1.0.5",
"@expo/json-file": "~9.1.4", "@expo/json-file": "~9.1.4",
"@expo/spawn-async": "^1.7.2", "@expo/spawn-async": "^1.7.2",
@ -5992,15 +5993,15 @@
} }
}, },
"node_modules/@expo/prebuild-config": { "node_modules/@expo/prebuild-config": {
"version": "9.0.5", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.5.tgz", "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.6.tgz",
"integrity": "sha512-oiSVU5ePu9lsOvn5p4xplqjzPlcZHzKYwzuonTa9GCH1GxcOEIBsvMVQiHBXHtqvgV2dztjm34kdXV//+9jtCA==", "integrity": "sha512-HDTdlMkTQZ95rd6EpvuLM+xkZV03yGLc38FqI37qKFLJtUN1WnYVaWsuXKoljd1OrVEVsHe6CfqKwaPZ52D56Q==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@expo/config": "~11.0.7", "@expo/config": "~11.0.9",
"@expo/config-plugins": "~10.0.2", "@expo/config-plugins": "~10.0.2",
"@expo/config-types": "^53.0.3", "@expo/config-types": "^53.0.4",
"@expo/image-utils": "^0.7.4", "@expo/image-utils": "^0.7.4",
"@expo/json-file": "^9.1.4", "@expo/json-file": "^9.1.4",
"@react-native/normalize-colors": "0.79.2", "@react-native/normalize-colors": "0.79.2",
@ -7608,9 +7609,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
"integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7621,9 +7622,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
"integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7634,9 +7635,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
"integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7647,9 +7648,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
"integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7660,9 +7661,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
"integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7673,9 +7674,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
"integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7686,9 +7687,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
"integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7699,9 +7700,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
"integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7712,9 +7713,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
"integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7725,9 +7726,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
"integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7738,9 +7739,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
"integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -7751,9 +7752,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
"integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -7764,9 +7765,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
"integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -7777,9 +7778,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
"integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -7790,9 +7791,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
"integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -7803,9 +7804,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
"integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7816,9 +7817,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7829,9 +7830,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7842,9 +7843,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -7855,9 +7856,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8762,9 +8763,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.40", "version": "20.17.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.40.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.43.tgz",
"integrity": "sha512-XNlderXNxSooRdgQFCX2aYoRtHhbUK86Iogm4T7c+pWHbYfVz5frT8ywZ94kXoMjC0f7EReLRiM0tGNtcxXOIA==", "integrity": "sha512-DnDEcDUnVAUYSa7U03QvrXbj1MZj00xoyi/a3lRGkR/c7BFUnqv+OY9EUphMqXUKdZJEOmuzu2mm+LmCisnPow==",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
@ -14553,9 +14554,9 @@
} }
}, },
"node_modules/ethers": { "node_modules/ethers": {
"version": "6.13.7", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.7.tgz", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.0.tgz",
"integrity": "sha512-qbaJ0uIrjh+huP1Lad2f2QtzW5dcqSVjIzVH6yWB4dKoMuj2WqYz5aMeeQTCNpAKgTJBM5J9vcc2cYJ23UAimQ==", "integrity": "sha512-KgHwltNSMdbrGWEyKkM0Rt2s+u1nDH/5BVDQakLinzGEJi4bWindBzZSCC4gKsbZjwDTI6ex/8suR9Ihbmz4IQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -14738,26 +14739,26 @@
} }
}, },
"node_modules/expo": { "node_modules/expo": {
"version": "53.0.7", "version": "53.0.8",
"resolved": "https://registry.npmjs.org/expo/-/expo-53.0.7.tgz", "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.8.tgz",
"integrity": "sha512-ghX529ZG/PnDtSQTzcl3qtt6/i9ktW1Ie8BE5u936MWCiPMwydxzZ/bilM3XlckLqKEsGsqmmpA1eVcWxkm1Ow==", "integrity": "sha512-5CQWayZFDKif++HwfI6ysRNfePYH3MOEZw5edQStQyoL2MehzlasZoICSYHzqptMdMFSt2RTM5Tqgn8L4wYmVg==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/cli": "0.24.11", "@expo/cli": "0.24.12",
"@expo/config": "~11.0.8", "@expo/config": "~11.0.9",
"@expo/config-plugins": "~10.0.2", "@expo/config-plugins": "~10.0.2",
"@expo/fingerprint": "0.12.4", "@expo/fingerprint": "0.12.4",
"@expo/metro-config": "0.20.13", "@expo/metro-config": "0.20.14",
"@expo/vector-icons": "^14.0.0", "@expo/vector-icons": "^14.0.0",
"babel-preset-expo": "~13.1.11", "babel-preset-expo": "~13.1.11",
"expo-asset": "~11.1.5", "expo-asset": "~11.1.5",
"expo-constants": "~17.1.5", "expo-constants": "~17.1.6",
"expo-file-system": "~18.1.9", "expo-file-system": "~18.1.9",
"expo-font": "~13.3.1", "expo-font": "~13.3.1",
"expo-keep-awake": "~14.1.4", "expo-keep-awake": "~14.1.4",
"expo-modules-autolinking": "2.1.9", "expo-modules-autolinking": "2.1.10",
"expo-modules-core": "2.3.12", "expo-modules-core": "2.3.12",
"react-native-edge-to-edge": "1.6.0", "react-native-edge-to-edge": "1.6.0",
"whatwg-url-without-unicode": "8.0.0-3" "whatwg-url-without-unicode": "8.0.0-3"
@ -14803,13 +14804,13 @@
} }
}, },
"node_modules/expo-constants": { "node_modules/expo-constants": {
"version": "17.1.5", "version": "17.1.6",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.5.tgz", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.6.tgz",
"integrity": "sha512-9kjfQjVG6RgBQjFOo7LewxuZgTnYufXPuqpF00Ju5q2dAFW9Eh1SyJpFxbt7KoN+Wwu0hcIr/nQ0lPQugkg07Q==", "integrity": "sha512-q5mLvJiLtPcaZ7t2diSOlQ2AyxIO8YMVEJsEfI/ExkGj15JrflNQ7CALEW6IF/uNae/76qI/XcjEuuAyjdaCNw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@expo/config": "~11.0.7", "@expo/config": "~11.0.9",
"@expo/env": "~1.0.5" "@expo/env": "~1.0.5"
}, },
"peerDependencies": { "peerDependencies": {
@ -14927,9 +14928,9 @@
} }
}, },
"node_modules/expo/node_modules/expo-modules-autolinking": { "node_modules/expo/node_modules/expo-modules-autolinking": {
"version": "2.1.9", "version": "2.1.10",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.9.tgz", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.10.tgz",
"integrity": "sha512-54InfnWy1BR54IDZoawqdFAaF2lyLHe9J+2dZ7y91/36jVpBtAval39ZKt2IISFJZ7TVglsojl4P5BDcDGcvjQ==", "integrity": "sha512-k93fzoszrYTKbZ51DSVnewYIGUV6Gi22Su8qySXPFJEfvtDs2NUUNRHBZNKgLHvwc6xPzVC5j7JYbrpXNuY44A==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19006,9 +19007,9 @@
"dev": true "dev": true
}, },
"node_modules/metro": { "node_modules/metro": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro/-/metro-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro/-/metro-0.82.3.tgz",
"integrity": "sha512-hOBd4O4Cn/tLf3jz7IjSgD/A66MqMzgZuyF1I/pmNwYcY3q3j2vbh7Fa09KIbvUq5Yz7BewU356XboaEtEXPgA==", "integrity": "sha512-EfSLtuUmfsGk3znJ+zoN8cRLniQo3W1wyA+nJMfpTLdENfbbPnGRTwmKhzRcJIUh9jgkrrF4oRQ5shLtQ2DsUw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19033,18 +19034,18 @@
"jest-worker": "^29.7.0", "jest-worker": "^29.7.0",
"jsc-safe-url": "^0.2.2", "jsc-safe-url": "^0.2.2",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"metro-babel-transformer": "0.82.2", "metro-babel-transformer": "0.82.3",
"metro-cache": "0.82.2", "metro-cache": "0.82.3",
"metro-cache-key": "0.82.2", "metro-cache-key": "0.82.3",
"metro-config": "0.82.2", "metro-config": "0.82.3",
"metro-core": "0.82.2", "metro-core": "0.82.3",
"metro-file-map": "0.82.2", "metro-file-map": "0.82.3",
"metro-resolver": "0.82.2", "metro-resolver": "0.82.3",
"metro-runtime": "0.82.2", "metro-runtime": "0.82.3",
"metro-source-map": "0.82.2", "metro-source-map": "0.82.3",
"metro-symbolicate": "0.82.2", "metro-symbolicate": "0.82.3",
"metro-transform-plugins": "0.82.2", "metro-transform-plugins": "0.82.3",
"metro-transform-worker": "0.82.2", "metro-transform-worker": "0.82.3",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"nullthrows": "^1.1.1", "nullthrows": "^1.1.1",
"serialize-error": "^2.1.0", "serialize-error": "^2.1.0",
@ -19061,9 +19062,9 @@
} }
}, },
"node_modules/metro-babel-transformer": { "node_modules/metro-babel-transformer": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.3.tgz",
"integrity": "sha512-c2gesA7/B4dovPmmYC2HziNXb4XFG3YkQ9FjEzwRnR6KH2hT7nJn6mkcri1h85r3sMttpnmoBuZ8WDz980Zhlw==", "integrity": "sha512-eC0f1MSA8rg7VoNDCYMIAIe5AEgYBskh5W8rIa4RGRdmEOsGlXbAV0AWMYoA7NlIALW/S9b10AcdIwD3n1e50w==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19094,25 +19095,25 @@
} }
}, },
"node_modules/metro-cache": { "node_modules/metro-cache": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.3.tgz",
"integrity": "sha512-MxY4xvPKuE68NYpKJjH8YvVVugDL2QcuTracHsV5/30ZIaRr0v1QuAX5vt45OCQDQQWeh1rDv3E4JB6AbIvnZQ==", "integrity": "sha512-9zKhicA5GENROeP+iXku1NrI8FegtwEg3iPXHGixkm1Yppkbwsy/3lSHSiJZoT6GkZmxUDjN6sQ5QQ+/p72Msw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"exponential-backoff": "^3.1.1", "exponential-backoff": "^3.1.1",
"flow-enums-runtime": "^0.0.6", "flow-enums-runtime": "^0.0.6",
"https-proxy-agent": "^7.0.5", "https-proxy-agent": "^7.0.5",
"metro-core": "0.82.2" "metro-core": "0.82.3"
}, },
"engines": { "engines": {
"node": ">=18.18" "node": ">=18.18"
} }
}, },
"node_modules/metro-cache-key": { "node_modules/metro-cache-key": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.3.tgz",
"integrity": "sha512-lfjC9zzSri+rS7lkoCh04LniFga8JQVUqSuscD9KraIm9zRzwIwvaMx8V6Oogiezs+FAJUOSnVNhHcHc9l8H2Q==", "integrity": "sha512-dDLTUOJ7YYqGog9kR55InchwnkkHuxBXD765J3hQVWWPCy6xO9uZXZYGX1Y/tIMV8U7Ho1Sve0V13n5rFajrRQ==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19123,9 +19124,9 @@
} }
}, },
"node_modules/metro-config": { "node_modules/metro-config": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.82.3.tgz",
"integrity": "sha512-0dG3qCFLoE3ddNexAxSLJ7FbGjEbwUjDNOgYeCLoPSkKB01k5itvvr2HFfl2HisOCfLcpjpVzF5NtB/O71lxfA==", "integrity": "sha512-GRG9sBkPvrGXD/Wu3RdEDuWg5NDixF9t0c6Zz9kZ9Aa/aQY+m85JgaCI5HYEV+UzVC/IUFFSpJiMfzQRicppLw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19133,34 +19134,34 @@
"cosmiconfig": "^5.0.5", "cosmiconfig": "^5.0.5",
"flow-enums-runtime": "^0.0.6", "flow-enums-runtime": "^0.0.6",
"jest-validate": "^29.7.0", "jest-validate": "^29.7.0",
"metro": "0.82.2", "metro": "0.82.3",
"metro-cache": "0.82.2", "metro-cache": "0.82.3",
"metro-core": "0.82.2", "metro-core": "0.82.3",
"metro-runtime": "0.82.2" "metro-runtime": "0.82.3"
}, },
"engines": { "engines": {
"node": ">=18.18" "node": ">=18.18"
} }
}, },
"node_modules/metro-core": { "node_modules/metro-core": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.3.tgz",
"integrity": "sha512-d2XMkWbRh6PdPV1OZ8OyUyDWrtEbQ1m5ASpKtemLPbujfoE4RlwFZdl4ljfBNVVZ1s0z7tgsSFwKMyTeXgjtSg==", "integrity": "sha512-JQZDdXo3hyLl1pqVT4IKEwcBK+3f11qFXeCjQ1hjVpjMwQLOqSM02J7NC/4DNSBt+qWBxWj6R5Jphcc7+9AEWw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"flow-enums-runtime": "^0.0.6", "flow-enums-runtime": "^0.0.6",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"metro-resolver": "0.82.2" "metro-resolver": "0.82.3"
}, },
"engines": { "engines": {
"node": ">=18.18" "node": ">=18.18"
} }
}, },
"node_modules/metro-file-map": { "node_modules/metro-file-map": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.3.tgz",
"integrity": "sha512-pax0WA80eRH096YO0kwox+ZD5im3V0Vswr2x1YqdMcZVWlr6uwXgQdo9q+mpcvJ1k77J+hmY5HIg71bqrUptVg==", "integrity": "sha512-o4wtloAge85MZl85F87FT59R/4tn5GvCvLfYcnzzDB20o2YX9AMxZqswrGMaei/GbD/Win5FrLF/Iq8oetcByA==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19204,9 +19205,9 @@
"peer": true "peer": true
}, },
"node_modules/metro-minify-terser": { "node_modules/metro-minify-terser": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.3.tgz",
"integrity": "sha512-+nveaEdQUvsoi0OSr4Cp+btevZsg2DKsu8kUJsvyLIcRRFPUw9CwzF3V2cA5b55DY5LcIJyAcZf4D9ARKfoilQ==", "integrity": "sha512-/3FasOULfHq1P0KPNFy5y28Th5oknPSwEbt9JELVBMAPhUnLqQkCLr4M+RQzKG3aEQN1/mEqenWApFCkk6Nm/Q==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19218,9 +19219,9 @@
} }
}, },
"node_modules/metro-resolver": { "node_modules/metro-resolver": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.3.tgz",
"integrity": "sha512-Who2hGzq2aCGSsBaQBU0L3SADiy/kj/gv0coujNWziRY4SKq7ECKzWqtVk1JlEF7IGXDDRDxEgFuLmPV6mZGVQ==", "integrity": "sha512-pdib7UrOM04j/RjWmaqmjjWRiuCbpA8BdUSuXzvBaK0QlNzHkRRDv6kiOGxgQ+UgG+KdbPcJktsW9olqiDhf9w==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19231,9 +19232,9 @@
} }
}, },
"node_modules/metro-runtime": { "node_modules/metro-runtime": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.3.tgz",
"integrity": "sha512-gEcb2AfDs3GRs2SFjtEmG0k61B/cZEVCbh6cSmkjJpyHr+VRjw77MnDpX9AUcJYa4bCT63E7IEySOMM0Z8p87g==", "integrity": "sha512-J4SrUUsBy9ire8I2sFuXN5MzPmuBHlx1bjvAjdoo1ecpH2mtS3ubRqVnMotBxuK5+GhrbW0mtg5/46PVXy26cw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19245,9 +19246,9 @@
} }
}, },
"node_modules/metro-source-map": { "node_modules/metro-source-map": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.3.tgz",
"integrity": "sha512-S26xPdz1/EeAY0HqaPXfny8CeiY0Dvl4sBLQiXGXhoES4gUDAuMhA1tioKrv5F+x68Sod8cp8Js6EGqbMXeqMA==", "integrity": "sha512-gz7wfjz23rit6ePQ7NKE9x+VOWGKm54vli4wbphR9W+3y0bh6Ad7T0BGH9DUzRAnOnOorewrVEqFmT24mia5sg==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19256,9 +19257,9 @@
"@babel/types": "^7.25.2", "@babel/types": "^7.25.2",
"flow-enums-runtime": "^0.0.6", "flow-enums-runtime": "^0.0.6",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"metro-symbolicate": "0.82.2", "metro-symbolicate": "0.82.3",
"nullthrows": "^1.1.1", "nullthrows": "^1.1.1",
"ob1": "0.82.2", "ob1": "0.82.3",
"source-map": "^0.5.6", "source-map": "^0.5.6",
"vlq": "^1.0.0" "vlq": "^1.0.0"
}, },
@ -19277,15 +19278,15 @@
} }
}, },
"node_modules/metro-symbolicate": { "node_modules/metro-symbolicate": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.3.tgz",
"integrity": "sha512-iheanMnOMned6gjt6sKSfU5AoNyV6pJyQAWydwuHcjhGpa/kiAM0kKmw23qHejELK89Yw8HDZ3Fd/5l1jxpFVA==", "integrity": "sha512-WZKhR+QGbwkOLWP1z58Y7BFWUqLVDEEPsSQ5UI5+OWQDAwdtsPU9+sSNoJtD5qRU9qrB2XewQE3lJ2EQRRFJew==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"flow-enums-runtime": "^0.0.6", "flow-enums-runtime": "^0.0.6",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"metro-source-map": "0.82.2", "metro-source-map": "0.82.3",
"nullthrows": "^1.1.1", "nullthrows": "^1.1.1",
"source-map": "^0.5.6", "source-map": "^0.5.6",
"vlq": "^1.0.0" "vlq": "^1.0.0"
@ -19308,9 +19309,9 @@
} }
}, },
"node_modules/metro-transform-plugins": { "node_modules/metro-transform-plugins": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.3.tgz",
"integrity": "sha512-kEveuEVxghTEXkDiyY0MT5QRqei092KJG46nduo0VghFgI6QFodbAjFit1ULyWsn2VOTGSUDJ3VgHBMy7MaccA==", "integrity": "sha512-s1gVrkhczwMbxZLRSLCJ16K/4Sqx5IhO4sWlL6j0jlIEs1/Drn3JrkUUdQTtgmJS8SBpxmmB66cw7wnz751dVg==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19326,9 +19327,9 @@
} }
}, },
"node_modules/metro-transform-worker": { "node_modules/metro-transform-worker": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.2.tgz", "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.3.tgz",
"integrity": "sha512-MJQNz6cGjqewCRqFmPrsHu6Oe93v2B6zgHkrNxQ6XdPMJz5VHD33m8q+8UsNJOH8wUMoRu5JmYtuUTIVIFxh2A==", "integrity": "sha512-z5Y7nYlSlLAEhjFi73uEJh69G5IC6HFZmXFcrxnY+JNlsjT2r0GgsDF4WaQGtarAIt5NP88V8983/PedwNfEcw==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -19337,13 +19338,13 @@
"@babel/parser": "^7.25.3", "@babel/parser": "^7.25.3",
"@babel/types": "^7.25.2", "@babel/types": "^7.25.2",
"flow-enums-runtime": "^0.0.6", "flow-enums-runtime": "^0.0.6",
"metro": "0.82.2", "metro": "0.82.3",
"metro-babel-transformer": "0.82.2", "metro-babel-transformer": "0.82.3",
"metro-cache": "0.82.2", "metro-cache": "0.82.3",
"metro-cache-key": "0.82.2", "metro-cache-key": "0.82.3",
"metro-minify-terser": "0.82.2", "metro-minify-terser": "0.82.3",
"metro-source-map": "0.82.2", "metro-source-map": "0.82.3",
"metro-transform-plugins": "0.82.2", "metro-transform-plugins": "0.82.3",
"nullthrows": "^1.1.1" "nullthrows": "^1.1.1"
}, },
"engines": { "engines": {
@ -20805,9 +20806,9 @@
"integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==" "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="
}, },
"node_modules/ob1": { "node_modules/ob1": {
"version": "0.82.2", "version": "0.82.3",
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.2.tgz", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.3.tgz",
"integrity": "sha512-sfUaYpjkAdHgu8cXLAyWXO98jW1EUOStTDNslfC9eb3tBLExe67PRqh09J0xdD6AlFKHFGTvXPbuHGvlrZNJNA==", "integrity": "sha512-8/SeymYlPMVODpCATHqm+X8eiuvD1GsKVa11n688V4GGgjrM3CRvrbtrYBs4t89LJDkv5CwGYPdqayuY0DmTTA==",
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@ -23221,9 +23222,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.40.1", "version": "4.40.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.7" "@types/estree": "1.0.7"
@ -23236,26 +23237,26 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.40.1", "@rollup/rollup-android-arm-eabi": "4.40.2",
"@rollup/rollup-android-arm64": "4.40.1", "@rollup/rollup-android-arm64": "4.40.2",
"@rollup/rollup-darwin-arm64": "4.40.1", "@rollup/rollup-darwin-arm64": "4.40.2",
"@rollup/rollup-darwin-x64": "4.40.1", "@rollup/rollup-darwin-x64": "4.40.2",
"@rollup/rollup-freebsd-arm64": "4.40.1", "@rollup/rollup-freebsd-arm64": "4.40.2",
"@rollup/rollup-freebsd-x64": "4.40.1", "@rollup/rollup-freebsd-x64": "4.40.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.1", "@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
"@rollup/rollup-linux-arm-musleabihf": "4.40.1", "@rollup/rollup-linux-arm-musleabihf": "4.40.2",
"@rollup/rollup-linux-arm64-gnu": "4.40.1", "@rollup/rollup-linux-arm64-gnu": "4.40.2",
"@rollup/rollup-linux-arm64-musl": "4.40.1", "@rollup/rollup-linux-arm64-musl": "4.40.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.1", "@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
"@rollup/rollup-linux-riscv64-gnu": "4.40.1", "@rollup/rollup-linux-riscv64-gnu": "4.40.2",
"@rollup/rollup-linux-riscv64-musl": "4.40.1", "@rollup/rollup-linux-riscv64-musl": "4.40.2",
"@rollup/rollup-linux-s390x-gnu": "4.40.1", "@rollup/rollup-linux-s390x-gnu": "4.40.2",
"@rollup/rollup-linux-x64-gnu": "4.40.1", "@rollup/rollup-linux-x64-gnu": "4.40.2",
"@rollup/rollup-linux-x64-musl": "4.40.1", "@rollup/rollup-linux-x64-musl": "4.40.2",
"@rollup/rollup-win32-arm64-msvc": "4.40.1", "@rollup/rollup-win32-arm64-msvc": "4.40.2",
"@rollup/rollup-win32-ia32-msvc": "4.40.1", "@rollup/rollup-win32-ia32-msvc": "4.40.2",
"@rollup/rollup-win32-x64-msvc": "4.40.1", "@rollup/rollup-win32-x64-msvc": "4.40.2",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },

722
src/components/QRScanner/QRScannerDialog.vue

@ -1,722 +0,0 @@
<!-- QRScannerDialog.vue -->
<template>
<div
v-if="visible && !isNativePlatform"
class="dialog-overlay z-[60] fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
>
<div
class="dialog relative bg-white rounded-lg shadow-xl max-w-lg w-full mx-4"
>
<!-- Header -->
<div
class="p-4 border-b border-gray-200 flex justify-between items-center"
>
<div>
<h3 class="text-lg font-medium text-gray-900">Scan QR Code</h3>
<span class="text-xs text-gray-500">v1.1.0</span>
</div>
<button
class="text-gray-400 hover:text-gray-500"
aria-label="Close dialog"
@click="close"
>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<!-- Scanner -->
<div class="p-4">
<div
v-if="useQRReader && !isNativePlatform"
class="relative aspect-square"
>
<!-- Status Message -->
<div
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-center py-2 z-10"
>
<div
v-if="isInitializing"
class="flex items-center justify-center space-x-2"
>
<svg
class="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0
3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span>{{ initializationStatus }}</span>
</div>
<p
v-else-if="isScanning"
class="flex items-center justify-center space-x-2"
>
<span
class="inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"
></span>
<span>Position QR code in the frame</span>
</p>
<p v-else-if="error" class="text-red-300">
<span class="font-medium">Error:</span> {{ error }}
</p>
<p v-else class="flex items-center justify-center space-x-2">
<span
class="inline-block w-2 h-2 bg-blue-500 rounded-full"
></span>
<span>Ready to scan</span>
</p>
</div>
<qrcode-stream
:camera="preferredCamera"
@decode="onDecode"
@init="onInit"
@detect="onDetect"
@error="onError"
@camera-on="onCameraOn"
@camera-off="onCameraOff"
/>
<!-- Scanning Frame -->
<div
class="absolute inset-0 border-2"
:class="{
'border-blue-500': !error && !isScanning,
'border-green-500 animate-pulse': isScanning,
'border-red-500': error,
}"
style="opacity: 0.5; pointer-events: none"
></div>
<!-- Debug Info -->
<div
class="absolute bottom-16 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center py-1"
>
Camera: {{ preferredCamera === "user" ? "Front" : "Back" }} |
Status: {{ cameraStatus }}
</div>
<!-- Camera Switch Button -->
<button
class="absolute bottom-4 right-4 bg-white rounded-full p-2 shadow-lg"
title="Switch camera"
@click="toggleCamera"
>
<svg
class="h-6 w-6 text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0
011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
</div>
<div v-else class="text-center py-8">
<p class="text-gray-500">
{{
isNativePlatform
? "Using native camera scanner..."
: "QR code scanning is not supported in this browser."
}}
</p>
<p v-if="!isNativePlatform" class="text-sm text-gray-400 mt-2">
Please ensure you're using a modern browser with camera access.
</p>
</div>
</div>
<!-- Error Banner -->
<div
v-if="error"
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4"
role="alert"
>
<strong class="font-bold">Camera Error:</strong>
<span class="block sm:inline">{{ error }}</span>
<ul class="mt-2 text-sm text-red-600 list-disc list-inside">
<li v-if="error.includes('No camera found')">
Check if your device has a camera and it is enabled.
</li>
<li v-if="error.includes('denied')">
Allow camera access in your browser settings and reload the page.
</li>
<li v-if="error.includes('in use')">
Close other applications that may be using the camera.
</li>
<li v-if="error.includes('HTTPS')">
Ensure you are using a secure (HTTPS) connection.
</li>
<li
v-if="
!error.includes('No camera found') &&
!error.includes('denied') &&
!error.includes('in use') &&
!error.includes('HTTPS')
"
>
Try refreshing the page or using a different browser/device.
</li>
</ul>
</div>
<!-- Footer -->
<div class="p-4 border-t border-gray-200">
<div class="flex flex-col space-y-4">
<!-- Instructions -->
<div class="text-sm text-gray-600">
<ul class="list-disc list-inside space-y-1">
<li>Ensure the QR code is well-lit and in focus</li>
<li>Hold your device steady</li>
<li>The QR code should fit within the scanning frame</li>
</ul>
</div>
<!-- Error Message -->
<p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
<!-- Actions -->
<div class="flex justify-end space-x-2">
<button
v-if="error"
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
@click="retryScanning"
>
Retry
</button>
<button
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200"
@click="close"
>
Cancel
</button>
<button
class="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600"
@click="copyLogs"
>
Copy Debug Logs
</button>
</div>
</div>
</div>
</div>
</div>
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader";
import { QRScannerOptions } from "@/services/QRScanner/types";
import { logger } from "@/utils/logger";
import { Capacitor } from "@capacitor/core";
import { logCollector } from "@/utils/LogCollector";
interface ScanProps {
onScan: (result: string) => void;
onError?: (error: Error) => void;
options?: QRScannerOptions;
onClose?: () => void;
}
interface DetectionResult {
content?: string;
location?: {
topLeft: { x: number; y: number };
topRight: { x: number; y: number };
bottomLeft: { x: number; y: number };
bottomRight: { x: number; y: number };
};
}
@Component({
components: {
QrcodeStream,
},
})
export default class QRScannerDialog extends Vue {
@Prop({ type: Function, required: true }) onScan!: ScanProps["onScan"];
@Prop({ type: Function }) onError?: ScanProps["onError"];
@Prop({ type: Object }) options?: ScanProps["options"];
@Prop({ type: Function }) onClose?: ScanProps["onClose"];
// Version
readonly version = "1.1.0";
visible = true;
error: string | null = null;
useQRReader = __USE_QR_READER__;
isNativePlatform =
Capacitor.isNativePlatform() ||
__IS_MOBILE__ ||
Capacitor.getPlatform() === "android" ||
Capacitor.getPlatform() === "ios";
isInitializing = true;
isScanning = false;
preferredCamera: "user" | "environment" = "environment";
initializationStatus = "Checking camera access...";
cameraStatus = "Initializing";
errorMessage = "";
created() {
logger.log("[QRScannerDialog] created");
logger.log("[QRScannerDialog] Props received:", {
onScan: typeof this.onScan,
onError: typeof this.onError,
options: this.options,
onClose: typeof this.onClose,
});
logger.log("[QRScannerDialog] Initial state:", {
visible: this.visible,
error: this.error,
useQRReader: this.useQRReader,
isNativePlatform: this.isNativePlatform,
isInitializing: this.isInitializing,
isScanning: this.isScanning,
preferredCamera: this.preferredCamera,
initializationStatus: this.initializationStatus,
cameraStatus: this.cameraStatus,
errorMessage: this.errorMessage,
});
logger.log("QRScannerDialog platform detection:", {
capacitorNative: Capacitor.isNativePlatform(),
isMobile: __IS_MOBILE__,
platform: Capacitor.getPlatform(),
useQRReader: this.useQRReader,
isNativePlatform: this.isNativePlatform,
userAgent: navigator.userAgent,
mediaDevices: !!navigator.mediaDevices,
getUserMedia: !!(
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
),
});
if (this.isNativePlatform) {
logger.log("Closing QR dialog on native platform");
this.$nextTick(() => this.close());
}
}
mounted() {
logger.log("[QRScannerDialog] mounted");
// Timer to warn if no QR code detected after 10 seconds
this._scanTimeout = setTimeout(() => {
if (!this.isScanning) {
logger.warn("[QRScannerDialog] No QR code detected after 10 seconds");
}
}, 10000);
// Periodic timer to log waiting status every 5 seconds
this._waitingInterval = setInterval(() => {
if (!this.isScanning && this.cameraStatus === "Active") {
logger.log("[QRScannerDialog] Still waiting for QR code detection...");
}
}, 5000);
logger.log("[QRScannerDialog] Waiting interval started");
}
beforeUnmount() {
if (this._scanTimeout) {
clearTimeout(this._scanTimeout);
logger.log("[QRScannerDialog] Scan timeout cleared");
}
if (this._waitingInterval) {
clearInterval(this._waitingInterval);
logger.log("[QRScannerDialog] Waiting interval cleared");
}
logger.log("[QRScannerDialog] beforeUnmount");
}
async onInit(promise: Promise<void>): Promise<void> {
logger.log("[QRScannerDialog] onInit called");
if (this.isNativePlatform) {
logger.log("Closing QR dialog on native platform");
this.$nextTick(() => this.close());
return;
}
this.isInitializing = true;
logger.log("[QRScannerDialog] isInitializing set to", this.isInitializing);
this.error = null;
this.initializationStatus = "Checking camera access...";
logger.log(
"[QRScannerDialog] initializationStatus set to",
this.initializationStatus,
);
try {
if (!navigator.mediaDevices) {
logger.log("[QRScannerDialog] Camera API not available");
throw new Error(
"Camera API not available. Please ensure you're using HTTPS.",
);
}
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(device) => device.kind === "videoinput",
);
logger.log("[QRScannerDialog] videoDevices found:", videoDevices.length);
if (videoDevices.length === 0) {
throw new Error("No camera found on this device");
}
this.initializationStatus = "Requesting camera permission...";
logger.log(
"[QRScannerDialog] initializationStatus set to",
this.initializationStatus,
);
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: this.preferredCamera,
width: { ideal: 1280 },
height: { ideal: 720 },
},
});
stream.getTracks().forEach((track) => track.stop());
this.initializationStatus = "Camera permission granted...";
logger.log(
"[QRScannerDialog] initializationStatus set to",
this.initializationStatus,
);
} catch (permissionError) {
const error = permissionError as Error;
logger.log(
"[QRScannerDialog] Camera permission error:",
error.name,
error.message,
);
if (
error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError"
) {
throw new Error(
"Camera access denied. Please grant camera permission and try again.",
);
} else if (
error.name === "NotFoundError" ||
error.name === "DevicesNotFoundError"
) {
throw new Error(
"No camera found. Please ensure your device has a camera.",
);
} else if (
error.name === "NotReadableError" ||
error.name === "TrackStartError"
) {
throw new Error("Camera is in use by another application.");
} else {
throw new Error(`Camera error: ${error.message}`);
}
}
this.initializationStatus = "Starting QR scanner...";
logger.log(
"[QRScannerDialog] initializationStatus set to",
this.initializationStatus,
);
await promise;
this.isInitializing = false;
this.cameraStatus = "Ready";
logger.log("[QRScannerDialog] QR scanner initialized successfully");
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
} catch (error) {
const wrappedError =
error instanceof Error ? error : new Error(String(error));
this.error = wrappedError.message;
this.cameraStatus = "Error";
logger.log(
"[QRScannerDialog] Error initializing QR scanner:",
wrappedError.message,
);
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
if (this.onError) {
this.onError(wrappedError);
}
} finally {
this.isInitializing = false;
logger.log(
"[QRScannerDialog] isInitializing set to",
this.isInitializing,
);
}
}
onCameraOn(): void {
this.cameraStatus = "Active";
logger.log("[QRScannerDialog] Camera turned on successfully");
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
}
onCameraOff(): void {
this.cameraStatus = "Off";
logger.log("[QRScannerDialog] Camera turned off");
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
}
onDetect(result: DetectionResult | Promise<DetectionResult>): void {
const ts = new Date().toISOString();
logger.log(`[QRScannerDialog] onDetect called at ${ts} with`, result);
this.isScanning = true;
this.cameraStatus = "Detecting";
logger.log("[QRScannerDialog] isScanning set to", this.isScanning);
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
const processResult = (detection: DetectionResult | DetectionResult[]) => {
try {
logger.log(
`[QRScannerDialog] onDetect exit at ${new Date().toISOString()} with detection:`,
detection,
);
// Fallback: If detection is an array, check the first element
let rawValue: string | undefined;
if (
Array.isArray(detection) &&
detection.length > 0 &&
"rawValue" in detection[0]
) {
rawValue = detection[0].rawValue;
} else if (
detection &&
typeof detection === "object" &&
"rawValue" in detection &&
detection.rawValue
) {
rawValue = (detection as unknown).rawValue;
}
if (rawValue) {
logger.log(
"[QRScannerDialog] Fallback: Detected rawValue, treating as scan:",
rawValue,
);
this.isInitializing = false;
this.initializationStatus = "QR code captured!";
this.onScan(rawValue);
try {
logger.log("[QRScannerDialog] About to call close() after scan");
this.close();
logger.log(
"[QRScannerDialog] close() called successfully after scan",
);
} catch (err) {
logger.error("[QRScannerDialog] Error calling close():", err);
}
}
} catch (error) {
this.handleError(error);
} finally {
this.isScanning = false;
this.cameraStatus = "Active";
logger.log("[QRScannerDialog] isScanning set to", this.isScanning);
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
}
};
if (result instanceof Promise) {
result
.then(processResult)
.catch((error: Error) => this.handleError(error))
.finally(() => {
this.isScanning = false;
this.cameraStatus = "Active";
logger.log("[QRScannerDialog] isScanning set to", this.isScanning);
logger.log(
"[QRScannerDialog] cameraStatus set to",
this.cameraStatus,
);
});
} else {
processResult(result);
}
}
private handleError(error: unknown): void {
const wrappedError =
error instanceof Error ? error : new Error(String(error));
this.error = wrappedError.message;
this.cameraStatus = "Error";
logger.log("[QRScannerDialog] handleError:", wrappedError.message);
logger.log("[QRScannerDialog] cameraStatus set to", this.cameraStatus);
if (this.onError) {
this.onError(wrappedError);
}
}
onDecode(result: string): void {
const ts = new Date().toISOString();
logger.log(
`[QRScannerDialog] onDecode called at ${ts} with result:`,
result,
);
try {
this.isInitializing = false;
this.initializationStatus = "QR code captured!";
logger.log(
"[QRScannerDialog] UI state updated after scan: isInitializing set to",
this.isInitializing,
", initializationStatus set to",
this.initializationStatus,
);
this.onScan(result);
this.close();
logger.log(
`[QRScannerDialog] onDecode exit at ${new Date().toISOString()}`,
);
} catch (error) {
this.handleError(error);
}
}
toggleCamera(): void {
const prevCamera = this.preferredCamera;
this.preferredCamera =
this.preferredCamera === "user" ? "environment" : "user";
logger.log(
"[QRScannerDialog] toggleCamera from",
prevCamera,
"to",
this.preferredCamera,
);
logger.log(
"[QRScannerDialog] preferredCamera set to",
this.preferredCamera,
);
}
retryScanning(): void {
logger.log("[QRScannerDialog] retryScanning called");
this.error = null;
this.isInitializing = true;
logger.log("[QRScannerDialog] isInitializing set to", this.isInitializing);
logger.log("[QRScannerDialog] Scanning re-initialized");
}
close = async (): Promise<void> => {
logger.log("[QRScannerDialog] close called");
this.visible = false;
logger.log("[QRScannerDialog] visible set to", this.visible);
// Notify parent/service
if (typeof this.onClose === "function") {
logger.log("[QRScannerDialog] Calling onClose prop");
this.onClose();
}
await this.$nextTick();
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
logger.log("[QRScannerDialog] Dialog element removed from DOM");
} else {
logger.log("[QRScannerDialog] Dialog element NOT removed from DOM");
}
};
onScanDetect(promisedResult) {
const ts = new Date().toISOString();
logger.log(
`[QRScannerDialog] onScanDetect called at ${ts} with`,
promisedResult,
);
promisedResult
.then((result) => {
logger.log(
`[QRScannerDialog] onScanDetect exit at ${new Date().toISOString()} with result:`,
result,
);
this.onScan(result);
})
.catch((error) => {
logger.error(
`[QRScannerDialog] onScanDetect error at ${new Date().toISOString()}:`,
error,
);
this.errorMessage = error.message || "Scan error";
if (this.onError) this.onError(error);
});
}
onScanError(error) {
const ts = new Date().toISOString();
logger.error(`[QRScannerDialog] onScanError called at ${ts}:`, error);
this.errorMessage = error.message || "Camera error";
if (this.onError) this.onError(error);
}
async startMobileScan() {
try {
logger.log("[QRScannerDialog] startMobileScan called");
const scanner = QRScannerFactory.getInstance();
await scanner.startScan();
} catch (error) {
logger.error("[QRScannerDialog] Error starting mobile scan:", error);
if (this.onError) this.onError(error);
}
}
async copyLogs() {
logger.log("[QRScannerDialog] copyLogs called");
try {
await navigator.clipboard.writeText(logCollector.getLogs());
alert("Logs copied to clipboard!");
} catch (e) {
alert("Failed to copy logs: " + (e instanceof Error ? e.message : e));
}
}
}
</script>
<style scoped>
.dialog-overlay {
backdrop-filter: blur(4px);
}
.qrcode-stream {
width: 100%;
height: 100%;
}
@keyframes pulse {
0% {
opacity: 0.5;
}
50% {
opacity: 0.75;
}
100% {
opacity: 0.5;
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>

15
src/services/QRScanner/QRScannerService.ts

@ -1,15 +0,0 @@
export interface QRScannerListener {
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: QRScannerListener): void;
cleanup(): Promise<void>;
onStream(callback: (stream: MediaStream | null) => void): void;
}

263
src/services/QRScanner/WebDialogQRScanner.ts

@ -1,263 +0,0 @@
import { createApp, App } from "vue";
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
import QRScannerDialog from "@/components/QRScanner/QRScannerDialog.vue";
import { logger } from "@/utils/logger";
export class WebDialogQRScanner implements QRScannerService {
private dialogInstance: App | null = null;
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
private scanListener: ScanListener | null = null;
private isScanning = false;
private container: HTMLElement | null = null;
private sessionId: number | null = null;
private failsafeTimeout: unknown = null;
constructor(private options?: QRScannerOptions) {}
async checkPermissions(): Promise<boolean> {
try {
logger.log("[QRScanner] Checking camera permissions...");
const permissions = await navigator.permissions.query({
name: "camera" as PermissionName,
});
logger.log("[QRScanner] Permission state:", permissions.state);
return permissions.state === "granted";
} catch (error) {
logger.error("[QRScanner] Error checking camera permissions:", error);
return false;
}
}
async requestPermissions(): Promise<boolean> {
try {
// First check if we have any video devices
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(
(device) => device.kind === "videoinput",
);
if (videoDevices.length === 0) {
logger.error("No video devices found");
throw new Error("No camera found on this device");
}
// Try to get a stream with specific constraints
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
},
});
// Stop the test stream immediately
stream.getTracks().forEach((track) => track.stop());
return true;
} catch (error) {
const wrappedError =
error instanceof Error ? error : new Error(String(error));
logger.error("Error requesting camera permissions:", {
error: wrappedError.message,
stack: wrappedError.stack,
name: wrappedError.name,
});
// Provide more specific error messages
if (
wrappedError.name === "NotFoundError" ||
wrappedError.name === "DevicesNotFoundError"
) {
throw new Error("No camera found on this device");
} else if (
wrappedError.name === "NotAllowedError" ||
wrappedError.name === "PermissionDeniedError"
) {
throw new Error(
"Camera access denied. Please grant camera permission and try again",
);
} else if (
wrappedError.name === "NotReadableError" ||
wrappedError.name === "TrackStartError"
) {
throw new Error("Camera is in use by another application");
} else {
throw new Error(`Camera error: ${wrappedError.message}`);
}
}
}
async isSupported(): Promise<boolean> {
try {
// Check for secure context first
if (!window.isSecureContext) {
logger.warn("Camera access requires HTTPS (secure context)");
return false;
}
// Check for camera API support
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
logger.warn("Camera API not supported in this browser");
return false;
}
// Check if we have any video devices
const devices = await navigator.mediaDevices.enumerateDevices();
const hasVideoDevices = devices.some(
(device) => device.kind === "videoinput",
);
if (!hasVideoDevices) {
logger.warn("No video devices found");
return false;
}
return true;
} catch (error) {
logger.error("Error checking camera support:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
return false;
}
}
async startScan(): Promise<void> {
if (this.isScanning) {
return;
}
try {
this.isScanning = true;
this.sessionId = Date.now();
logger.log(
`[WebDialogQRScanner] Opening dialog, session: ${this.sessionId}`,
);
// Create and mount dialog component
this.container = document.createElement("div");
document.body.appendChild(this.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: () => {
logger.log(
`[WebDialogQRScanner] onClose received from dialog, session: ${this.sessionId}`,
);
this.stopScan("dialog onClose");
},
options: this.options,
sessionId: this.sessionId,
});
this.dialogComponent = this.dialogInstance.mount(
this.container,
) as InstanceType<typeof QRScannerDialog>;
// Failsafe: force cleanup after 60s if dialog is still open
this.failsafeTimeout = setTimeout(() => {
if (this.isScanning) {
logger.warn(
`[WebDialogQRScanner] Failsafe triggered, forcing cleanup for session: ${this.sessionId}`,
);
this.stopScan("failsafe timeout");
}
}, 60000);
logger.log(
`[WebDialogQRScanner] Failsafe timeout set for session: ${this.sessionId}`,
);
} catch (error) {
this.isScanning = false;
const wrappedError =
error instanceof Error ? error : new Error(String(error));
if (this.scanListener?.onError) {
this.scanListener.onError(wrappedError);
}
logger.error("Error starting scan:", wrappedError);
this.cleanupContainer();
throw wrappedError;
}
}
async stopScan(reason: string = "manual"): Promise<void> {
if (!this.isScanning) {
return;
}
try {
logger.log(
`[WebDialogQRScanner] stopScan called, reason: ${reason}, session: ${this.sessionId}`,
);
if (this.dialogComponent) {
await this.dialogComponent.close();
logger.log(
`[WebDialogQRScanner] dialogComponent.close() called, session: ${this.sessionId}`,
);
}
if (this.dialogInstance) {
this.dialogInstance.unmount();
logger.log(
`[WebDialogQRScanner] dialogInstance.unmount() called, session: ${this.sessionId}`,
);
}
} catch (error) {
const wrappedError =
error instanceof Error ? error : new Error(String(error));
logger.error("Error stopping scan:", wrappedError);
throw wrappedError;
} finally {
this.isScanning = false;
if (this.failsafeTimeout) {
clearTimeout(this.failsafeTimeout);
this.failsafeTimeout = null;
logger.log(
`[WebDialogQRScanner] Failsafe timeout cleared, session: ${this.sessionId}`,
);
}
this.cleanupContainer();
}
}
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
private cleanupContainer(): void {
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
logger.log(
`[WebDialogQRScanner] Dialog container removed from DOM, session: ${this.sessionId}`,
);
} else {
logger.log(
`[WebDialogQRScanner] Dialog container NOT removed from DOM, session: ${this.sessionId}`,
);
}
this.container = null;
}
async cleanup(): Promise<void> {
try {
await this.stopScan("cleanup");
} catch (error) {
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();
this.sessionId = null;
}
}
}

15
src/views/ContactQRScanShowView.vue

@ -75,8 +75,13 @@
</div> </div>
<div class="text-center"> <div class="text-center">
<h2 class="text-2xl text-center font-semibold mt-6 mb-2">Scan Contact Info</h2> <h2 class="text-2xl text-center font-semibold mt-6 mb-2">
<div v-if="isScanning" class="relative aspect-square rounded-xl overflow-hidden bg-slate-800 w-[67vw] max-w-[33vh] mx-auto"> Scan Contact Info
</h2>
<div
v-if="isScanning"
class="relative aspect-square rounded-xl overflow-hidden bg-slate-800 w-[67vw] max-w-[33vh] mx-auto"
>
<!-- Status Message --> <!-- Status Message -->
<div <div
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10" class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10"
@ -117,9 +122,7 @@
></span> ></span>
<span>Position QR code in the frame</span> <span>Position QR code in the frame</span>
</p> </p>
<p v-else-if="error" class="text-red-400"> <p v-else-if="error" class="text-red-400">Error: {{ error }}</p>
Error: {{ error }}
</p>
<p v-else class="flex items-center justify-center space-x-2"> <p v-else class="flex items-center justify-center space-x-2">
<span class="inline-block w-2 h-2 bg-blue-500 rounded-full"></span> <span class="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
<span>Ready to scan</span> <span>Ready to scan</span>
@ -157,7 +160,7 @@
<font-awesome icon="camera-rotate" class="size-6 text-gray-600" /> <font-awesome icon="camera-rotate" class="size-6 text-gray-600" />
</button> </button>
</div> </div>
<div class="mt-4" v-else> <div v-else class="mt-4">
<button <button
v-if="isNativePlatform" v-if="isNativePlatform"
class="bg-blue-500 text-white px-4 py-2 rounded-md" class="bg-blue-500 text-white px-4 py-2 rounded-md"

Loading…
Cancel
Save