You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
6.0 KiB
210 lines
6.0 KiB
import {
|
|
BarcodeScanner,
|
|
BarcodeFormat,
|
|
StartScanOptions,
|
|
LensFacing,
|
|
} from "@capacitor-mlkit/barcode-scanning";
|
|
import { QRScannerService, ScanListener, QRScannerOptions } 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>> = [];
|
|
private cleanupPromise: Promise<void> | null = null;
|
|
|
|
async checkPermissions(): Promise<boolean> {
|
|
try {
|
|
logger.debug("Checking camera permissions");
|
|
const { camera } = await BarcodeScanner.checkPermissions();
|
|
return camera === "granted";
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error checking camera permissions:", {
|
|
error: wrappedError.message,
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async requestPermissions(): Promise<boolean> {
|
|
try {
|
|
// First check if we already have permissions
|
|
if (await this.checkPermissions()) {
|
|
logger.debug("Camera permissions already granted");
|
|
return true;
|
|
}
|
|
|
|
logger.debug("Requesting camera permissions");
|
|
const { camera } = await BarcodeScanner.requestPermissions();
|
|
const granted = camera === "granted";
|
|
logger.debug(`Camera permissions ${granted ? "granted" : "denied"}`);
|
|
return granted;
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error requesting camera permissions:", {
|
|
error: wrappedError.message,
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async isSupported(): Promise<boolean> {
|
|
try {
|
|
logger.debug("Checking scanner support");
|
|
const { supported } = await BarcodeScanner.isSupported();
|
|
logger.debug(`Scanner support: ${supported}`);
|
|
return supported;
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error checking scanner support:", {
|
|
error: wrappedError.message,
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async startScan(options?: QRScannerOptions): Promise<void> {
|
|
if (this.isScanning) {
|
|
logger.debug("Scanner already running");
|
|
return;
|
|
}
|
|
|
|
if (this.cleanupPromise) {
|
|
logger.debug("Waiting for previous cleanup to complete");
|
|
await this.cleanupPromise;
|
|
}
|
|
|
|
try {
|
|
// Ensure we have permissions before starting
|
|
if (!(await this.checkPermissions())) {
|
|
logger.debug("Requesting camera permissions");
|
|
const granted = await this.requestPermissions();
|
|
if (!granted) {
|
|
throw new Error("Camera permission denied");
|
|
}
|
|
}
|
|
|
|
// Check if scanning is supported
|
|
if (!(await this.isSupported())) {
|
|
throw new Error("QR scanning not supported on this device");
|
|
}
|
|
|
|
logger.info("Starting MLKit scanner");
|
|
this.isScanning = true;
|
|
|
|
const scanOptions: StartScanOptions = {
|
|
formats: [BarcodeFormat.QrCode],
|
|
lensFacing:
|
|
options?.camera === "front" ? LensFacing.Front : LensFacing.Back,
|
|
};
|
|
|
|
logger.debug("Scanner options:", scanOptions);
|
|
|
|
// Add listener for barcode scans
|
|
const handle = await BarcodeScanner.addListener(
|
|
"barcodeScanned",
|
|
(result) => {
|
|
if (this.scanListener && result.barcode?.rawValue) {
|
|
this.scanListener.onScan(result.barcode.rawValue);
|
|
}
|
|
},
|
|
);
|
|
this.listenerHandles.push(handle.remove);
|
|
|
|
// Start continuous scanning
|
|
await BarcodeScanner.startScan(scanOptions);
|
|
logger.info("MLKit scanner started successfully");
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error during QR scan:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
});
|
|
this.isScanning = false;
|
|
await this.cleanup();
|
|
this.scanListener?.onError?.(wrappedError);
|
|
throw wrappedError;
|
|
}
|
|
}
|
|
|
|
async stopScan(): Promise<void> {
|
|
if (!this.isScanning) {
|
|
logger.debug("Scanner not running");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.debug("Stopping QR scanner");
|
|
await BarcodeScanner.stopScan();
|
|
logger.info("QR scanner stopped successfully");
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error stopping QR scan:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
});
|
|
this.scanListener?.onError?.(wrappedError);
|
|
throw wrappedError;
|
|
} finally {
|
|
this.isScanning = false;
|
|
}
|
|
}
|
|
|
|
addListener(listener: ScanListener): void {
|
|
this.scanListener = listener;
|
|
}
|
|
|
|
async cleanup(): Promise<void> {
|
|
// Prevent multiple simultaneous cleanup attempts
|
|
if (this.cleanupPromise) {
|
|
return this.cleanupPromise;
|
|
}
|
|
|
|
this.cleanupPromise = (async () => {
|
|
try {
|
|
logger.debug("Starting QR scanner cleanup");
|
|
|
|
// Stop scanning if active
|
|
if (this.isScanning) {
|
|
await this.stopScan();
|
|
}
|
|
|
|
// Remove all listeners
|
|
for (const handle of this.listenerHandles) {
|
|
try {
|
|
await handle();
|
|
} catch (error) {
|
|
logger.warn("Error removing listener:", error);
|
|
}
|
|
}
|
|
|
|
logger.info("QR scanner cleanup completed");
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error during cleanup:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
});
|
|
throw wrappedError;
|
|
} finally {
|
|
this.listenerHandles = [];
|
|
this.scanListener = null;
|
|
this.cleanupPromise = null;
|
|
}
|
|
})();
|
|
|
|
return this.cleanupPromise;
|
|
}
|
|
|
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
|
// No-op for native scanner
|
|
callback(null);
|
|
}
|
|
}
|
|
|