chore: updates for qr code reader rules, linting, and cleanup
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user