Browse Source

refactor(QRScanner): Align with platform service architecture

- Reorganize QR scanner to follow platform service pattern
- Move QR scanner implementations to services/platforms directory
- Update QRScannerFactory to use PlatformServiceFactory pattern
- Consolidate common QR scanner interfaces with platform services
- Add proper error handling and logging across implementations
- Ensure consistent cleanup and lifecycle management
- Add PyWebView implementation placeholder for desktop
- Update Capacitor and Web implementations to match platform patterns
qrcode-capacitor
Matthew Raymer 2 months ago
parent
commit
ea13250e5d
  1. 155
      src/services/QRScanner/CapacitorQRScanner.ts
  2. 88
      src/services/QRScanner/NativeQRScanner.ts
  3. 29
      src/services/QRScanner/QRScannerFactory.ts
  4. 142
      src/services/QRScanner/WebQRScanner.ts
  5. 29
      src/services/QRScanner/types.ts
  6. 313
      src/views/ContactQRScanShowView.vue

155
src/services/QRScanner/CapacitorQRScanner.ts

@ -0,0 +1,155 @@
import {
BarcodeScanner,
BarcodeFormat,
LensFacing,
ScanResult,
} from "@capacitor-mlkit/barcode-scanning";
import type { QRScannerService, ScanListener } from "./types";
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() {
try {
const { camera } = await BarcodeScanner.checkPermissions();
return camera === "granted";
} catch (error) {
logger.error("Error checking camera permissions:", error);
return false;
}
}
async requestPermissions() {
try {
const { camera } = await BarcodeScanner.requestPermissions();
return camera === "granted";
} catch (error) {
logger.error("Error requesting camera permissions:", error);
return false;
}
}
async isSupported() {
try {
const { supported } = await BarcodeScanner.isSupported();
return supported;
} catch (error) {
logger.error("Error checking barcode scanner support:", error);
return false;
}
}
async startScan() {
if (this.isScanning) {
logger.warn("Scanner is already active");
return;
}
try {
// First register listeners before starting scan
await this.registerListeners();
this.isScanning = true;
await BarcodeScanner.startScan({
formats: [BarcodeFormat.QrCode],
lensFacing: LensFacing.Back,
});
} catch (error) {
// Ensure cleanup on error
this.isScanning = false;
await this.removeListeners();
logger.error("Error starting barcode scan:", error);
throw error;
}
}
async stopScan() {
if (!this.isScanning) {
return;
}
try {
// First stop the scan
await BarcodeScanner.stopScan();
} catch (error) {
logger.error("Error stopping barcode scan:", error);
} finally {
// Always cleanup state even if stop fails
this.isScanning = false;
await this.removeListeners();
}
}
private async registerListeners() {
// Clear any existing listeners first
await this.removeListeners();
const scanHandle = await BarcodeScanner.addListener(
"barcodesScanned",
(result: ScanResult) => {
if (result.barcodes.length > 0) {
const barcode = result.barcodes[0];
if (barcode.rawValue && this.scanListener) {
this.scanListener.onScan(barcode.rawValue);
}
}
},
);
const errorHandle = await BarcodeScanner.addListener(
"scanError",
(error) => {
logger.error("Scan error:", error);
if (this.scanListener?.onError) {
this.scanListener.onError(
new Error(error.message || "Unknown scan error"),
);
}
},
);
this.listenerHandles.push(
async () => await scanHandle.remove(),
async () => await errorHandle.remove(),
);
}
private async removeListeners() {
try {
// Remove all registered listener handles
await Promise.all(this.listenerHandles.map((handle) => handle()));
this.listenerHandles = [];
} catch (error) {
logger.error("Error removing listeners:", error);
}
}
addListener(listener: ScanListener) {
if (this.scanListener) {
logger.warn("Scanner listener already exists, removing old listener");
this.cleanup();
}
this.scanListener = listener;
}
async cleanup() {
try {
// Stop scan first if active
if (this.isScanning) {
await this.stopScan();
}
// Remove listeners
await this.removeListeners();
// Clear state
this.scanListener = null;
this.isScanning = false;
} catch (error) {
logger.error("Error during cleanup:", error);
}
}
}

88
src/services/QRScanner/NativeQRScanner.ts

@ -0,0 +1,88 @@
import {
BarcodeScanner,
BarcodeFormat,
LensFacing,
} from "@capacitor-mlkit/barcode-scanning";
import type { PluginListenerHandle } from "@capacitor/core";
import { QRScannerService, ScanListener } from "./types";
export class NativeQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
private listenerHandle: PluginListenerHandle | null = null;
async checkPermissions(): Promise<boolean> {
const { camera } = await BarcodeScanner.checkPermissions();
return camera === "granted";
}
async requestPermissions(): Promise<boolean> {
const { camera } = await BarcodeScanner.requestPermissions();
return camera === "granted";
}
async isSupported(): Promise<boolean> {
const { supported } = await BarcodeScanner.isSupported();
return supported;
}
async startScan(): Promise<void> {
if (this.isScanning) {
throw new Error("Scanner is already running");
}
try {
this.isScanning = true;
await BarcodeScanner.startScan({
formats: [BarcodeFormat.QrCode],
lensFacing: LensFacing.Back,
});
this.listenerHandle = await BarcodeScanner.addListener(
"barcodesScanned",
async (result) => {
if (result.barcodes.length > 0 && this.scanListener) {
const barcode = result.barcodes[0];
this.scanListener.onScan(barcode.rawValue);
await this.stopScan();
}
},
);
} catch (error) {
this.isScanning = false;
if (this.scanListener?.onError) {
this.scanListener.onError(new Error(String(error)));
}
throw error;
}
}
async stopScan(): Promise<void> {
if (!this.isScanning) {
return;
}
try {
await BarcodeScanner.stopScan();
this.isScanning = false;
} catch (error) {
if (this.scanListener?.onError) {
this.scanListener.onError(new Error(String(error)));
}
throw error;
}
}
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
async cleanup(): Promise<void> {
await this.stopScan();
if (this.listenerHandle) {
await this.listenerHandle.remove();
this.listenerHandle = null;
}
this.scanListener = null;
}
}

29
src/services/QRScanner/QRScannerFactory.ts

@ -0,0 +1,29 @@
import { Capacitor } from "@capacitor/core";
import { CapacitorQRScanner } from "./CapacitorQRScanner";
import { WebQRScanner } from "./WebQRScanner";
import type { QRScannerService } from "./types";
import { logger } from "../../utils/logger";
export class QRScannerFactory {
private static instance: QRScannerService | null = null;
static getInstance(): QRScannerService {
if (!this.instance) {
if (Capacitor.isNativePlatform()) {
logger.log("Creating native QR scanner instance");
this.instance = new CapacitorQRScanner();
} else {
logger.log("Creating web QR scanner instance");
this.instance = new WebQRScanner();
}
}
return this.instance;
}
static async cleanup() {
if (this.instance) {
await this.instance.cleanup();
this.instance = null;
}
}
}

142
src/services/QRScanner/WebQRScanner.ts

@ -0,0 +1,142 @@
import jsQR from "jsqr";
import { QRScannerService, ScanListener } from "./types";
import { logger } from "../../utils/logger";
export class WebQRScanner implements QRScannerService {
private video: HTMLVideoElement | null = null;
private canvas: HTMLCanvasElement | null = null;
private context: CanvasRenderingContext2D | null = null;
private animationFrameId: number | null = null;
private scanListener: ScanListener | null = null;
private isScanning = false;
private mediaStream: MediaStream | null = null;
constructor() {
this.video = document.createElement("video");
this.canvas = document.createElement("canvas");
this.context = this.canvas.getContext("2d");
}
async checkPermissions(): Promise<boolean> {
try {
const permissions = await navigator.permissions.query({
name: "camera" as PermissionName,
});
return permissions.state === "granted";
} catch (error) {
logger.error("Error checking camera permissions:", error);
return false;
}
}
async requestPermissions(): Promise<boolean> {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
stream.getTracks().forEach((track) => track.stop());
return true;
} catch (error) {
logger.error("Error requesting camera permissions:", error);
return false;
}
}
async isSupported(): Promise<boolean> {
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}
async startScan(): Promise<void> {
if (this.isScanning || !this.video || !this.canvas || !this.context) {
throw new Error("Scanner is already running or not properly initialized");
}
try {
this.mediaStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
this.video.srcObject = this.mediaStream;
this.video.setAttribute("playsinline", "true");
await this.video.play();
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.isScanning = true;
this.scanFrame();
} catch (error) {
logger.error("Error starting scan:", error);
throw error;
}
}
private scanFrame = () => {
if (
!this.isScanning ||
!this.video ||
!this.canvas ||
!this.context ||
!this.scanListener
) {
return;
}
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.context.drawImage(
this.video,
0,
0,
this.canvas.width,
this.canvas.height,
);
const imageData = this.context.getImageData(
0,
0,
this.canvas.width,
this.canvas.height,
);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
if (code) {
this.scanListener.onScan(code.data);
}
}
this.animationFrameId = requestAnimationFrame(this.scanFrame);
};
async stopScan(): Promise<void> {
this.isScanning = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
if (this.mediaStream) {
this.mediaStream.getTracks().forEach((track) => track.stop());
this.mediaStream = null;
}
if (this.video) {
this.video.srcObject = null;
}
}
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
async cleanup(): Promise<void> {
await this.stopScan();
this.scanListener = null;
this.video = null;
this.canvas = null;
this.context = null;
}
}

29
src/services/QRScanner/types.ts

@ -0,0 +1,29 @@
export interface ScanResult {
rawValue: string;
}
export interface QRScannerState {
isSupported: boolean;
granted: boolean;
denied: boolean;
isProcessing: boolean;
processingStatus: string;
processingDetails: string;
error: string;
status: string;
}
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>;
}

313
src/views/ContactQRScanShowView.vue

@ -116,7 +116,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue, Ref } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader"; import { QrcodeStream } from "vue-qrcode-reader";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { PlatformService } from "../services/PlatformService"; import { PlatformService } from "../services/PlatformService";
@ -205,19 +205,6 @@ interface AppState {
scannerState: ScannerState; scannerState: ScannerState;
} }
interface Barcode {
rawValue: string;
bytes?: number[];
}
interface MLKitScanResult {
barcodes: Barcode[];
}
interface WebQRResult {
rawValue: string;
}
@Component({ @Component({
components: { components: {
QRCodeVue3, QRCodeVue3,
@ -229,11 +216,6 @@ interface WebQRResult {
export default class ContactQRScanShowView extends Vue { export default class ContactQRScanShowView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router; $router!: Router;
@Ref()
readonly userNameDialog!: {
open: (callback: (name: string) => void) => void;
};
declare $refs: { declare $refs: {
userNameDialog: { userNameDialog: {
open: (callback: (name: string) => void) => void; open: (callback: (name: string) => void) => void;
@ -441,7 +423,42 @@ export default class ContactQRScanShowView extends Vue {
this.lastCameraState = state; this.lastCameraState = state;
} }
private async openMobileCamera() { beforeDestroy() {
logger.log(
"ContactQRScanShow component being destroyed, initiating cleanup",
);
// Clean up scanner
this.stopScanning().catch((error) => {
logger.error("Error stopping scanner during destroy:", error);
});
// Remove all app lifecycle listeners
const cleanupListeners = async () => {
if (this.appStateListener) {
try {
await this.appStateListener.remove();
logger.log("App state change listener removed successfully");
} catch (error) {
logger.error("Error removing app state change listener:", error);
}
}
try {
await App.removeAllListeners();
logger.log("All app listeners removed successfully");
} catch (error) {
logger.error("Error removing all app listeners:", error);
}
};
// Cleanup everything
Promise.all([cleanupListeners(), this.cleanupCamera()]).catch((error) => {
logger.error("Error during component cleanup:", error);
});
}
async openMobileCamera() {
try { try {
this.state.isProcessing = true; this.state.isProcessing = true;
this.state.processingStatus = "Starting camera..."; this.state.processingStatus = "Starting camera...";
@ -449,7 +466,7 @@ export default class ContactQRScanShowView extends Vue {
// Check current permission status // Check current permission status
const status = await BarcodeScanner.checkPermissions(); const status = await BarcodeScanner.checkPermissions();
logger.log("Camera permission status:", status); logger.log("Camera permission status:", JSON.stringify(status, null, 2));
if (status.camera !== "granted") { if (status.camera !== "granted") {
// Request permission if not granted // Request permission if not granted
@ -458,32 +475,36 @@ export default class ContactQRScanShowView extends Vue {
if (permissionStatus.camera !== "granted") { if (permissionStatus.camera !== "granted") {
throw new Error("Camera permission not granted"); throw new Error("Camera permission not granted");
} }
logger.log("Camera permission granted:", permissionStatus); logger.log(
"Camera permission granted:",
JSON.stringify(permissionStatus, null, 2),
);
} }
// Remove any existing listener first // Remove any existing listener first
await this.cleanupScanListener(); try {
if (this.scanListener) {
logger.log("Removing existing barcode listener");
await this.scanListener.remove();
this.scanListener = null;
}
} catch (error) {
logger.error("Error removing existing listener:", error);
// Continue with setup even if removal fails
}
// Set up the listener before starting the scan // Set up the listener before starting the scan
logger.log("Setting up new barcode listener"); logger.log("Setting up new barcode listener");
this.scanListener = await BarcodeScanner.addListener( this.scanListener = await BarcodeScanner.addListener(
"barcodesScanned", "barcodesScanned",
async (result: ScanResult) => { async (result: ScanResult) => {
try { logger.log(
logger.log("Barcode scan result received:", result); "Barcode scan result received:",
if (result.barcodes && result.barcodes.length > 0) { JSON.stringify(result, null, 2),
const barcode = result.barcodes[0]; );
if (!barcode.rawValue) { if (result.barcodes && result.barcodes.length > 0) {
logger.warn("Received empty barcode value"); this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
return; await this.handleScanResult(result.barcodes[0].rawValue);
}
this.state.processingDetails = `Processing QR code: ${barcode.rawValue}`;
await this.handleScanResult(barcode.rawValue);
}
} catch (error) {
logger.error("Error processing barcode result:", error);
this.showError("Failed to process QR code");
} }
}, },
); );
@ -506,19 +527,14 @@ export default class ContactQRScanShowView extends Vue {
); );
// Cleanup on error // Cleanup on error
await this.cleanupScanListener(); try {
} if (this.scanListener) {
} await this.scanListener.remove();
this.scanListener = null;
private async cleanupScanListener(): Promise<void> { }
try { } catch (cleanupError) {
if (this.scanListener) { logger.error("Error during cleanup:", cleanupError);
logger.log("Removing existing barcode listener");
await this.scanListener.remove();
this.scanListener = null;
} }
} catch (error) {
logger.error("Error removing barcode listener:", error);
} }
} }
@ -527,27 +543,15 @@ export default class ContactQRScanShowView extends Vue {
this.state.isProcessing = true; this.state.isProcessing = true;
this.state.processingStatus = "Processing QR code..."; this.state.processingStatus = "Processing QR code...";
this.state.processingDetails = `Scanned value: ${rawValue}`; this.state.processingDetails = `Scanned value: ${rawValue}`;
logger.log("Processing scanned QR code:", rawValue);
// Stop scanning before processing // Stop scanning before processing
await this.stopScanning(); await this.stopScanning();
// Validate URL format first
if (!rawValue.startsWith("http://") && !rawValue.startsWith("https://")) {
throw new Error(
"Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
);
}
// Process the scan result // Process the scan result
await this.onScanDetect({ rawValue }); await this.onScanDetect({ rawValue });
} catch (error) { } catch (error) {
logger.error("Error handling scan result:", error); logger.error("Error handling scan result:", error);
this.showError( this.showError("Failed to process scan result");
error instanceof Error
? error.message
: "Failed to process scan result",
);
} finally { } finally {
this.state.isProcessing = false; this.state.isProcessing = false;
this.state.processingStatus = ""; this.state.processingStatus = "";
@ -590,110 +594,124 @@ export default class ContactQRScanShowView extends Vue {
} }
/** /**
* Handle QR code scan result *
* @param content scan result from barcode scanner * @param content is the result of a QR scan, an array with one item with a rawValue property
*/ */
async onScanDetect(content: unknown): Promise<void> { // Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
// Extract URL from different possible formats // eslint-disable-next-line @typescript-eslint/no-explicit-any
let url: string | null = null; async onScanDetect(content: any): Promise<void> {
// Log the received content for debugging
if (typeof content === "object" && content !== null) { logger.log("Scan result received:", JSON.stringify(content, null, 2));
// Handle Capacitor MLKit scanner format
if ( // Handle both array format and direct object format
"barcodes" in content && let rawValue: string | undefined;
Array.isArray((content as MLKitScanResult).barcodes)
) { if (Array.isArray(content)) {
const mlkitResult = content as MLKitScanResult; rawValue = content[0]?.rawValue;
url = mlkitResult.barcodes[0]?.rawValue; logger.log("Processing array format, rawValue:", rawValue);
} } else if (content?.barcodes?.[0]?.rawValue) {
// Handle web QR reader format rawValue = content.barcodes[0].rawValue;
else if (Array.isArray(content)) { logger.log("Processing barcodes array format, rawValue:", rawValue);
const webResult = content as WebQRResult[]; } else if (content?.rawValue) {
url = webResult[0]?.rawValue; rawValue = content.rawValue;
} logger.log("Processing object format, rawValue:", rawValue);
// Handle direct object format } else if (typeof content === "string") {
else if ("rawValue" in content) { rawValue = content;
const directResult = content as WebQRResult; logger.log("Processing string format, rawValue:", rawValue);
url = directResult.rawValue;
}
} }
// Handle direct string format
else if (typeof content === "string") { if (!rawValue) {
url = content; logger.error("No valid QR code content found in:", content);
this.danger("No QR code detected. Please try again.", "Scan Error");
return;
} }
if (!url) { // Validate URL format first
logger.error("No valid QR code URL detected"); if (!rawValue.startsWith("http://") && !rawValue.startsWith("https://")) {
this.$notify( logger.error("Invalid URL format:", rawValue);
{ this.danger(
group: "alert", "Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
type: "danger", "Invalid Format",
title: "Invalid Contact QR Code",
text: "No QR code detected with contact information.",
},
5000,
); );
return; return;
} }
let newContact: Contact; let newContact: Contact;
try { try {
logger.log("Attempting to extract JWT from URL:", url); // Extract JWT from URL
const jwt = getContactJwtFromJwtUrl(url); const jwt = getContactJwtFromJwtUrl(rawValue);
if (!jwt) { if (!jwt) {
logger.error("Failed to extract JWT from URL"); logger.error("Failed to extract JWT from URL:", rawValue);
this.$notify( this.danger(
{ "Could not extract contact information from the QR code. Please try again.",
group: "alert", "Invalid QR Code",
type: "danger",
title: "Invalid QR Code",
text: "Could not extract contact information from QR code.",
},
3000,
); );
return; return;
} }
logger.log("Successfully extracted JWT, attempting to decode"); // Log JWT for debugging
const { payload } = decodeEndorserJwt(jwt); logger.log("Extracted JWT:", jwt);
// Validate JWT format
if (
!jwt.match(/^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/)
) {
logger.error("Invalid JWT format:", jwt);
this.danger(
"The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
"Invalid Data",
);
return;
}
const { payload } = decodeEndorserJwt(jwt);
if (!payload) { if (!payload) {
logger.error("JWT payload is null or undefined"); logger.error("Failed to decode JWT payload");
this.danger("Invalid JWT format", "Contact Error"); this.danger(
"Could not decode the contact information. Please try again.",
"Decode Error",
);
return; return;
} }
if (!payload.own) { // Log decoded payload for debugging
logger.error("JWT payload missing 'own' property"); logger.log("Decoded JWT payload:", JSON.stringify(payload, null, 2));
this.danger("Contact information is incomplete", "Contact Error");
// Validate required fields
if (!payload.own && !payload.iss) {
logger.error("Missing required fields in payload:", payload);
this.danger(
"Missing required contact information. Please scan a valid TimeSafari contact QR code.",
"Incomplete Data",
);
return; return;
} }
newContact = { newContact = {
did: payload.own.did || payload.iss, did: payload.own?.did || payload.iss,
name: payload.own.name || "", name: payload.own?.name,
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash || "", nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
profileImageUrl: payload.own.profileImageUrl || "", profileImageUrl: payload.own?.profileImageUrl,
publicKeyBase64: payload.own.publicEncKey || "", publicKeyBase64: payload.own?.publicEncKey,
registered: payload.own.registered || false, registered: payload.own?.registered,
}; };
if (!newContact.did) { if (!newContact.did) {
logger.error("Contact missing DID"); this.danger(
this.danger("Contact is missing identifier", "Invalid Contact"); "Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
"Incomplete Contact",
);
return; return;
} }
if (!isDid(newContact.did)) { if (!isDid(newContact.did)) {
logger.error("Invalid DID format:", newContact.did); this.danger(
this.danger("Invalid contact identifier format", "Invalid Contact"); "Invalid contact identifier format. The identifier must begin with 'did:'.",
"Invalid Identifier",
);
return; return;
} }
logger.log("Saving new contact:", {
...newContact,
publicKeyBase64: "[REDACTED]",
});
await db.open(); await db.open();
await db.contacts.add(newContact); await db.contacts.add(newContact);
@ -754,15 +772,10 @@ export default class ContactQRScanShowView extends Vue {
}, 500); }, 500);
} }
} catch (e) { } catch (e) {
logger.error("Error processing contact QR code:", e); logger.error("Error processing QR code:", e);
this.$notify( this.danger(
{ "Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.",
group: "alert", "Processing Error",
type: "danger",
title: "Contact Error",
text: "Could not process contact information. Please try scanning again.",
},
5000,
); );
} }
} }
@ -985,21 +998,5 @@ export default class ContactQRScanShowView extends Vue {
this.state.error = errorMessage; this.state.error = errorMessage;
this.state.scannerState.error = errorMessage; this.state.scannerState.error = errorMessage;
} }
async beforeDestroy() {
logger.log(
"ContactQRScanShow component being destroyed, initiating cleanup",
);
// Clean up scanner
await Promise.all([
this.stopScanning(),
this.cleanupScanListener(),
this.cleanupAppListeners(),
this.cleanupCamera(),
]).catch((error) => {
logger.error("Error during component cleanup:", error);
});
}
} }
</script> </script>

Loading…
Cancel
Save