forked from jsnbuchanan/crowd-funder-for-time-pwa
Remove temporary alert() debug calls from ImageMethodDialog camera preview
- Cleaned up all alert() calls used for diagnosing camera access issues on mobile browsers - Camera preview now starts without pop-up interruptions - Retained logging and user notifications for error handling and diagnostics
This commit is contained in:
@@ -1,6 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||||
<div class="dialog relative">
|
<div class="dialog relative">
|
||||||
|
<!-- Diagnostic Panel -->
|
||||||
|
<div
|
||||||
|
v-if="showDiagnostics"
|
||||||
|
class="absolute top-0 left-0 right-0 bg-black/80 text-white text-xs p-2 z-20 overflow-auto max-h-[30vh]"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<p><strong>Camera State:</strong> {{ cameraState }}</p>
|
||||||
|
<p><strong>State Message:</strong> {{ cameraStateMessage || 'None' }}</p>
|
||||||
|
<p><strong>Error:</strong> {{ error || 'None' }}</p>
|
||||||
|
<p><strong>Preview Active:</strong> {{ showCameraPreview ? 'Yes' : 'No' }}</p>
|
||||||
|
<p><strong>Stream Active:</strong> {{ !!cameraStream ? 'Yes' : 'No' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p><strong>Browser:</strong> {{ userAgent }}</p>
|
||||||
|
<p><strong>HTTPS:</strong> {{ isSecureContext ? 'Yes' : 'No' }}</p>
|
||||||
|
<p><strong>MediaDevices:</strong> {{ hasMediaDevices ? 'Yes' : 'No' }}</p>
|
||||||
|
<p><strong>GetUserMedia:</strong> {{ hasGetUserMedia ? 'Yes' : 'No' }}</p>
|
||||||
|
<p><strong>Platform:</strong> {{ platformCapabilities.isMobile ? 'Mobile' : 'Desktop' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Diagnostics Button -->
|
||||||
|
<button
|
||||||
|
@click="toggleDiagnostics"
|
||||||
|
class="absolute top-2 right-2 bg-black/50 text-white px-2 py-1 rounded text-xs z-30"
|
||||||
|
>
|
||||||
|
{{ showDiagnostics ? 'Hide Diagnostics' : 'Show Diagnostics' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="text-lg text-center font-bold relative">
|
<div class="text-lg text-center font-bold relative">
|
||||||
<h1 id="ViewHeading" class="text-center font-bold">
|
<h1 id="ViewHeading" class="text-center font-bold">
|
||||||
<span v-if="uploading">Uploading Image…</span>
|
<span v-if="uploading">Uploading Image…</span>
|
||||||
@@ -16,6 +47,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FEEDBACK: Show if camera preview is not visible after mounting -->
|
||||||
|
<div v-if="!showCameraPreview && !blob && isRegistered" class="bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm">
|
||||||
|
<strong>Camera preview not started.</strong>
|
||||||
|
<div v-if="cameraState === 'off'">
|
||||||
|
<span v-if="platformCapabilities.isMobile">
|
||||||
|
<b>Note:</b> This mobile browser may not support direct camera access, or the app is treating it as a native app.<br>
|
||||||
|
<b>Tip:</b> Try using a desktop browser, or check if your browser supports camera access for web apps.<br>
|
||||||
|
<b>Developer:</b> The platform detection logic may be skipping camera preview for mobile browsers. <br>
|
||||||
|
<b>Action:</b> Review <code>platformCapabilities.isMobile</code> and ensure web browsers on mobile are not treated as native apps.
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<b>Tip:</b> Your browser supports camera APIs, but the preview did not start. Try refreshing the page or checking browser permissions.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="cameraState === 'error'">
|
||||||
|
<b>Error:</b> {{ error || cameraStateMessage }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<b>Status:</b> {{ cameraStateMessage || 'Unknown reason.' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<template v-if="isRegistered">
|
<template v-if="isRegistered">
|
||||||
<div v-if="!blob">
|
<div v-if="!blob">
|
||||||
@@ -227,6 +280,16 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
|
|
||||||
private platformCapabilities = this.platformService.getCapabilities();
|
private platformCapabilities = this.platformService.getCapabilities();
|
||||||
|
|
||||||
|
// Add diagnostic properties
|
||||||
|
showDiagnostics = false;
|
||||||
|
userAgent = navigator.userAgent;
|
||||||
|
isSecureContext = window.isSecureContext;
|
||||||
|
hasMediaDevices = !!navigator.mediaDevices;
|
||||||
|
hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
||||||
|
cameraState: 'off' | 'initializing' | 'ready' | 'active' | 'error' | 'permission_denied' | 'not_found' | 'in_use' = 'off';
|
||||||
|
cameraStateMessage?: string;
|
||||||
|
error: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle hook: Initializes component and retrieves user settings
|
* Lifecycle hook: Initializes component and retrieves user settings
|
||||||
* @throws {Error} When settings retrieval fails
|
* @throws {Error} When settings retrieval fails
|
||||||
@@ -251,6 +314,14 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Try to start camera preview automatically if not on mobile
|
||||||
|
if (!this.platformCapabilities.isNativeApp) {
|
||||||
|
this.startCameraPreview();
|
||||||
|
} else {
|
||||||
|
logger.warn("Camera preview not started: running in native app context.");
|
||||||
|
this.cameraState = 'off';
|
||||||
|
this.cameraStateMessage = 'Camera preview not started due to native app context.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,7 +338,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
this.visible = true;
|
this.visible = true;
|
||||||
|
|
||||||
// Start camera preview immediately if not on mobile
|
// Start camera preview immediately if not on mobile
|
||||||
if (!this.platformCapabilities.isMobile) {
|
if (!this.platformCapabilities.isNativeApp) {
|
||||||
this.startCameraPreview();
|
this.startCameraPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,14 +410,21 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||||
logger.debug("Platform capabilities:", this.platformCapabilities);
|
logger.debug("Platform capabilities:", this.platformCapabilities);
|
||||||
|
|
||||||
if (this.platformCapabilities.isMobile) {
|
if (this.platformCapabilities.isNativeApp) {
|
||||||
logger.debug("Using platform service for mobile device");
|
logger.debug("Using platform service for mobile device");
|
||||||
|
this.cameraState = 'initializing';
|
||||||
|
this.cameraStateMessage = 'Using platform camera service...';
|
||||||
try {
|
try {
|
||||||
const result = await this.platformService.takePicture();
|
const result = await this.platformService.takePicture();
|
||||||
this.blob = result.blob;
|
this.blob = result.blob;
|
||||||
this.fileName = result.fileName;
|
this.fileName = result.fileName;
|
||||||
|
this.cameraState = 'ready';
|
||||||
|
this.cameraStateMessage = 'Photo captured successfully';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error taking picture:", error);
|
logger.error("Error taking picture:", error);
|
||||||
|
this.cameraState = 'error';
|
||||||
|
this.cameraStateMessage = error instanceof Error ? error.message : 'Failed to take picture';
|
||||||
|
this.error = error instanceof Error ? error.message : 'Failed to take picture';
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -362,13 +440,18 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
|
|
||||||
logger.debug("Starting camera preview for desktop browser");
|
logger.debug("Starting camera preview for desktop browser");
|
||||||
try {
|
try {
|
||||||
|
this.cameraState = 'initializing';
|
||||||
|
this.cameraStateMessage = 'Requesting camera access...';
|
||||||
this.showCameraPreview = true;
|
this.showCameraPreview = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: "environment" },
|
video: { facingMode: "environment" },
|
||||||
});
|
});
|
||||||
|
logger.debug("Camera access granted");
|
||||||
this.cameraStream = stream;
|
this.cameraStream = stream;
|
||||||
|
this.cameraState = 'active';
|
||||||
|
this.cameraStateMessage = 'Camera is active';
|
||||||
|
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
@@ -385,6 +468,10 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error starting camera preview:", error);
|
logger.error("Error starting camera preview:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to access camera';
|
||||||
|
this.cameraState = 'error';
|
||||||
|
this.cameraStateMessage = errorMessage;
|
||||||
|
this.error = errorMessage;
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -404,6 +491,9 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
this.cameraStream = null;
|
this.cameraStream = null;
|
||||||
}
|
}
|
||||||
this.showCameraPreview = false;
|
this.showCameraPreview = false;
|
||||||
|
this.cameraState = 'off';
|
||||||
|
this.cameraStateMessage = 'Camera stopped';
|
||||||
|
this.error = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async capturePhoto() {
|
async capturePhoto() {
|
||||||
@@ -449,7 +539,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
|
|
||||||
async retryImage() {
|
async retryImage() {
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
if (!this.platformCapabilities.isMobile) {
|
if (!this.platformCapabilities.isNativeApp) {
|
||||||
await this.startCameraPreview();
|
await this.startCameraPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,6 +623,11 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add toggle method
|
||||||
|
toggleDiagnostics() {
|
||||||
|
this.showDiagnostics = !this.showDiagnostics;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -562,4 +657,11 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add styles for diagnostic panel */
|
||||||
|
.diagnostic-panel {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -90,16 +90,57 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// First try the Permissions API if available
|
||||||
|
if (navigator.permissions && navigator.permissions.query) {
|
||||||
|
try {
|
||||||
const permissions = await navigator.permissions.query({
|
const permissions = await navigator.permissions.query({
|
||||||
name: "camera" as PermissionName,
|
name: "camera" as PermissionName,
|
||||||
});
|
});
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Permission state:`,
|
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
||||||
permissions.state,
|
permissions.state,
|
||||||
);
|
);
|
||||||
const granted = permissions.state === "granted";
|
if (permissions.state === "granted") {
|
||||||
this.updateCameraState(granted ? "ready" : "permission_denied");
|
this.updateCameraState("ready", "Camera permissions granted");
|
||||||
return granted;
|
return true;
|
||||||
|
}
|
||||||
|
} catch (permError) {
|
||||||
|
// Permissions API might not be supported, continue with getUserMedia check
|
||||||
|
logger.error(
|
||||||
|
`[WebInlineQRScanner:${this.id}] Permissions API not supported:`,
|
||||||
|
permError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Permissions API is not available or didn't return granted,
|
||||||
|
// try a test getUserMedia call
|
||||||
|
try {
|
||||||
|
const testStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
});
|
||||||
|
// If we get here, we have permission
|
||||||
|
testStream.getTracks().forEach(track => track.stop());
|
||||||
|
this.updateCameraState("ready", "Camera permissions granted");
|
||||||
|
return true;
|
||||||
|
} catch (mediaError) {
|
||||||
|
const error = mediaError as Error;
|
||||||
|
logger.error(
|
||||||
|
`[WebInlineQRScanner:${this.id}] getUserMedia test failed:`,
|
||||||
|
{
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error.name === "NotAllowedError" || error.name === "PermissionDeniedError") {
|
||||||
|
this.updateCameraState("permission_denied", "Camera access denied");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// For other errors, we'll try requesting permissions explicitly
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Error checking camera permissions:`,
|
`[WebInlineQRScanner:${this.id}] Error checking camera permissions:`,
|
||||||
@@ -122,6 +163,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// First check if we have any video devices
|
// First check if we have any video devices
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
const videoDevices = devices.filter(
|
const videoDevices = devices.filter(
|
||||||
@@ -131,10 +173,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||||
count: videoDevices.length,
|
count: videoDevices.length,
|
||||||
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (videoDevices.length === 0) {
|
if (videoDevices.length === 0) {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] No video devices found`);
|
logger.error(`[WebInlineQRScanner:${this.id}] No video devices found`);
|
||||||
|
this.updateCameraState("not_found", "No camera found on this device");
|
||||||
throw new Error("No camera found on this device");
|
throw new Error("No camera found on this device");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +192,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: {
|
video: {
|
||||||
facingMode: "environment",
|
facingMode: "environment",
|
||||||
@@ -168,28 +213,35 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
track.stop();
|
track.stop();
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (mediaError) {
|
||||||
const wrappedError =
|
const error = mediaError as Error;
|
||||||
error instanceof Error ? error : new Error(String(error));
|
logger.error(
|
||||||
|
`[WebInlineQRScanner:${this.id}] Error requesting camera access:`,
|
||||||
|
{
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Update state based on error type
|
// Update state based on error type
|
||||||
if (
|
if (
|
||||||
wrappedError.name === "NotFoundError" ||
|
error.name === "NotFoundError" ||
|
||||||
wrappedError.name === "DevicesNotFoundError"
|
error.name === "DevicesNotFoundError"
|
||||||
) {
|
) {
|
||||||
this.updateCameraState("not_found", "No camera found on this device");
|
this.updateCameraState("not_found", "No camera found on this device");
|
||||||
throw new Error("No camera found on this device");
|
throw new Error("No camera found on this device");
|
||||||
} else if (
|
} else if (
|
||||||
wrappedError.name === "NotAllowedError" ||
|
error.name === "NotAllowedError" ||
|
||||||
wrappedError.name === "PermissionDeniedError"
|
error.name === "PermissionDeniedError"
|
||||||
) {
|
) {
|
||||||
this.updateCameraState("permission_denied", "Camera access denied");
|
this.updateCameraState("permission_denied", "Camera access denied");
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Camera access denied. Please grant camera permission and try again",
|
"Camera access denied. Please grant camera permission and try again",
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
wrappedError.name === "NotReadableError" ||
|
error.name === "NotReadableError" ||
|
||||||
wrappedError.name === "TrackStartError"
|
error.name === "TrackStartError"
|
||||||
) {
|
) {
|
||||||
this.updateCameraState(
|
this.updateCameraState(
|
||||||
"in_use",
|
"in_use",
|
||||||
@@ -197,9 +249,22 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
);
|
);
|
||||||
throw new Error("Camera is in use by another application");
|
throw new Error("Camera is in use by another application");
|
||||||
} else {
|
} else {
|
||||||
this.updateCameraState("error", wrappedError.message);
|
this.updateCameraState("error", error.message);
|
||||||
throw new Error(`Camera error: ${wrappedError.message}`);
|
throw new Error(`Camera error: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const wrappedError =
|
||||||
|
error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error(
|
||||||
|
`[WebInlineQRScanner:${this.id}] Error in requestPermissions:`,
|
||||||
|
{
|
||||||
|
error: wrappedError.message,
|
||||||
|
stack: wrappedError.stack,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw wrappedError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user