diff --git a/src/components/QRScanner/QRScannerDialog.vue b/src/components/QRScanner/QRScannerDialog.vue index 75e5213d..49660a57 100644 --- a/src/components/QRScanner/QRScannerDialog.vue +++ b/src/components/QRScanner/QRScannerDialog.vue @@ -67,8 +67,8 @@ <path class="opacity-75" fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 - 3.042 1.135 5.824 3 7.938l3-2.647z" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 + 3.042 1.135 5.824 3 7.938l3-2.647z" ></path> </svg> <span>{{ initializationStatus }}</span> @@ -164,6 +164,40 @@ </div> </div> + <!-- Error Banner --> + <div + v-if="error" + class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" + role="alert" + > + <strong class="font-bold">Camera Error:</strong> + <span class="block sm:inline">{{ error }}</span> + <ul class="mt-2 text-sm text-red-600 list-disc list-inside"> + <li v-if="error.includes('No camera found')"> + Check if your device has a camera and it is enabled. + </li> + <li v-if="error.includes('denied')"> + Allow camera access in your browser settings and reload the page. + </li> + <li v-if="error.includes('in use')"> + Close other applications that may be using the camera. + </li> + <li v-if="error.includes('HTTPS')"> + Ensure you are using a secure (HTTPS) connection. + </li> + <li + v-if=" + !error.includes('No camera found') && + !error.includes('denied') && + !error.includes('in use') && + !error.includes('HTTPS') + " + > + Try refreshing the page or using a different browser/device. + </li> + </ul> + </div> + <!-- Footer --> <div class="p-4 border-t border-gray-200"> <div class="flex flex-col space-y-4"> @@ -275,7 +309,8 @@ export default class QRScannerDialog extends Vue { async onInit(promise: Promise<void>): Promise<void> { if (this.isNativePlatform) { - logger.log("Skipping web scanner initialization on native platform"); + logger.log("Closing QR dialog on native platform"); + this.$nextTick(() => this.close()); return; } @@ -291,14 +326,28 @@ export default class QRScannerDialog extends Vue { ); } + // Check for video devices + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter( + (device) => device.kind === "videoinput", + ); + + if (videoDevices.length === 0) { + throw new Error("No camera found on this device"); + } + logger.log("Starting QR scanner initialization...", { mediaDevices: !!navigator.mediaDevices, getUserMedia: !!( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ), + videoDevices: videoDevices.length, constraints: { - video: true, - facingMode: this.preferredCamera, + video: { + facingMode: this.preferredCamera, + width: { ideal: 1280 }, + height: { ideal: 720 }, + }, }, }); @@ -308,6 +357,8 @@ export default class QRScannerDialog extends Vue { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: this.preferredCamera, + width: { ideal: 1280 }, + height: { ideal: 720 }, }, }); @@ -350,7 +401,7 @@ export default class QRScannerDialog extends Vue { // Now initialize the QR scanner this.initializationStatus = "Starting QR scanner..."; logger.log("Initializing QR scanner..."); - // await promise; // <-- comment this out for debugging + await promise; this.isInitializing = false; this.cameraStatus = "Ready"; @@ -401,8 +452,8 @@ export default class QRScannerDialog extends Vue { } }; - // Handle both promise and non-promise results - if (result && typeof result.then === "function") { + // Use instanceof Promise for type narrowing + if (result instanceof Promise) { result .then(processResult) .catch((error: Error) => this.handleError(error)) diff --git a/src/services/QRScanner/WebDialogQRScanner.ts b/src/services/QRScanner/WebDialogQRScanner.ts index 4af57c7e..ac7dfd3e 100644 --- a/src/services/QRScanner/WebDialogQRScanner.ts +++ b/src/services/QRScanner/WebDialogQRScanner.ts @@ -28,26 +28,95 @@ export class WebDialogQRScanner implements QRScannerService { async requestPermissions(): Promise<boolean> { try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + // First check if we have any video devices + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter( + (device) => device.kind === "videoinput", + ); + + if (videoDevices.length === 0) { + logger.error("No video devices found"); + throw new Error("No camera found on this device"); + } + + // Try to get a stream with specific constraints + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: "environment", + width: { ideal: 1280 }, + height: { ideal: 720 }, + }, + }); + + // Stop the test stream immediately stream.getTracks().forEach((track) => track.stop()); return true; } catch (error) { + const wrappedError = + error instanceof Error ? error : new Error(String(error)); logger.error("Error requesting camera permissions:", { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, + error: wrappedError.message, + stack: wrappedError.stack, + name: wrappedError.name, }); - return false; + + // Provide more specific error messages + if ( + wrappedError.name === "NotFoundError" || + wrappedError.name === "DevicesNotFoundError" + ) { + throw new Error("No camera found on this device"); + } else if ( + wrappedError.name === "NotAllowedError" || + wrappedError.name === "PermissionDeniedError" + ) { + throw new Error( + "Camera access denied. Please grant camera permission and try again", + ); + } else if ( + wrappedError.name === "NotReadableError" || + wrappedError.name === "TrackStartError" + ) { + throw new Error("Camera is in use by another application"); + } else { + throw new Error(`Camera error: ${wrappedError.message}`); + } } } async isSupported(): Promise<boolean> { - // Check for secure context first - if (!window.isSecureContext) { - logger.warn("Camera access requires HTTPS (secure context)"); + try { + // Check for secure context first + if (!window.isSecureContext) { + logger.warn("Camera access requires HTTPS (secure context)"); + return false; + } + + // Check for camera API support + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + logger.warn("Camera API not supported in this browser"); + return false; + } + + // Check if we have any video devices + const devices = await navigator.mediaDevices.enumerateDevices(); + const hasVideoDevices = devices.some( + (device) => device.kind === "videoinput", + ); + + if (!hasVideoDevices) { + logger.warn("No video devices found"); + return false; + } + + return true; + } catch (error) { + logger.error("Error checking camera support:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); return false; } - // Then check for camera API support - return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); } async startScan(): Promise<void> {