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
This commit is contained in:
155
src/services/QRScanner/CapacitorQRScanner.ts
Normal file
155
src/services/QRScanner/CapacitorQRScanner.ts
Normal file
@@ -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
Normal file
88
src/services/QRScanner/NativeQRScanner.ts
Normal file
@@ -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
Normal file
29
src/services/QRScanner/QRScannerFactory.ts
Normal file
@@ -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
Normal file
142
src/services/QRScanner/WebQRScanner.ts
Normal file
@@ -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
Normal file
29
src/services/QRScanner/types.ts
Normal file
@@ -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>;
|
||||
}
|
||||
@@ -116,7 +116,7 @@
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
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 { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { PlatformService } from "../services/PlatformService";
|
||||
@@ -205,19 +205,6 @@ interface AppState {
|
||||
scannerState: ScannerState;
|
||||
}
|
||||
|
||||
interface Barcode {
|
||||
rawValue: string;
|
||||
bytes?: number[];
|
||||
}
|
||||
|
||||
interface MLKitScanResult {
|
||||
barcodes: Barcode[];
|
||||
}
|
||||
|
||||
interface WebQRResult {
|
||||
rawValue: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QRCodeVue3,
|
||||
@@ -229,11 +216,6 @@ interface WebQRResult {
|
||||
export default class ContactQRScanShowView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
@Ref()
|
||||
readonly userNameDialog!: {
|
||||
open: (callback: (name: string) => void) => void;
|
||||
};
|
||||
|
||||
declare $refs: {
|
||||
userNameDialog: {
|
||||
open: (callback: (name: string) => void) => void;
|
||||
@@ -441,7 +423,42 @@ export default class ContactQRScanShowView extends Vue {
|
||||
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 {
|
||||
this.state.isProcessing = true;
|
||||
this.state.processingStatus = "Starting camera...";
|
||||
@@ -449,7 +466,7 @@ export default class ContactQRScanShowView extends Vue {
|
||||
|
||||
// Check current permission status
|
||||
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") {
|
||||
// Request permission if not granted
|
||||
@@ -458,32 +475,36 @@ export default class ContactQRScanShowView extends Vue {
|
||||
if (permissionStatus.camera !== "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
|
||||
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
|
||||
logger.log("Setting up new barcode listener");
|
||||
this.scanListener = await BarcodeScanner.addListener(
|
||||
"barcodesScanned",
|
||||
async (result: ScanResult) => {
|
||||
try {
|
||||
logger.log("Barcode scan result received:", result);
|
||||
if (result.barcodes && result.barcodes.length > 0) {
|
||||
const barcode = result.barcodes[0];
|
||||
if (!barcode.rawValue) {
|
||||
logger.warn("Received empty barcode value");
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
logger.log(
|
||||
"Barcode scan result received:",
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
if (result.barcodes && result.barcodes.length > 0) {
|
||||
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
|
||||
await this.handleScanResult(result.barcodes[0].rawValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -506,19 +527,14 @@ export default class ContactQRScanShowView extends Vue {
|
||||
);
|
||||
|
||||
// Cleanup on error
|
||||
await this.cleanupScanListener();
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupScanListener(): Promise<void> {
|
||||
try {
|
||||
if (this.scanListener) {
|
||||
logger.log("Removing existing barcode listener");
|
||||
await this.scanListener.remove();
|
||||
this.scanListener = null;
|
||||
try {
|
||||
if (this.scanListener) {
|
||||
await this.scanListener.remove();
|
||||
this.scanListener = null;
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
logger.error("Error during cleanup:", cleanupError);
|
||||
}
|
||||
} 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.processingStatus = "Processing QR code...";
|
||||
this.state.processingDetails = `Scanned value: ${rawValue}`;
|
||||
logger.log("Processing scanned QR code:", rawValue);
|
||||
|
||||
// Stop scanning before processing
|
||||
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
|
||||
await this.onScanDetect({ rawValue });
|
||||
} catch (error) {
|
||||
logger.error("Error handling scan result:", error);
|
||||
this.showError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to process scan result",
|
||||
);
|
||||
this.showError("Failed to process scan result");
|
||||
} finally {
|
||||
this.state.isProcessing = false;
|
||||
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> {
|
||||
// Extract URL from different possible formats
|
||||
let url: string | null = null;
|
||||
// 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): Promise<void> {
|
||||
// Log the received content for debugging
|
||||
logger.log("Scan result received:", JSON.stringify(content, null, 2));
|
||||
|
||||
if (typeof content === "object" && content !== null) {
|
||||
// Handle Capacitor MLKit scanner format
|
||||
if (
|
||||
"barcodes" in content &&
|
||||
Array.isArray((content as MLKitScanResult).barcodes)
|
||||
) {
|
||||
const mlkitResult = content as MLKitScanResult;
|
||||
url = mlkitResult.barcodes[0]?.rawValue;
|
||||
}
|
||||
// Handle web QR reader format
|
||||
else if (Array.isArray(content)) {
|
||||
const webResult = content as WebQRResult[];
|
||||
url = webResult[0]?.rawValue;
|
||||
}
|
||||
// Handle direct object format
|
||||
else if ("rawValue" in content) {
|
||||
const directResult = content as WebQRResult;
|
||||
url = directResult.rawValue;
|
||||
}
|
||||
}
|
||||
// Handle direct string format
|
||||
else if (typeof content === "string") {
|
||||
url = content;
|
||||
// Handle both array format and direct object format
|
||||
let rawValue: string | undefined;
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
rawValue = content[0]?.rawValue;
|
||||
logger.log("Processing array format, rawValue:", rawValue);
|
||||
} else if (content?.barcodes?.[0]?.rawValue) {
|
||||
rawValue = content.barcodes[0].rawValue;
|
||||
logger.log("Processing barcodes array format, rawValue:", rawValue);
|
||||
} else if (content?.rawValue) {
|
||||
rawValue = content.rawValue;
|
||||
logger.log("Processing object format, rawValue:", rawValue);
|
||||
} else if (typeof content === "string") {
|
||||
rawValue = content;
|
||||
logger.log("Processing string format, rawValue:", rawValue);
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
logger.error("No valid QR code URL detected");
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Invalid Contact QR Code",
|
||||
text: "No QR code detected with contact information.",
|
||||
},
|
||||
5000,
|
||||
if (!rawValue) {
|
||||
logger.error("No valid QR code content found in:", content);
|
||||
this.danger("No QR code detected. Please try again.", "Scan Error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL format first
|
||||
if (!rawValue.startsWith("http://") && !rawValue.startsWith("https://")) {
|
||||
logger.error("Invalid URL format:", rawValue);
|
||||
this.danger(
|
||||
"Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
|
||||
"Invalid Format",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newContact: Contact;
|
||||
try {
|
||||
logger.log("Attempting to extract JWT from URL:", url);
|
||||
const jwt = getContactJwtFromJwtUrl(url);
|
||||
// Extract JWT from URL
|
||||
const jwt = getContactJwtFromJwtUrl(rawValue);
|
||||
if (!jwt) {
|
||||
logger.error("Failed to extract JWT from URL");
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Invalid QR Code",
|
||||
text: "Could not extract contact information from QR code.",
|
||||
},
|
||||
3000,
|
||||
logger.error("Failed to extract JWT from URL:", rawValue);
|
||||
this.danger(
|
||||
"Could not extract contact information from the QR code. Please try again.",
|
||||
"Invalid QR Code",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("Successfully extracted JWT, attempting to decode");
|
||||
const { payload } = decodeEndorserJwt(jwt);
|
||||
// Log JWT for debugging
|
||||
logger.log("Extracted JWT:", jwt);
|
||||
|
||||
if (!payload) {
|
||||
logger.error("JWT payload is null or undefined");
|
||||
this.danger("Invalid JWT format", "Contact Error");
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (!payload.own) {
|
||||
logger.error("JWT payload missing 'own' property");
|
||||
this.danger("Contact information is incomplete", "Contact Error");
|
||||
const { payload } = decodeEndorserJwt(jwt);
|
||||
if (!payload) {
|
||||
logger.error("Failed to decode JWT payload");
|
||||
this.danger(
|
||||
"Could not decode the contact information. Please try again.",
|
||||
"Decode Error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Log decoded payload for debugging
|
||||
logger.log("Decoded JWT payload:", JSON.stringify(payload, null, 2));
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
newContact = {
|
||||
did: payload.own.did || payload.iss,
|
||||
name: payload.own.name || "",
|
||||
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash || "",
|
||||
profileImageUrl: payload.own.profileImageUrl || "",
|
||||
publicKeyBase64: payload.own.publicEncKey || "",
|
||||
registered: payload.own.registered || false,
|
||||
did: payload.own?.did || payload.iss,
|
||||
name: payload.own?.name,
|
||||
nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
|
||||
profileImageUrl: payload.own?.profileImageUrl,
|
||||
publicKeyBase64: payload.own?.publicEncKey,
|
||||
registered: payload.own?.registered,
|
||||
};
|
||||
|
||||
if (!newContact.did) {
|
||||
logger.error("Contact missing DID");
|
||||
this.danger("Contact is missing identifier", "Invalid Contact");
|
||||
this.danger(
|
||||
"Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
|
||||
"Incomplete Contact",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDid(newContact.did)) {
|
||||
logger.error("Invalid DID format:", newContact.did);
|
||||
this.danger("Invalid contact identifier format", "Invalid Contact");
|
||||
this.danger(
|
||||
"Invalid contact identifier format. The identifier must begin with 'did:'.",
|
||||
"Invalid Identifier",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("Saving new contact:", {
|
||||
...newContact,
|
||||
publicKeyBase64: "[REDACTED]",
|
||||
});
|
||||
await db.open();
|
||||
await db.contacts.add(newContact);
|
||||
|
||||
@@ -754,15 +772,10 @@ export default class ContactQRScanShowView extends Vue {
|
||||
}, 500);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error processing contact QR code:", e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Contact Error",
|
||||
text: "Could not process contact information. Please try scanning again.",
|
||||
},
|
||||
5000,
|
||||
logger.error("Error processing QR code:", e);
|
||||
this.danger(
|
||||
"Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.",
|
||||
"Processing Error",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -985,21 +998,5 @@ export default class ContactQRScanShowView extends Vue {
|
||||
this.state.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>
|
||||
|
||||
Reference in New Issue
Block a user