forked from trent_larson/crowd-funder-for-time-pwa
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">
|
<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
|
||||||
|
logger.log("Scan result received:", JSON.stringify(content, null, 2));
|
||||||
|
|
||||||
if (typeof content === "object" && content !== null) {
|
// Handle both array format and direct object format
|
||||||
// Handle Capacitor MLKit scanner format
|
let rawValue: string | undefined;
|
||||||
if (
|
|
||||||
"barcodes" in content &&
|
if (Array.isArray(content)) {
|
||||||
Array.isArray((content as MLKitScanResult).barcodes)
|
rawValue = content[0]?.rawValue;
|
||||||
) {
|
logger.log("Processing array format, rawValue:", rawValue);
|
||||||
const mlkitResult = content as MLKitScanResult;
|
} else if (content?.barcodes?.[0]?.rawValue) {
|
||||||
url = mlkitResult.barcodes[0]?.rawValue;
|
rawValue = content.barcodes[0].rawValue;
|
||||||
}
|
logger.log("Processing barcodes array format, rawValue:", rawValue);
|
||||||
// Handle web QR reader format
|
} else if (content?.rawValue) {
|
||||||
else if (Array.isArray(content)) {
|
rawValue = content.rawValue;
|
||||||
const webResult = content as WebQRResult[];
|
logger.log("Processing object format, rawValue:", rawValue);
|
||||||
url = webResult[0]?.rawValue;
|
} else if (typeof content === "string") {
|
||||||
}
|
rawValue = content;
|
||||||
// Handle direct object format
|
logger.log("Processing string format, rawValue:", rawValue);
|
||||||
else if ("rawValue" in content) {
|
|
||||||
const directResult = content as WebQRResult;
|
|
||||||
url = directResult.rawValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle direct string format
|
|
||||||
else if (typeof content === "string") {
|
|
||||||
url = content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url) {
|
if (!rawValue) {
|
||||||
logger.error("No valid QR code URL detected");
|
logger.error("No valid QR code content found in:", content);
|
||||||
this.$notify(
|
this.danger("No QR code detected. Please try again.", "Scan Error");
|
||||||
{
|
return;
|
||||||
group: "alert",
|
}
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact QR Code",
|
// Validate URL format first
|
||||||
text: "No QR code detected with contact information.",
|
if (!rawValue.startsWith("http://") && !rawValue.startsWith("https://")) {
|
||||||
},
|
logger.error("Invalid URL format:", rawValue);
|
||||||
5000,
|
this.danger(
|
||||||
|
"Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
|
||||||
|
"Invalid Format",
|
||||||
);
|
);
|
||||||
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);
|
||||||
|
|
||||||
if (!payload) {
|
// Validate JWT format
|
||||||
logger.error("JWT payload is null or undefined");
|
if (
|
||||||
this.danger("Invalid JWT format", "Contact Error");
|
!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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.own) {
|
const { payload } = decodeEndorserJwt(jwt);
|
||||||
logger.error("JWT payload missing 'own' property");
|
if (!payload) {
|
||||||
this.danger("Contact information is incomplete", "Contact Error");
|
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;
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user