From a8812714a343cebc02d6945e12b00953c803c2e1 Mon Sep 17 00:00:00 2001
From: Matthew Raymer <matthew.raymer@anomalistdesign.com>
Date: Tue, 22 Apr 2025 10:00:37 +0000
Subject: [PATCH] fix(qr): improve QR scanner implementation and error handling

- Implement robust QR scanner factory with platform detection
- Add proper camera permissions to Android manifest
- Improve error handling and logging across scanner implementations
- Add continuous scanning mode for Capacitor/MLKit scanner
- Enhance UI feedback during scanning process
- Fix build configuration for proper platform detection
- Clean up resources properly in scanner components
- Add TypeScript improvements and error wrapping

The changes include:
- Adding CAMERA permission to AndroidManifest.xml
- Setting proper build flags (__IS_MOBILE__, __USE_QR_READER__)
- Implementing continuous scanning mode for better UX
- Adding proper cleanup of scanner resources
- Improving error handling and type safety
- Enhancing UI with loading states and error messages
---
 .../rules/qr-code-implementation-guide.mdc    | 578 ++++++++----------
 android/.gradle/file-system.probe             | Bin 8 -> 8 bytes
 android/app/src/main/AndroidManifest.xml      |   2 +
 package.json                                  |   5 +-
 src/components/QRScanner/QRScannerDialog.vue  |  26 +-
 src/services/QRScanner/CapacitorQRScanner.ts  |  65 +-
 src/services/QRScanner/QRScannerFactory.ts    |  53 +-
 src/services/QRScanner/WebDialogQRScanner.ts  |  58 +-
 src/views/ContactQRScanShowView.vue           | 194 +++---
 vite.config.common.mts                        |   2 +
 10 files changed, 540 insertions(+), 443 deletions(-)

diff --git a/.cursor/rules/qr-code-implementation-guide.mdc b/.cursor/rules/qr-code-implementation-guide.mdc
index 0ba19bff..0f0a9b21 100644
--- a/.cursor/rules/qr-code-implementation-guide.mdc
+++ b/.cursor/rules/qr-code-implementation-guide.mdc
@@ -133,289 +133,61 @@ export class CapacitorQRScanner implements QRScannerService {
     }
   }
 
-  // Implement other interface methods...
-}
-```
-
-5. **Implement Web Scanner**
-```typescript
-// WebDialogQRScanner.ts
-export class WebDialogQRScanner implements QRScannerService {
-  private dialogInstance: App | null = null;
-  private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
-  private scanListener: ScanListener | null = null;
-  async checkPermissions(): Promise<boolean> {
+  async requestPermissions() {
     try {
-      const permissions = await navigator.permissions.query({
-        name: 'camera' as PermissionName
-      });
-      return permissions.state === 'granted';
+      const { camera } = await BarcodeScanner.requestPermissions();
+      return camera === 'granted';
     } catch (error) {
-      logger.error('Error checking camera permissions:', error);
+      logger.error('Error requesting camera permissions:', error);
       return false;
     }
   }
 
-  // Implement other interface methods...
-}
-```
-
-6. **Create Dialog Component**
-```vue
-<!-- QRScannerDialog.vue -->
-<template>
-  <div v-if="visible" class="dialog-overlay z-[60]">
-    <div class="dialog relative">
-      <!-- Dialog content -->
-      <div v-if="useQRReader">
-        <qrcode-stream
-          class="w-full max-w-lg mx-auto"
-          @detect="onScanDetect"
-          @error="onScanError"
-        />
-      </div>
-      <div v-else>
-        <!-- Mobile camera button -->
-      </div>
-    </div>
-  </div>
-</template>
-
-<script lang="ts">
-@Component({
-  components: { QrcodeStream }
-})
-export default class QRScannerDialog extends Vue {
-  // Implementation...
-}
-</script>
-```
-
-## Usage Example
-
-```typescript
-// In your component
-async function scanQRCode() {
-  const scanner = QRScannerFactory.getInstance();
-  
-  if (!(await scanner.checkPermissions())) {
-    const granted = await scanner.requestPermissions();
-    if (!granted) {
-      throw new Error('Camera permission denied');
-    }
+  async isSupported() {
+    return Capacitor.isNativePlatform();
   }
 
-  scanner.addListener({
-    onScan: (result) => {
-      console.log('Scanned:', result);
-    },
-    onError: (error) => {
-      console.error('Scan error:', error);
-    }
-  });
-
-  await scanner.startScan();
-}
-
-// Cleanup when done
-onUnmounted(() => {
-  QRScannerFactory.cleanup();
-});
-```
-
-## Platform-Specific Notes
-
-### Mobile (Capacitor)
-- Uses MLKit for optimal performance
-- Handles native permissions
-- Supports both iOS and Android
-- Uses back camera by default
-- Handles device rotation
-
-### Web
-- Uses MediaDevices API
-- Requires HTTPS for camera access
-- Handles browser compatibility
-- Manages memory and resources
-- Provides fallback UI
-
-## Testing
-
-1. **Unit Tests**
-- Test factory pattern
-- Test platform detection
-- Test error handling
-- Test cleanup procedures
-
-2. **Integration Tests**
-- Test permission flows
-- Test camera access
-- Test QR code detection
-- Test cross-platform behavior
-
-3. **E2E Tests**
-- Test full scanning workflow
-- Test UI feedback
-- Test error scenarios
-- Test platform differences
-
-## Common Issues and Solutions
-
-1. **Permission Handling**
-- Always check permissions first
-- Provide clear user feedback
-- Handle denial gracefully
-- Implement retry logic
-
-2. **Resource Management**
-- Clean up after scanning
-- Handle component unmounting
-- Release camera resources
-- Clear event listeners
-
-3. **Error Handling**
-- Log errors appropriately
-- Provide user feedback
-- Implement fallbacks
-- Handle edge cases
-
-4. **Performance**
-- Optimize camera preview
-- Handle memory usage
-- Manage battery impact
-- Consider device capabilities
-
-# QR Code Implementation Guide
-
-## Directory Structure
-
-```
-src/
-├── components/
-│   └── QRScanner/
-│       ├── types.ts
-│       ├── factory.ts
-│       ├── CapacitorScanner.ts
-│       ├── WebDialogScanner.ts
-│       └── QRScannerDialog.vue
-├── services/
-│   └── QRScanner/
-│       ├── types.ts
-│       ├── QRScannerFactory.ts
-│       ├── CapacitorQRScanner.ts
-│       └── WebDialogQRScanner.ts
-```
-
-## Core Interfaces
-
-```typescript
-// types.ts
-export interface ScanListener {
-  onScan: (result: string) => void;
-  onError?: (error: Error) => void;
-}
-
-export interface QRScannerService {
-  checkPermissions(): Promise<boolean>;
-  requestPermissions(): Promise<boolean>;
-  isSupported(): Promise<boolean>;
-  startScan(): Promise<void>;
-  stopScan(): Promise<void>;
-  addListener(listener: ScanListener): void;
-  cleanup(): Promise<void>;
-}
-```
-
-## Configuration Files
-
-### Vite Configuration
-```typescript
-// vite.config.ts
-export default defineConfig({
-  define: {
-    __USE_QR_READER__: JSON.stringify(!isMobile),
-    __IS_MOBILE__: JSON.stringify(isMobile),
-  },
-  build: {
-    rollupOptions: {
-      external: isMobile ? ['vue-qrcode-reader'] : [],
-    }
-  }
-});
-```
+  async startScan() {
+    if (this.isScanning) return;
+    this.isScanning = true;
 
-### Capacitor Configuration
-```typescript
-// capacitor.config.ts
-const config: CapacitorConfig = {
-  plugins: {
-    MLKitBarcodeScanner: {
-      formats: ['QR_CODE'],
-      detectorSize: 1.0,
-      lensFacing: 'back',
-      googleBarcodeScannerModuleInstallState: true
+    try {
+      await BarcodeScanner.startScan();
+    } catch (error) {
+      this.isScanning = false;
+      throw error;
     }
   }
-};
-```
-
-## Implementation Steps
-
-1. **Install Dependencies**
-```bash
-npm install @capacitor-mlkit/barcode-scanning vue-qrcode-reader
-```
 
-2. **Create Core Types**
-Create the interface files as shown above.
-
-3. **Implement Factory**
-```typescript
-// QRScannerFactory.ts
-export class QRScannerFactory {
-  private static instance: QRScannerService | null = null;
+  async stopScan() {
+    if (!this.isScanning) return;
+    this.isScanning = false;
 
-  static getInstance(): QRScannerService {
-    if (!this.instance) {
-      if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
-        this.instance = new CapacitorQRScanner();
-      } else if (__USE_QR_READER__) {
-        this.instance = new WebDialogQRScanner();
-      } else {
-        throw new Error('No QR scanner implementation available');
-      }
+    try {
+      await BarcodeScanner.stopScan();
+    } catch (error) {
+      logger.error('Error stopping scan:', error);
     }
-    return this.instance;
   }
 
-  static async cleanup() {
-    if (this.instance) {
-      await this.instance.cleanup();
-      this.instance = null;
-    }
+  addListener(listener: ScanListener) {
+    this.scanListener = listener;
+    const handle = BarcodeScanner.addListener('barcodeScanned', (result) => {
+      if (this.scanListener) {
+        this.scanListener.onScan(result.barcode);
+      }
+    });
+    this.listenerHandles.push(handle.remove);
   }
-}
-```
 
-4. **Implement Mobile Scanner**
-```typescript
-// CapacitorQRScanner.ts
-export class CapacitorQRScanner implements QRScannerService {
-  private scanListener: ScanListener | null = null;
-  private isScanning = false;
-  private listenerHandles: Array<() => Promise<void>> = [];
-
-  async checkPermissions() {
-    try {
-      const { camera } = await BarcodeScanner.checkPermissions();
-      return camera === 'granted';
-    } catch (error) {
-      logger.error('Error checking camera permissions:', error);
-      return false;
+  async cleanup() {
+    await this.stopScan();
+    for (const handle of this.listenerHandles) {
+      await handle();
     }
+    this.listenerHandles = [];
+    this.scanListener = null;
   }
-
-  // Implement other interface methods...
 }
 ```
 
@@ -426,6 +198,7 @@ export class WebDialogQRScanner implements QRScannerService {
   private dialogInstance: App | null = null;
   private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
   private scanListener: ScanListener | null = null;
+
   async checkPermissions(): Promise<boolean> {
     try {
       const permissions = await navigator.permissions.query({
@@ -438,7 +211,61 @@ export class WebDialogQRScanner implements QRScannerService {
     }
   }
 
-  // Implement other interface methods...
+  async requestPermissions(): Promise<boolean> {
+    try {
+      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+      stream.getTracks().forEach(track => track.stop());
+      return true;
+    } catch (error) {
+      logger.error('Error requesting camera permissions:', error);
+      return false;
+    }
+  }
+
+  async isSupported(): Promise<boolean> {
+    return 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices;
+  }
+
+  async startScan() {
+    if (this.dialogInstance) return;
+
+    const container = document.createElement('div');
+    document.body.appendChild(container);
+
+    this.dialogInstance = createApp(QRScannerDialog, {
+      onScan: (result: string) => {
+        if (this.scanListener) {
+          this.scanListener.onScan(result);
+        }
+      },
+      onError: (error: Error) => {
+        if (this.scanListener?.onError) {
+          this.scanListener.onError(error);
+        }
+      },
+      onClose: () => {
+        this.cleanup();
+      }
+    });
+
+    this.dialogComponent = this.dialogInstance.mount(container) as InstanceType<typeof QRScannerDialog>;
+  }
+
+  async stopScan() {
+    await this.cleanup();
+  }
+
+  addListener(listener: ScanListener) {
+    this.scanListener = listener;
+  }
+
+  async cleanup() {
+    if (this.dialogInstance) {
+      this.dialogInstance.unmount();
+      this.dialogInstance = null;
+      this.dialogComponent = null;
+    }
+  }
 }
 ```
 
@@ -448,29 +275,125 @@ export class WebDialogQRScanner implements QRScannerService {
 <template>
   <div v-if="visible" class="dialog-overlay z-[60]">
     <div class="dialog relative">
-      <!-- Dialog content -->
-      <div v-if="useQRReader">
-        <qrcode-stream
-          class="w-full max-w-lg mx-auto"
-          @detect="onScanDetect"
-          @error="onScanError"
-        />
+      <div class="dialog-header">
+        <h2>Scan QR Code</h2>
+        <button @click="onClose" class="close-button">×</button>
       </div>
-      <div v-else>
-        <!-- Mobile camera button -->
+      <div class="dialog-content">
+        <div v-if="useQRReader">
+          <qrcode-stream
+            class="w-full max-w-lg mx-auto"
+            @detect="onScanDetect"
+            @error="onScanError"
+          />
+        </div>
+        <div v-else>
+          <button @click="startMobileScan" class="scan-button">
+            Start Camera
+          </button>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script lang="ts">
-@Component({
-  components: { QrcodeStream }
-})
-export default class QRScannerDialog extends Vue {
-  // Implementation...
-}
+import { defineComponent } from 'vue';
+import { QrcodeStream } from 'vue-qrcode-reader';
+
+export default defineComponent({
+  name: 'QRScannerDialog',
+  components: { QrcodeStream },
+  props: {
+    onScan: {
+      type: Function,
+      required: true
+    },
+    onError: {
+      type: Function,
+      required: true
+    },
+    onClose: {
+      type: Function,
+      required: true
+    }
+  },
+  data() {
+    return {
+      visible: true,
+      useQRReader: __USE_QR_READER__
+    };
+  },
+  methods: {
+    onScanDetect(promisedResult: Promise<string>) {
+      promisedResult
+        .then(result => this.onScan(result))
+        .catch(error => this.onError(error));
+    },
+    onScanError(error: Error) {
+      this.onError(error);
+    },
+    async startMobileScan() {
+      try {
+        const scanner = QRScannerFactory.getInstance();
+        await scanner.startScan();
+      } catch (error) {
+        this.onError(error as Error);
+      }
+    }
+  }
+});
 </script>
+
+<style scoped>
+.dialog-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.dialog {
+  background: white;
+  border-radius: 8px;
+  padding: 20px;
+  max-width: 90vw;
+  max-height: 90vh;
+  overflow: auto;
+}
+
+.dialog-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.close-button {
+  background: none;
+  border: none;
+  font-size: 24px;
+  cursor: pointer;
+}
+
+.scan-button {
+  background: #4CAF50;
+  color: white;
+  padding: 10px 20px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.scan-button:hover {
+  background: #45a049;
+}
+</style>
 ```
 
 ## Usage Example
@@ -513,6 +436,7 @@ onUnmounted(() => {
 - Supports both iOS and Android
 - Uses back camera by default
 - Handles device rotation
+- Provides native UI for scanning
 
 ### Web
 - Uses MediaDevices API
@@ -520,6 +444,7 @@ onUnmounted(() => {
 - Handles browser compatibility
 - Manages memory and resources
 - Provides fallback UI
+- Uses vue-qrcode-reader for web scanning
 
 ## Testing
 
@@ -528,41 +453,74 @@ onUnmounted(() => {
 - Test platform detection
 - Test error handling
 - Test cleanup procedures
+- Test permission flows
 
 2. **Integration Tests**
-- Test permission flows
 - Test camera access
 - Test QR code detection
 - Test cross-platform behavior
+- Test UI components
+- Test error scenarios
 
 3. **E2E Tests**
-- Test full scanning workflow
-- Test UI feedback
-- Test error scenarios
-- Test platform differences
-
-## Common Issues and Solutions
-
-1. **Permission Handling**
-- Always check permissions first
-- Provide clear user feedback
-- Handle denial gracefully
-- Implement retry logic
-
-2. **Resource Management**
-- Clean up after scanning
-- Handle component unmounting
-- Release camera resources
-- Clear event listeners
-
-3. **Error Handling**
-- Log errors appropriately
-- Provide user feedback
-- Implement fallbacks
-- Handle edge cases
-
-4. **Performance**
-- Optimize camera preview
-- Handle memory usage
-- Manage battery impact
-- Consider device capabilities
+- Test complete scanning flow
+- Test permission handling
+- Test cross-platform compatibility
+- Test error recovery
+- Test cleanup procedures
+
+## Best Practices
+
+1. **Error Handling**
+- Always handle permission errors gracefully
+- Provide clear error messages to users
+- Implement proper cleanup on errors
+- Log errors for debugging
+
+2. **Performance**
+- Clean up resources when not in use
+- Handle device rotation properly
+- Optimize camera usage
+- Manage memory efficiently
+
+3. **Security**
+- Request minimum required permissions
+- Handle sensitive data securely
+- Validate scanned data
+- Implement proper cleanup
+
+4. **User Experience**
+- Provide clear feedback
+- Handle edge cases gracefully
+- Support both platforms seamlessly
+- Implement proper loading states
+
+## Troubleshooting
+
+1. **Common Issues**
+- Camera permissions denied
+- Device not supported
+- Scanner not working
+- Memory leaks
+- UI glitches
+
+2. **Solutions**
+- Check permissions
+- Verify device support
+- Debug scanner implementation
+- Monitor memory usage
+- Test UI components
+
+## Maintenance
+
+1. **Regular Updates**
+- Keep dependencies updated
+- Monitor platform changes
+- Update documentation
+- Review security patches
+
+2. **Performance Monitoring**
+- Track memory usage
+- Monitor camera performance
+- Check error rates
+- Analyze user feedback
diff --git a/android/.gradle/file-system.probe b/android/.gradle/file-system.probe
index 44390a335b6402f9ddb8b68a72a8e49b763285fe..21f85321bbba8c0d15fb09c1ad15248d07005fc8 100644
GIT binary patch
literal 8
PcmZQzV4M~+FMc@y2de^@

literal 8
PcmZQzV4Nn^HRTNe2NnXn

diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 70ac8410..991dd37e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -41,4 +41,6 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" android:required="true" />
 </manifest>
diff --git a/package.json b/package.json
index 15ac17ea..a43aa208 100644
--- a/package.json
+++ b/package.json
@@ -23,11 +23,12 @@
     "clean:electron": "rimraf dist-electron",
     "build:pywebview": "vite build --config vite.config.pywebview.mts",
     "build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
-    "build:capacitor": "vite build --config vite.config.capacitor.mts",
+    "build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
     "build:web": "vite build --config vite.config.web.mts",
     "electron:dev": "npm run build && electron dist-electron",
     "electron:start": "electron dist-electron",
-    "build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
+    "clean:android": "adb uninstall app.timesafari.app || true",
+    "build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
     "electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
     "electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
     "electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
diff --git a/src/components/QRScanner/QRScannerDialog.vue b/src/components/QRScanner/QRScannerDialog.vue
index 4e102aaf..9f2efebe 100644
--- a/src/components/QRScanner/QRScannerDialog.vue
+++ b/src/components/QRScanner/QRScannerDialog.vue
@@ -80,15 +80,21 @@ import { QRScannerOptions } from "@/services/QRScanner/types";
 import { logger } from "@/utils/logger";
 import { Capacitor } from "@capacitor/core";
 
+interface ScanProps {
+  onScan: (result: string) => void;
+  onError?: (error: Error) => void;
+  options?: QRScannerOptions;
+}
+
 @Component({
   components: {
     QrcodeStream,
   },
 })
 export default class QRScannerDialog extends Vue {
-  @Prop({ type: Function, required: true }) onScan!: (result: string) => void;
-  @Prop({ type: Function }) onError?: (error: Error) => void;
-  @Prop({ type: Object }) options?: QRScannerOptions;
+  @Prop({ type: Function, required: true }) onScan!: ScanProps['onScan'];
+  @Prop({ type: Function }) onError?: ScanProps['onError'];
+  @Prop({ type: Object }) options?: ScanProps['options'];
 
   visible = true;
   error: string | null = null;
@@ -126,11 +132,12 @@ export default class QRScannerDialog extends Vue {
       await promise;
       this.error = null;
     } catch (error) {
-      this.error = error instanceof Error ? error.message : String(error);
+      const wrappedError = error instanceof Error ? error : new Error(String(error));
+      this.error = wrappedError.message;
       if (this.onError) {
-        this.onError(error instanceof Error ? error : new Error(String(error)));
+        this.onError(wrappedError);
       }
-      logger.error("Error initializing QR scanner:", error);
+      logger.error("Error initializing QR scanner:", wrappedError);
     }
   }
 
@@ -139,11 +146,12 @@ export default class QRScannerDialog extends Vue {
       this.onScan(result);
       this.close();
     } catch (error) {
-      this.error = error instanceof Error ? error.message : String(error);
+      const wrappedError = error instanceof Error ? error : new Error(String(error));
+      this.error = wrappedError.message;
       if (this.onError) {
-        this.onError(error instanceof Error ? error : new Error(String(error)));
+        this.onError(wrappedError);
       }
-      logger.error("Error handling QR scan result:", error);
+      logger.error("Error handling QR scan result:", wrappedError);
     }
   }
 
diff --git a/src/services/QRScanner/CapacitorQRScanner.ts b/src/services/QRScanner/CapacitorQRScanner.ts
index 1fcb644b..4edbd1de 100644
--- a/src/services/QRScanner/CapacitorQRScanner.ts
+++ b/src/services/QRScanner/CapacitorQRScanner.ts
@@ -10,13 +10,16 @@ import { logger } from "@/utils/logger";
 export class CapacitorQRScanner implements QRScannerService {
   private scanListener: ScanListener | null = null;
   private isScanning = false;
+  private listenerHandles: Array<() => Promise<void>> = [];
 
   async checkPermissions(): Promise<boolean> {
     try {
       const { camera } = await BarcodeScanner.checkPermissions();
       return camera === "granted";
     } catch (error) {
-      logger.error("Error checking camera permissions:", error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error checking camera permissions:", wrappedError);
       return false;
     }
   }
@@ -32,7 +35,9 @@ export class CapacitorQRScanner implements QRScannerService {
       const { camera } = await BarcodeScanner.requestPermissions();
       return camera === "granted";
     } catch (error) {
-      logger.error("Error requesting camera permissions:", error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error requesting camera permissions:", wrappedError);
       return false;
     }
   }
@@ -42,7 +47,9 @@ export class CapacitorQRScanner implements QRScannerService {
       const { supported } = await BarcodeScanner.isSupported();
       return supported;
     } catch (error) {
-      logger.error("Error checking scanner support:", error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error checking scanner support:", wrappedError);
       return false;
     }
   }
@@ -79,17 +86,23 @@ export class CapacitorQRScanner implements QRScannerService {
       };
 
       logger.log("Scanner options:", scanOptions);
-      const result = await BarcodeScanner.scan(scanOptions);
-      logger.log("Scan result:", result);
-
-      if (result.barcodes.length > 0) {
-        this.scanListener?.onScan(result.barcodes[0].rawValue);
-      }
+      
+      // Add listener for barcode scans
+      const handle = await BarcodeScanner.addListener('barcodeScanned', (result) => {
+        if (this.scanListener) {
+          this.scanListener.onScan(result.barcode.rawValue);
+        }
+      });
+      this.listenerHandles.push(handle.remove);
+      
+      // Start continuous scanning
+      await BarcodeScanner.startScan(scanOptions);
     } catch (error) {
-      logger.error("Error during QR scan:", error);
-      this.scanListener?.onError?.(error as Error);
-    } finally {
-      this.isScanning = false;
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error during QR scan:", wrappedError);
+      this.scanListener?.onError?.(wrappedError);
+      throw wrappedError;
     }
   }
 
@@ -100,10 +113,14 @@ export class CapacitorQRScanner implements QRScannerService {
 
     try {
       await BarcodeScanner.stopScan();
-      this.isScanning = false;
     } catch (error) {
-      logger.error("Error stopping QR scan:", error);
-      this.scanListener?.onError?.(error as Error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error stopping QR scan:", wrappedError);
+      this.scanListener?.onError?.(wrappedError);
+      throw wrappedError;
+    } finally {
+      this.isScanning = false;
     }
   }
 
@@ -112,7 +129,19 @@ export class CapacitorQRScanner implements QRScannerService {
   }
 
   async cleanup(): Promise<void> {
-    await this.stopScan();
-    this.scanListener = null;
+    try {
+      await this.stopScan();
+      for (const handle of this.listenerHandles) {
+        await handle();
+      }
+    } catch (error) {
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error during cleanup:", wrappedError);
+      throw wrappedError;
+    } finally {
+      this.listenerHandles = [];
+      this.scanListener = null;
+    }
   }
 }
diff --git a/src/services/QRScanner/QRScannerFactory.ts b/src/services/QRScanner/QRScannerFactory.ts
index fc8e9b49..216eda91 100644
--- a/src/services/QRScanner/QRScannerFactory.ts
+++ b/src/services/QRScanner/QRScannerFactory.ts
@@ -11,8 +11,15 @@ export class QRScannerFactory {
   private static instance: QRScannerService | null = null;
 
   private static isNativePlatform(): boolean {
+    // Debug logging for build flags
+    logger.log("Build flags:", {
+      IS_MOBILE: typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : 'undefined',
+      USE_QR_READER: typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : 'undefined',
+      VITE_PLATFORM: process.env.VITE_PLATFORM,
+    });
+
     const capacitorNative = Capacitor.isNativePlatform();
-    const isMobile = __IS_MOBILE__;
+    const isMobile = typeof __IS_MOBILE__ !== 'undefined' ? __IS_MOBILE__ : capacitorNative;
     const platform = Capacitor.getPlatform();
 
     logger.log("Platform detection:", {
@@ -22,12 +29,16 @@ export class QRScannerFactory {
       userAgent: navigator.userAgent,
     });
 
-    // Force native scanner on Android/iOS
+    // Always use native scanner on Android/iOS
     if (platform === "android" || platform === "ios") {
+      logger.log("Using native scanner due to platform:", platform);
       return true;
     }
 
-    return capacitorNative || isMobile;
+    // For other platforms, use native if available
+    const useNative = capacitorNative || isMobile;
+    logger.log("Platform decision:", { useNative, reason: useNative ? "capacitorNative/isMobile" : "web" });
+    return useNative;
   }
 
   /**
@@ -40,19 +51,24 @@ export class QRScannerFactory {
         `Creating QR scanner for platform: ${isNative ? "native" : "web"}`,
       );
 
-      if (isNative) {
-        logger.log("Using native MLKit scanner");
-        this.instance = new CapacitorQRScanner();
-      } else if (__USE_QR_READER__) {
-        logger.log("Using web QR scanner");
-        this.instance = new WebDialogQRScanner();
-      } else {
-        throw new Error(
-          "No QR scanner implementation available for this platform",
-        );
+      try {
+        if (isNative) {
+          logger.log("Using native MLKit scanner");
+          this.instance = new CapacitorQRScanner();
+        } else if (typeof __USE_QR_READER__ !== 'undefined' ? __USE_QR_READER__ : !isNative) {
+          logger.log("Using web QR scanner");
+          this.instance = new WebDialogQRScanner();
+        } else {
+          throw new Error(
+            "No QR scanner implementation available for this platform",
+          );
+        }
+      } catch (error) {
+        logger.error("Error creating QR scanner:", error);
+        throw error;
       }
     }
-    return this.instance!; // We know it's not null here
+    return this.instance!;
   }
 
   /**
@@ -60,8 +76,13 @@ export class QRScannerFactory {
    */
   static async cleanup(): Promise<void> {
     if (this.instance) {
-      await this.instance.cleanup();
-      this.instance = null;
+      try {
+        await this.instance.cleanup();
+      } catch (error) {
+        logger.error("Error cleaning up QR scanner:", error);
+      } finally {
+        this.instance = null;
+      }
     }
   }
 }
diff --git a/src/services/QRScanner/WebDialogQRScanner.ts b/src/services/QRScanner/WebDialogQRScanner.ts
index 0ed57f86..bf592fab 100644
--- a/src/services/QRScanner/WebDialogQRScanner.ts
+++ b/src/services/QRScanner/WebDialogQRScanner.ts
@@ -8,6 +8,7 @@ export class WebDialogQRScanner implements QRScannerService {
   private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
   private scanListener: ScanListener | null = null;
   private isScanning = false;
+  private container: HTMLElement | null = null;
 
   constructor(private options?: QRScannerOptions) {}
 
@@ -18,7 +19,9 @@ export class WebDialogQRScanner implements QRScannerService {
       });
       return permissions.state === "granted";
     } catch (error) {
-      logger.error("Error checking camera permissions:", error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error checking camera permissions:", wrappedError);
       return false;
     }
   }
@@ -29,7 +32,9 @@ export class WebDialogQRScanner implements QRScannerService {
       stream.getTracks().forEach((track) => track.stop());
       return true;
     } catch (error) {
-      logger.error("Error requesting camera permissions:", error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error requesting camera permissions:", wrappedError);
       return false;
     }
   }
@@ -47,8 +52,8 @@ export class WebDialogQRScanner implements QRScannerService {
       this.isScanning = true;
 
       // Create and mount dialog component
-      const container = document.createElement("div");
-      document.body.appendChild(container);
+      this.container = document.createElement("div");
+      document.body.appendChild(this.container);
 
       this.dialogInstance = createApp(QRScannerDialog, {
         onScan: (result: string) => {
@@ -64,16 +69,18 @@ export class WebDialogQRScanner implements QRScannerService {
         options: this.options,
       });
 
-      this.dialogComponent = this.dialogInstance.mount(container).$refs
+      this.dialogComponent = this.dialogInstance.mount(this.container).$refs
         .dialog as InstanceType<typeof QRScannerDialog>;
     } catch (error) {
       this.isScanning = false;
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
       if (this.scanListener?.onError) {
-        this.scanListener.onError(
-          error instanceof Error ? error : new Error(String(error)),
-        );
+        this.scanListener.onError(wrappedError);
       }
-      logger.error("Error starting scan:", error);
+      logger.error("Error starting scan:", wrappedError);
+      this.cleanupContainer();
+      throw wrappedError;
     }
   }
 
@@ -89,9 +96,14 @@ export class WebDialogQRScanner implements QRScannerService {
       if (this.dialogInstance) {
         this.dialogInstance.unmount();
       }
-      this.isScanning = false;
     } catch (error) {
-      logger.error("Error stopping scan:", error);
+      const wrappedError =
+        error instanceof Error ? error : new Error(String(error));
+      logger.error("Error stopping scan:", wrappedError);
+      throw wrappedError;
+    } finally {
+      this.isScanning = false;
+      this.cleanupContainer();
     }
   }
 
@@ -99,10 +111,26 @@ export class WebDialogQRScanner implements QRScannerService {
     this.scanListener = listener;
   }
 
+  private cleanupContainer(): void {
+    if (this.container && this.container.parentNode) {
+      this.container.parentNode.removeChild(this.container);
+    }
+    this.container = null;
+  }
+
   async cleanup(): Promise<void> {
-    await this.stopScan();
-    this.dialogComponent = null;
-    this.dialogInstance = null;
-    this.scanListener = null;
+    try {
+      await this.stopScan();
+    } 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();
+    }
   }
 }
diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue
index f315e658..01720407 100644
--- a/src/views/ContactQRScanShowView.vue
+++ b/src/views/ContactQRScanShowView.vue
@@ -27,7 +27,7 @@
         <br />
         <span
           class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
-          @click="() => $refs.userNameDialog.open((name) => (givenName = name))"
+          @click="openUserNameDialog"
         >
           click here to set it for them.
         </span>
@@ -77,8 +77,19 @@
 
     <div class="text-center">
       <h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
-      <qrcode-stream @detect="onScanDetect" @error="onScanError" />
-      <span>
+      <div v-if="isScanning" class="relative aspect-square">
+        <div class="absolute inset-0 border-2 border-blue-500 opacity-50 pointer-events-none"></div>
+      </div>
+      <div v-else>
+        <button 
+          class="bg-blue-500 text-white px-4 py-2 rounded-md mt-4"
+          @click="startScanning"
+        >
+          Start Scanning
+        </button>
+      </div>
+      <span v-if="error" class="text-red-500 block mt-2">{{ error }}</span>
+      <span v-else class="block mt-2">
         If you do not see a scanning camera window here, check your camera
         permissions.
       </span>
@@ -90,7 +101,6 @@
 import { AxiosError } from "axios";
 import QRCodeVue3 from "qr-code-generator-vue3";
 import { Component, Vue } from "vue-facing-decorator";
-import { QrcodeStream } from "vue-qrcode-reader";
 import { useClipboard } from "@vueuse/core";
 
 import QuickNav from "../components/QuickNav.vue";
@@ -110,9 +120,10 @@ import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
 import { retrieveAccountMetadata } from "../libs/util";
 import { Router } from "vue-router";
 import { logger } from "../utils/logger";
+import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
+
 @Component({
   components: {
-    QrcodeStream,
     QRCodeVue3,
     QuickNav,
     UserNameDialog,
@@ -128,6 +139,8 @@ export default class ContactQRScanShow extends Vue {
   hideRegisterPromptOnNewContact = false;
   isRegistered = false;
   qrValue = "";
+  isScanning = false;
+  error: string | null = null;
 
   ETHR_DID_PREFIX = ETHR_DID_PREFIX;
 
@@ -150,12 +163,55 @@ export default class ContactQRScanShow extends Vue {
         account,
         !!settings.isRegistered,
         name,
-        settings.profileImageUrl,
+        settings.profileImageUrl || "",
         false,
       );
     }
   }
 
+  async startScanning() {
+    try {
+      this.error = null;
+      this.isScanning = true;
+      
+      const scanner = QRScannerFactory.getInstance();
+      
+      // Check permissions first
+      if (!(await scanner.checkPermissions())) {
+        const granted = await scanner.requestPermissions();
+        if (!granted) {
+          this.error = "Camera permission denied";
+          this.isScanning = false;
+          return;
+        }
+      }
+      
+      // Add scan listener
+      scanner.addListener({
+        onScan: this.onScanDetect,
+        onError: this.onScanError
+      });
+      
+      // Start scanning
+      await scanner.startScan();
+    } catch (error) {
+      this.error = error instanceof Error ? error.message : String(error);
+      this.isScanning = false;
+      logger.error("Error starting scan:", error);
+    }
+  }
+
+  async stopScanning() {
+    try {
+      const scanner = QRScannerFactory.getInstance();
+      await scanner.stopScan();
+    } catch (error) {
+      logger.error("Error stopping scan:", error);
+    } finally {
+      this.isScanning = false;
+    }
+  }
+
   danger(message: string, title: string = "Error", timeout = 5000) {
     this.$notify(
       {
@@ -169,49 +225,37 @@ export default class ContactQRScanShow extends Vue {
   }
 
   /**
-   *
-   * @param content is the result of a QR scan, an array with one item with a rawValue property
+   * Handle QR code scan result
    */
-  // Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  async onScanDetect(content: any) {
-    const url = content[0]?.rawValue;
-    if (url) {
+  async onScanDetect(result: string) {
+    try {
       let newContact: Contact;
-      try {
-        const jwt = getContactJwtFromJwtUrl(url);
-        if (!jwt) {
-          this.$notify(
-            {
-              group: "alert",
-              type: "danger",
-              title: "No Contact Info",
-              text: "The contact info could not be parsed.",
-            },
-            3000,
-          );
-          return;
-        }
-        const { payload } = decodeEndorserJwt(jwt);
-        newContact = {
-          did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49
-          name: payload.own.name,
-          nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
-          profileImageUrl: payload.own.profileImageUrl,
-          publicKeyBase64: payload.own.publicEncKey,
-          registered: payload.own.registered,
-        };
-        if (!newContact.did) {
-          this.danger("There is no DID.", "Incomplete Contact");
-          return;
-        }
-        if (!isDid(newContact.did)) {
-          this.danger("The DID must begin with 'did:'", "Invalid DID");
-          return;
-        }
-      } catch (e) {
-        logger.error("Error parsing QR info:", e);
-        this.danger("Could not parse the QR info.", "Read Error");
+      const jwt = getContactJwtFromJwtUrl(result);
+      if (!jwt) {
+        this.$notify(
+          {
+            group: "alert",
+            type: "danger",
+            title: "No Contact Info",
+            text: "The contact info could not be parsed.",
+          },
+          3000,
+        );
+        return;
+      }
+      const { payload } = decodeEndorserJwt(jwt);
+      newContact = {
+        did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49
+        name: payload.own.name,
+        nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
+        profileImageUrl: payload.own.profileImageUrl,
+      };
+      if (!newContact.did) {
+        this.danger("There is no DID.", "Incomplete Contact");
+        return;
+      }
+      if (!isDid(newContact.did)) {
+        this.danger("The DID must begin with 'did:'", "Invalid DID");
         return;
       }
 
@@ -247,7 +291,7 @@ export default class ContactQRScanShow extends Vue {
                   type: "confirm",
                   title: "Register",
                   text: "Do you want to register them?",
-                  onCancel: async (stopAsking: boolean) => {
+                  onCancel: async (stopAsking?: boolean) => {
                     if (stopAsking) {
                       await db.settings.update(MASTER_SETTINGS_KEY, {
                         hideRegisterPromptOnNewContact: stopAsking,
@@ -255,7 +299,7 @@ export default class ContactQRScanShow extends Vue {
                       this.hideRegisterPromptOnNewContact = stopAsking;
                     }
                   },
-                  onNo: async (stopAsking: boolean) => {
+                  onNo: async (stopAsking?: boolean) => {
                     if (stopAsking) {
                       await db.settings.update(MASTER_SETTINGS_KEY, {
                         hideRegisterPromptOnNewContact: stopAsking,
@@ -285,16 +329,12 @@ export default class ContactQRScanShow extends Vue {
           5000,
         );
       }
-    } else {
-      this.$notify(
-        {
-          group: "alert",
-          type: "danger",
-          title: "Invalid Contact QR Code",
-          text: "No QR code detected with contact information.",
-        },
-        5000,
-      );
+
+      // Stop scanning after successful scan
+      await this.stopScanning();
+    } catch (error) {
+      this.error = error instanceof Error ? error.message : String(error);
+      logger.error("Error processing scan result:", error);
     }
   }
 
@@ -364,8 +404,8 @@ export default class ContactQRScanShow extends Vue {
       let userMessage = "There was an error.";
       const serverError = error as AxiosError;
       if (serverError) {
-        if (serverError.response?.data?.error?.message) {
-          userMessage = serverError.response.data.error.message;
+        if (serverError.response?.data && typeof serverError.response.data === 'object' && 'message' in serverError.response.data) {
+          userMessage = (serverError.response.data as {message: string}).message;
         } else if (serverError.message) {
           userMessage = serverError.message; // Info for the user
         } else {
@@ -387,18 +427,9 @@ export default class ContactQRScanShow extends Vue {
     }
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  onScanError(error: any) {
-    logger.error("Scan was invalid:", error);
-    this.$notify(
-      {
-        group: "alert",
-        type: "danger",
-        title: "Invalid Scan",
-        text: "The scan was invalid.",
-      },
-      5000,
-    );
+  onScanError(error: Error) {
+    this.error = error.message;
+    logger.error("Scan error:", error);
   }
 
   onCopyUrlToClipboard() {
@@ -435,5 +466,22 @@ export default class ContactQRScanShow extends Vue {
         );
       });
   }
+
+  openUserNameDialog() {
+    (this.$refs.userNameDialog as any).open((name: string) => {
+      this.givenName = name;
+    });
+  }
+
+  beforeDestroy() {
+    // Clean up scanner when component is destroyed
+    QRScannerFactory.cleanup();
+  }
 }
 </script>
+
+<style scoped>
+.aspect-square {
+  aspect-ratio: 1 / 1;
+}
+</style>
diff --git a/vite.config.common.mts b/vite.config.common.mts
index 1e288f5f..483dc4d4 100644
--- a/vite.config.common.mts
+++ b/vite.config.common.mts
@@ -44,6 +44,8 @@ export async function createBuildConfig(mode: string) {
       'process.env.VITE_PLATFORM': JSON.stringify(mode),
       'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
       __dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
+      __IS_MOBILE__: JSON.stringify(isCapacitor),
+      __USE_QR_READER__: JSON.stringify(!isCapacitor),
     },
     resolve: {
       alias: {