forked from jsnbuchanan/crowd-funder-for-time-pwa
- Add camera state tracking and listener management - Implement addCameraStateListener and removeCameraStateListener methods - Add state transitions during scanning operations - Improve error handling with state updates - Add proper type imports for CameraState and CameraStateListener This change ensures CapacitorQRScanner fully implements the QRScannerService interface and provides proper camera state feedback to consumers. Camera state is now tracked through the entire lifecycle of scanning operations, with appropriate state transitions for initialization, active scanning, errors, and cleanup.
241 lines
7.3 KiB
TypeScript
241 lines
7.3 KiB
TypeScript
import {
|
|
BarcodeScanner,
|
|
BarcodeFormat,
|
|
StartScanOptions,
|
|
LensFacing,
|
|
} from "@capacitor-mlkit/barcode-scanning";
|
|
import { QRScannerService, ScanListener, QRScannerOptions, CameraStateListener, CameraState } 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;
|
|
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
|
private currentState: CameraState = "off";
|
|
private currentStateMessage?: string;
|
|
|
|
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 {
|
|
this.updateCameraState("initializing", "Starting camera...");
|
|
|
|
// Ensure we have permissions before starting
|
|
if (!(await this.checkPermissions())) {
|
|
this.updateCameraState("permission_denied", "Camera permission denied");
|
|
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())) {
|
|
this.updateCameraState("error", "QR scanning not supported on this device");
|
|
throw new Error("QR scanning not supported on this device");
|
|
}
|
|
|
|
logger.info("Starting MLKit scanner");
|
|
this.isScanning = true;
|
|
this.updateCameraState("active", "Camera is active");
|
|
|
|
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;
|
|
this.updateCameraState("error", wrappedError.message);
|
|
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");
|
|
this.updateCameraState("off", "Camera stopped");
|
|
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.updateCameraState("error", wrappedError.message);
|
|
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);
|
|
}
|
|
|
|
addCameraStateListener(listener: CameraStateListener): void {
|
|
this.cameraStateListeners.add(listener);
|
|
// Immediately notify the new listener of current state
|
|
listener.onStateChange(this.currentState, this.currentStateMessage);
|
|
}
|
|
|
|
removeCameraStateListener(listener: CameraStateListener): void {
|
|
this.cameraStateListeners.delete(listener);
|
|
}
|
|
|
|
private updateCameraState(state: CameraState, message?: string): void {
|
|
this.currentState = state;
|
|
this.currentStateMessage = message;
|
|
// Notify all listeners of state change
|
|
for (const listener of this.cameraStateListeners) {
|
|
listener.onStateChange(state, message);
|
|
}
|
|
}
|
|
}
|