+
+
+
+
+
{{ processingStatus }}
+
{{ processingDetails }}
+
+
+
@@ -202,6 +211,20 @@ interface WorkerErrorMessage {
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage;
+interface WorkerResult {
+ success: boolean;
+ code?: {
+ data: string;
+ location: {
+ topLeft: { x: number; y: number };
+ topRight: { x: number; y: number };
+ bottomLeft: { x: number; y: number };
+ bottomRight: { x: number; y: number };
+ };
+ };
+ error?: string;
+}
+
@Component({
components: {
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
@@ -238,42 +261,75 @@ export default class ContactQRScanShow extends Vue {
private isCapturingPhoto = false;
private appStateListener?: { remove: () => Promise };
+ private isProcessing = false;
+ private processingStatus = '';
+ private processingDetails = '';
+
+ private worker: Worker | null = null;
+ private workerInitialized = false;
+ private initializationAttempts = 0;
+ private readonly MAX_INITIALIZATION_ATTEMPTS = 3;
+
async created() {
logger.log('ContactQRScanShow component created');
try {
// Remove any existing listeners first
- await App.removeAllListeners();
+ await this.cleanupAppListeners();
// Add app state listeners
- const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
+ await this.setupAppLifecycleListeners();
+
+ // Load initial data
+ await this.loadInitialData();
+
+ logger.log('ContactQRScanShow initialization complete');
+ } catch (error) {
+ logger.error('Failed to initialize ContactQRScanShow:', error);
+ this.showError('Failed to initialize. Please try again.');
+ }
+ }
+
+ private async cleanupAppListeners(): Promise {
+ try {
+ if (this.appStateListener) {
+ await this.appStateListener.remove();
+ this.appStateListener = undefined;
+ }
+ await App.removeAllListeners();
+ logger.log('App listeners cleaned up successfully');
+ } catch (error) {
+ logger.error('Error cleaning up app listeners:', error);
+ throw error;
+ }
+ }
+
+ private async setupAppLifecycleListeners(): Promise {
+ try {
+ // Add app state change listener
+ this.appStateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
logger.log('App state changed:', state);
- if (!state.isActive && this.cameraActive) {
+ if (!state.isActive) {
this.cleanupCamera();
}
});
- this.appStateListener = stateListener;
+ // Add pause listener
await App.addListener('pause', () => {
logger.log('App paused');
- if (this.cameraActive) {
- this.cleanupCamera();
- }
+ this.cleanupCamera();
});
+ // Add resume listener
await App.addListener('resume', () => {
logger.log('App resumed');
- if (this.cameraActive) {
- this.initializeCamera();
- }
+ // Don't automatically reinitialize camera on resume
+ // Let user explicitly request camera access again
});
- // Load initial data
- await this.loadInitialData();
-
- logger.log('ContactQRScanShow initialization complete');
+ logger.log('App lifecycle listeners setup complete');
} catch (error) {
- logger.error('Failed to initialize ContactQRScanShow:', error);
- this.showError('Failed to initialize. Please try again.');
+ logger.error('Error setting up app lifecycle listeners:', error);
+ throw error;
}
}
@@ -292,22 +348,34 @@ export default class ContactQRScanShow extends Vue {
}
}
- private cleanupCamera() {
+ private async cleanupCamera() {
try {
- this.cameraActive = false;
- this.isCapturingPhoto = false;
- this.addCameraState('cleanup');
- logger.log('Camera cleaned up successfully');
+ if (this.cameraActive) {
+ this.cameraActive = false;
+ this.isCapturingPhoto = false;
+ this.addCameraState('cleanup');
+ logger.log('Camera cleaned up successfully');
+ }
} catch (error) {
logger.error('Error during camera cleanup:', error);
}
}
private addCameraState(state: CameraState | QRProcessingState, details?: Record) {
+ // Prevent duplicate state transitions
+ if (this.lastCameraState === state) {
+ return;
+ }
+
const entry: CameraStateHistoryEntry = {
state,
timestamp: Date.now(),
- details
+ details: {
+ ...details,
+ cameraActive: this.cameraActive,
+ isCapturingPhoto: this.isCapturingPhoto,
+ isProcessing: this.isProcessing
+ }
};
this.cameraStateHistory.push(entry);
@@ -316,17 +384,11 @@ export default class ContactQRScanShow extends Vue {
this.cameraStateHistory.shift();
}
- // Enhanced logging with better details
- logger.log('Camera state transition:', {
- state,
- details: {
- ...details,
- cameraActive: this.cameraActive,
- isCapturingPhoto: this.isCapturingPhoto,
- historyLength: this.cameraStateHistory.length
- }
+ this.logWithDetails('Camera state transition', {
+ state: entry.state,
+ timestamp: entry.timestamp,
+ details: entry.details
});
-
this.lastCameraState = state;
}
@@ -363,6 +425,11 @@ export default class ContactQRScanShow extends Vue {
]).catch(error => {
logger.error('Error during component cleanup:', error);
});
+
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
+ }
}
private async handleQRCodeResult(data: string): Promise {
@@ -456,114 +523,376 @@ export default class ContactQRScanShow extends Vue {
}
}
- async processImageForQRCode(imageDataUrl: string): Promise {
+ private async initializeWorker(): Promise {
try {
- logger.log('Starting QR code processing');
+ if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) {
+ throw new Error('Maximum worker initialization attempts reached');
+ }
+
+ this.initializationAttempts++;
+ this.setProcessingStatus('Initializing', `Setting up QR scanner (attempt ${this.initializationAttempts})...`);
+ logger.log('Starting worker initialization', { attempt: this.initializationAttempts });
- // Create worker for image processing
- const worker = new Worker(URL.createObjectURL(new Blob([`
- self.onmessage = async function(e) {
+ // Import and wait for jsQR to be fully loaded
+ logger.log('Loading jsQR module...');
+ const jsQRModule = await import('jsqr');
+ if (!jsQRModule?.default) {
+ throw new Error('Failed to load jsQR module');
+ }
+ logger.log('jsQR module loaded successfully');
+
+ // Create worker with inline code and bundled jsQR
+ const workerCode = `
+ let jsQRInitialized = false;
+ let initializationError = null;
+
+ try {
+ // Get the raw jsQR function code
+ const jsQRCode = ${jsQRModule.default.toString()};
+
+ // Create a proper module-like environment
+ const context = {
+ module: { exports: {} },
+ exports: {},
+ self: self,
+ window: self, // Some modules expect window to be defined
+ global: self // Some modules expect global to be defined
+ };
+
+ // Create a function that will run in the proper context
+ const initFunction = new Function(
+ 'module', 'exports', 'self', 'window', 'global',
+ jsQRCode
+ );
+
+ // Execute the function in our context
+ initFunction.call(
+ context,
+ context.module,
+ context.exports,
+ context.self,
+ context.window,
+ context.global
+ );
+
+ // Get the jsQR function - it might be exported directly or as default
+ self.jsQR = context.module.exports.default || context.module.exports;
+
+ // Verify the function works
+ if (typeof self.jsQR === 'function') {
+ // Test with a small dummy image
+ const testData = new Uint8ClampedArray(4 * 4 * 4);
+ self.jsQR(testData, 4, 4, { inversionAttempts: "dontInvert" });
+
+ jsQRInitialized = true;
+ self.postMessage({ initialized: true });
+ } else {
+ throw new Error('jsQR is not a function after initialization');
+ }
+ } catch (error) {
+ initializationError = error;
+ console.error('Worker initialization error:', error);
+ self.postMessage({
+ error: 'Failed to initialize jsQR: ' + error.message,
+ details: {
+ errorType: error.name,
+ errorStack: error.stack,
+ jsQRType: typeof self.jsQR
+ }
+ });
+ }
+
+ self.onmessage = function(e) {
+ if (!jsQRInitialized) {
+ self.postMessage({
+ success: false,
+ error: 'QR scanner not initialized',
+ details: {
+ initializationError: initializationError ? initializationError.message : 'Unknown error',
+ jsQRType: typeof self.jsQR
+ }
+ });
+ return;
+ }
+
const { imageData, width, height } = e.data;
try {
- // Import jsQR in the worker
- importScripts('${window.location.origin}/assets/jsqr.js');
- const code = self.jsQR(imageData, width, height, {
- inversionAttempts: "dontInvert"
+ const uint8Array = new Uint8ClampedArray(imageData);
+
+ // Try normal orientation
+ let code = self.jsQR(uint8Array, width, height, {
+ inversionAttempts: "attemptBoth"
});
- self.postMessage({ success: true, code });
+
+ if (!code) {
+ // Try rotated 90 degrees
+ const rotated = rotateImageData(new ImageData(uint8Array, width, height), width, height);
+ if (rotated) {
+ code = self.jsQR(new Uint8ClampedArray(rotated.data), rotated.width, rotated.height, {
+ inversionAttempts: "attemptBoth"
+ });
+ }
+ }
+
+ self.postMessage(code ? { success: true, code } : { success: false, error: 'No QR code found' });
} catch (error) {
- self.postMessage({ success: false, error: error.message });
+ console.error('QR processing error:', error);
+ self.postMessage({
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ details: {
+ errorType: error.name,
+ errorStack: error instanceof Error ? error.stack : undefined,
+ jsQRType: typeof self.jsQR
+ }
+ });
}
};
- `], { type: 'text/javascript' })));
- const image = new Image();
- image.crossOrigin = 'anonymous';
- image.src = imageDataUrl;
+ function rotateImageData(imageData, width, height) {
+ const canvas = new OffscreenCanvas(height, width);
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return null;
+
+ const newImageData = ctx.createImageData(height, width);
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const srcIndex = (y * width + x) * 4;
+ const destIndex = ((width - x - 1) * height + y) * 4;
+ newImageData.data[destIndex] = imageData.data[srcIndex];
+ newImageData.data[destIndex + 1] = imageData.data[srcIndex + 1];
+ newImageData.data[destIndex + 2] = imageData.data[srcIndex + 2];
+ newImageData.data[destIndex + 3] = imageData.data[srcIndex + 3];
+ }
+ }
+ return { data: newImageData.data, width: height, height: width };
+ }
+ `;
+
+ // Terminate existing worker if it exists
+ if (this.worker) {
+ logger.log('Terminating existing worker');
+ this.worker.terminate();
+ this.worker = null;
+ this.workerInitialized = false;
+ }
+
+ // Create blob and worker
+ logger.log('Creating worker blob and URL');
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
+ const workerUrl = URL.createObjectURL(blob);
- await new Promise((resolve, reject) => {
- const timeout = setTimeout(() => reject(new Error('Image load timeout')), 5000);
- image.onload = () => {
- clearTimeout(timeout);
- resolve(undefined);
+ logger.log('Creating new worker');
+ this.worker = new Worker(workerUrl);
+
+ // Wait for worker to be ready with increased timeout
+ await new Promise((resolve, reject) => {
+ if (!this.worker) {
+ reject(new Error('Worker failed to initialize'));
+ return;
+ }
+
+ logger.log('Setting up worker message handlers');
+ const timeoutId = setTimeout(() => {
+ logger.error('Worker initialization timed out after 10 seconds');
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
+ }
+ reject(new Error('Worker initialization timed out'));
+ }, 10000);
+
+ this.worker.onmessage = (e) => {
+ logger.log('Received worker message:', e.data);
+ if (e.data?.initialized) {
+ clearTimeout(timeoutId);
+ this.workerInitialized = true;
+ resolve();
+ } else if (e.data?.error) {
+ clearTimeout(timeoutId);
+ reject(new Error(e.data.error));
+ }
};
- image.onerror = () => {
- clearTimeout(timeout);
- reject(new Error('Failed to load image'));
+
+ this.worker.onerror = (error) => {
+ logger.error('Worker error during initialization:', error);
+ clearTimeout(timeoutId);
+ reject(new Error(`Worker error: ${error.message}`));
};
});
- logger.log('Image loaded, creating canvas...');
- const canvas = document.createElement('canvas');
- const maxDimension = 1024; // Limit image size for better performance
+ // Clean up the URL after worker is initialized
+ URL.revokeObjectURL(workerUrl);
+ logger.log('Worker initialized successfully');
+ this.setProcessingStatus('Ready', 'QR scanner initialized');
- // Scale down image if needed while maintaining aspect ratio
- let width = image.naturalWidth || 800;
- let height = image.naturalHeight || 600;
- if (width > maxDimension || height > maxDimension) {
- if (width > height) {
- height = Math.floor(height * (maxDimension / width));
- width = maxDimension;
- } else {
- width = Math.floor(width * (maxDimension / height));
- height = maxDimension;
- }
- }
-
- canvas.width = width;
- canvas.height = height;
+ } catch (error) {
+ logger.error('Failed to initialize worker:', error);
+ this.setProcessingStatus('Error', `Failed to initialize QR scanner: ${error instanceof Error ? error.message : String(error)}`);
- const ctx = canvas.getContext('2d');
- if (!ctx) {
- throw new Error('Failed to get canvas context');
+ if (this.worker) {
+ this.worker.terminate();
+ this.worker = null;
}
+ this.workerInitialized = false;
- // Draw image maintaining orientation
- ctx.save();
- ctx.drawImage(image, 0, 0, width, height);
- ctx.restore();
+ // Show appropriate error message based on attempt count
+ if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) {
+ this.$notify({
+ group: "alert",
+ type: "error",
+ title: "QR Scanner Error",
+ text: "Failed to initialize QR scanner after multiple attempts. Please try again later.",
+ });
+ } else {
+ this.$notify({
+ group: "alert",
+ type: "warning",
+ title: "QR Scanner Warning",
+ text: "Failed to initialize QR scanner. Retrying...",
+ });
+ }
+ throw error;
+ }
+ }
- const imageData = ctx.getImageData(0, 0, width, height);
+ private async loadImage(imageDataUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
- logger.log('Processing image data for QR code...', {
- width,
- height,
- dataLength: imageData.data.length
- });
+ const loadTimeout = setTimeout(() => {
+ img.src = '';
+ reject(new Error('Image load timeout'));
+ }, 5000);
+
+ img.onload = () => {
+ clearTimeout(loadTimeout);
+ resolve(img);
+ };
+
+ img.onerror = () => {
+ clearTimeout(loadTimeout);
+ reject(new Error('Failed to load image'));
+ };
+
+ img.src = imageDataUrl;
+ });
+ }
- // Process QR code in worker
- const result = await new Promise((resolve, reject) => {
- worker.onmessage = (e: MessageEvent) => {
- if (e.data.success) {
- resolve(e.data.code);
- } else {
- reject(new Error(e.data.error));
+ private getImageData(img: HTMLImageElement): { imageData: ImageData; width: number; height: number } {
+ const MAX_IMAGE_DIMENSION = 1024;
+ const aspectRatio = img.naturalWidth / img.naturalHeight;
+
+ // Calculate dimensions maintaining aspect ratio
+ let width = MAX_IMAGE_DIMENSION;
+ let height = MAX_IMAGE_DIMENSION;
+
+ if (aspectRatio > 1) {
+ height = Math.round(width / aspectRatio);
+ } else {
+ width = Math.round(height * aspectRatio);
+ }
+
+ const canvas = new OffscreenCanvas(width, height);
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
+ if (!ctx) {
+ throw new Error('Failed to get canvas context');
+ }
+
+ // Clear canvas and ensure proper rendering
+ ctx.clearRect(0, 0, width, height);
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(img, 0, 0, width, height);
+
+ const imageData = ctx.getImageData(0, 0, width, height);
+ return { imageData, width, height };
+ }
+
+ private async processImageForQRCode(imageDataUrl: string): Promise {
+ try {
+ if (!this.worker || !this.workerInitialized) {
+ this.setProcessingStatus('Initializing', 'Setting up QR scanner...');
+ try {
+ await this.initializeWorker();
+ } catch (error) {
+ this.setProcessingStatus('Error', 'QR scanner initialization failed');
+ this.$notify({
+ group: "alert",
+ type: "error",
+ title: "QR Scanner Error",
+ text: "Failed to initialize QR scanner. Please try again.",
+ });
+ return;
+ }
+ }
+
+ this.isProcessing = true;
+ this.setProcessingStatus('Processing', 'Loading image...');
+
+ try {
+ const img = await this.loadImage(imageDataUrl);
+ const { imageData, width, height } = this.getImageData(img);
+
+ this.setProcessingStatus('Processing', 'Analyzing image for QR code...');
+
+ const result = await new Promise((resolve, reject) => {
+ if (!this.worker) {
+ reject(new Error('Worker not initialized'));
+ return;
}
- };
- worker.onerror = reject;
- worker.postMessage({
- imageData: imageData.data,
- width: imageData.width,
- height: imageData.height
- });
- });
- worker.terminate();
+ const timeoutId = setTimeout(() => {
+ this.setProcessingStatus('Error', 'QR code processing timed out');
+ reject(new Error('QR code processing timed out'));
+ }, 30000);
+
+ this.worker.onmessage = (e: MessageEvent) => {
+ clearTimeout(timeoutId);
+ resolve(e.data);
+ };
+
+ this.worker.onerror = (error) => {
+ clearTimeout(timeoutId);
+ this.setProcessingStatus('Error', `Processing error: ${error.message}`);
+ reject(error);
+ };
+
+ this.setProcessingStatus('Processing', 'Scanning for QR code...');
+ this.worker.postMessage({
+ imageData: Array.from(imageData.data),
+ width,
+ height
+ });
+ });
- if (result) {
- logger.log('QR code found:', { data: result.data });
- await this.handleQRCodeResult(result.data);
- } else {
- logger.log('No QR code found in image');
- this.showError('No QR code found. Please try again.');
+ if (result.success && result.code) {
+ this.setProcessingStatus('Success', 'QR code found!');
+ await this.handleQRCodeResult(result.code.data);
+ } else {
+ this.setProcessingStatus('Error', result.error || 'No QR code found');
+ this.$notify({
+ group: "alert",
+ type: "warning",
+ title: "No QR Code Found",
+ text: "Please make sure the QR code is clearly visible and try again.",
+ });
+ }
+ } catch (error) {
+ console.error('QR processing error:', error);
+ this.setProcessingStatus('Error', `Failed to process QR code: ${error instanceof Error ? error.message : String(error)}`);
+ this.$notify({
+ group: "alert",
+ type: "error",
+ title: "Processing Error",
+ text: "Failed to process the image. Please try again.",
+ });
}
- } catch (error) {
- logger.error('QR code processing failed:', error);
- this.showError('Failed to process QR code. Please try again.');
} finally {
- this.cameraActive = false;
+ this.isProcessing = false;
this.isCapturingPhoto = false;
this.addCameraState('processing_completed');
+ this.addCameraState('capture_completed');
}
}
@@ -609,9 +938,9 @@ export default class ContactQRScanShow extends Vue {
this.danger(
"Could not extract contact information from the QR code. Please try again.",
"Invalid QR Code",
- );
- return;
- }
+ );
+ return;
+ }
// Validate JWT format
if (
@@ -1057,5 +1386,34 @@ export default class ContactQRScanShow extends Vue {
this.addCameraState('capture_completed');
}
}
+
+ private setProcessingStatus(status: string, details = '') {
+ this.isProcessing = status !== 'Error' && status !== 'Success';
+ this.processingStatus = status;
+ this.processingDetails = details;
+
+ // Log the status change
+ this.logWithDetails('Processing status changed', {
+ status,
+ details,
+ isProcessing: this.isProcessing
+ });
+ }
+
+ private logWithDetails(message: string, details?: Record) {
+ const formattedDetails = details ?
+ '\n' + JSON.stringify(details, (key, value) => {
+ if (value instanceof Error) {
+ return {
+ message: value.message,
+ stack: value.stack,
+ name: value.name
+ };
+ }
+ return value;
+ }, 2) : '';
+
+ logger.log(`${message}${formattedDetails}`);
+ }
}