mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-23 23:28:18 +02:00
Improve camera access and troubleshooting for users on various devices
Enhances camera functionality by adding checks for browser compatibility, secure connection (HTTPS), device availability, file upload fallback, and a camera diagnostics test. Replit-Commit-Author: Agent Replit-Commit-Session-Id: dec54137-d5a2-4406-8ad4-5f2f9fb110bf Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/dde8c570-407a-4930-88c7-995e1ed77553.jpg
This commit is contained in:
1 parent
fbbe1375ab
commit
3ce27cfd4b
4 files changed
+302
-78
No files matched your search
@@ -10,9 +10,10 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useStoreContext } from '@/hooks/useStoreContext';
|
import { useStoreContext } from '@/hooks/useStoreContext';
|
||||||
import { useCamera } from '@/hooks/useCamera';
|
import { useCamera } from '@/hooks/useCamera';
|
||||||
|
import { useCameraTest } from '@/hooks/useCameraTest';
|
||||||
import { apiRequest } from '@/lib/queryClient';
|
import { apiRequest } from '@/lib/queryClient';
|
||||||
import { validateRequiredFields, validateEAN13, ARTICLE_CATEGORY_MAPPING, IDENTITY_TYPES, PAYMENT_METHODS } from '@/lib/validation';
|
import { validateRequiredFields, validateEAN13, ARTICLE_CATEGORY_MAPPING, IDENTITY_TYPES, PAYMENT_METHODS } from '@/lib/validation';
|
||||||
import { Package, User, Users, Save, Calendar, BadgeCheck, Info, Camera } from 'lucide-react';
|
import { Package, User, Users, Save, Calendar, BadgeCheck, Info, Camera, TestTube, Upload, Trash2 } from 'lucide-react';
|
||||||
import CameraModal from '@/components/CameraModal';
|
import CameraModal from '@/components/CameraModal';
|
||||||
import type { PhotoType } from '@/types/sale';
|
import type { PhotoType } from '@/types/sale';
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ export default function NewSaleForm() {
|
|||||||
stopCamera
|
stopCamera
|
||||||
} = useCamera();
|
} = useCamera();
|
||||||
|
|
||||||
|
// Camera test functionality
|
||||||
|
const { testResults, runCameraTest } = useCameraTest();
|
||||||
|
|
||||||
const form = useForm<FormData>({
|
const form = useForm<FormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
vendeur: '',
|
vendeur: '',
|
||||||
@@ -180,21 +184,36 @@ export default function NewSaleForm() {
|
|||||||
|
|
||||||
// Photo capture functions
|
// Photo capture functions
|
||||||
const handlePhotoCapture = async (photoType: PhotoType) => {
|
const handlePhotoCapture = async (photoType: PhotoType) => {
|
||||||
// Vérifier d'abord si les caméras sont supportées
|
console.log('Photo capture requested for:', photoType);
|
||||||
|
|
||||||
|
// Vérifications préliminaires
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
toast({
|
toast({
|
||||||
title: "Caméra non supportée",
|
title: "Navigateur non compatible",
|
||||||
description: "Votre navigateur ne supporte pas l'accès aux caméras",
|
description: "Votre navigateur ne supporte pas la capture photo. Utilisez Chrome, Firefox ou Safari récent.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si nous sommes en HTTPS (requis par certains navigateurs)
|
||||||
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
|
toast({
|
||||||
|
title: "Connexion sécurisée requise",
|
||||||
|
description: "L'accès à la caméra nécessite une connexion HTTPS.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Attempting to start camera...');
|
||||||
await startCamera(photoType);
|
await startCamera(photoType);
|
||||||
|
console.log('Camera started successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Camera start failed:', error);
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur caméra",
|
title: "Erreur d'accès à la caméra",
|
||||||
description: error instanceof Error ? error.message : "Impossible d'accéder à la caméra",
|
description: error instanceof Error ? error.message : "Impossible d'accéder à la caméra",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@@ -233,7 +252,69 @@ export default function NewSaleForm() {
|
|||||||
toast({
|
toast({
|
||||||
title: "Photo supprimée",
|
title: "Photo supprimée",
|
||||||
description: `Photo ${photoType} supprimée`,
|
description: `Photo ${photoType} supprimée`,
|
||||||
variant: "success",
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// File upload fallback
|
||||||
|
const handleFileUpload = (photoType: PhotoType, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast({
|
||||||
|
title: "Type de fichier invalide",
|
||||||
|
description: "Veuillez sélectionner une image",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
||||||
|
toast({
|
||||||
|
title: "Fichier trop volumineux",
|
||||||
|
description: "La taille maximale est de 5MB",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const result = e.target?.result as string;
|
||||||
|
form.setValue(photoType === 'recto' ? 'photoRecto' : 'photoVerso', result);
|
||||||
|
toast({
|
||||||
|
title: "Photo ajoutée",
|
||||||
|
description: `Photo ${photoType} ajoutée avec succès`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Camera test function
|
||||||
|
const handleCameraTest = async () => {
|
||||||
|
toast({
|
||||||
|
title: "Test en cours...",
|
||||||
|
description: "Vérification de l'accès aux caméras",
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await runCameraTest();
|
||||||
|
|
||||||
|
let message = `Test de caméra terminé:\n`;
|
||||||
|
message += `✓ Navigateur compatible: ${results.hasMediaDevices ? 'Oui' : 'Non'}\n`;
|
||||||
|
message += `✓ getUserMedia disponible: ${results.hasGetUserMedia ? 'Oui' : 'Non'}\n`;
|
||||||
|
message += `✓ Caméras détectées: ${results.videoDevicesCount}\n`;
|
||||||
|
message += `✓ Permissions: ${results.permissions}\n`;
|
||||||
|
|
||||||
|
if (results.error) {
|
||||||
|
message += `❌ Erreur: ${results.error}`;
|
||||||
|
} else {
|
||||||
|
message += `✅ Caméra fonctionnelle`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: results.error ? "Test échoué" : "Test réussi",
|
||||||
|
description: message,
|
||||||
|
variant: results.error ? "destructive" : "default",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -519,7 +600,19 @@ export default function NewSaleForm() {
|
|||||||
|
|
||||||
{/* Section Photos */}
|
{/* Section Photos */}
|
||||||
<div className="mt-6 space-y-4">
|
<div className="mt-6 space-y-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700">Photos de la pièce d'identité</h4>
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">Photos de la pièce d'identité</h4>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCameraTest}
|
||||||
|
>
|
||||||
|
<TestTube className="h-4 w-4 mr-2" />
|
||||||
|
Test caméra
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{/* Photo Recto */}
|
{/* Photo Recto */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -538,19 +631,37 @@ export default function NewSaleForm() {
|
|||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
onClick={() => handleRemovePhoto('recto')}
|
onClick={() => handleRemovePhoto('recto')}
|
||||||
>
|
>
|
||||||
✕
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
className="w-full h-32 border-dashed border-2"
|
variant="outline"
|
||||||
onClick={() => handlePhotoCapture('recto')}
|
className="w-full h-24 border-dashed border-2"
|
||||||
>
|
onClick={() => handlePhotoCapture('recto')}
|
||||||
<Camera className="h-6 w-6 mr-2" />
|
>
|
||||||
Prendre photo recto
|
<Camera className="h-5 w-5 mr-2" />
|
||||||
</Button>
|
Prendre photo
|
||||||
|
</Button>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||||
|
onChange={(e) => handleFileUpload('recto', e)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full h-8 text-xs"
|
||||||
|
>
|
||||||
|
<Upload className="h-3 w-3 mr-1" />
|
||||||
|
Ou choisir un fichier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -571,19 +682,37 @@ export default function NewSaleForm() {
|
|||||||
className="absolute top-2 right-2"
|
className="absolute top-2 right-2"
|
||||||
onClick={() => handleRemovePhoto('verso')}
|
onClick={() => handleRemovePhoto('verso')}
|
||||||
>
|
>
|
||||||
✕
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
className="w-full h-32 border-dashed border-2"
|
variant="outline"
|
||||||
onClick={() => handlePhotoCapture('verso')}
|
className="w-full h-24 border-dashed border-2"
|
||||||
>
|
onClick={() => handlePhotoCapture('verso')}
|
||||||
<Camera className="h-6 w-6 mr-2" />
|
>
|
||||||
Prendre photo verso
|
<Camera className="h-5 w-5 mr-2" />
|
||||||
</Button>
|
Prendre photo
|
||||||
|
</Button>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||||
|
onChange={(e) => handleFileUpload('verso', e)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full h-8 text-xs"
|
||||||
|
>
|
||||||
|
<Upload className="h-3 w-3 mr-1" />
|
||||||
|
Ou choisir un fichier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,59 +11,43 @@ export function useCamera() {
|
|||||||
const streamRef = useRef<MediaStream | null>(null);
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
|
|
||||||
const startCamera = useCallback(async (photoType: PhotoType) => {
|
const startCamera = useCallback(async (photoType: PhotoType) => {
|
||||||
|
console.log('Starting camera for photoType:', photoType);
|
||||||
|
|
||||||
|
// Vérifications de base
|
||||||
|
if (!navigator.mediaDevices) {
|
||||||
|
throw new Error('Votre navigateur ne supporte pas l\'accès aux médias. Utilisez Chrome, Firefox ou Safari récent.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error('getUserMedia n\'est pas supporté par votre navigateur.');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Vérification des permissions avant d'essayer
|
console.log('Testing basic camera access...');
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
|
||||||
console.log('Available video devices:', videoDevices.length);
|
|
||||||
|
|
||||||
if (videoDevices.length === 0) {
|
// Test simple d'abord - juste video: true
|
||||||
throw new Error('Aucune caméra détectée sur cet appareil');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration avancée pour tablettes et appareils mobiles
|
|
||||||
const constraints = {
|
|
||||||
video: {
|
|
||||||
facingMode: 'environment', // Caméra arrière par défaut
|
|
||||||
width: { ideal: 1280, max: 1920 }, // Résolution optimale
|
|
||||||
height: { ideal: 720, max: 1080 },
|
|
||||||
aspectRatio: { ideal: 16/9 },
|
|
||||||
frameRate: { ideal: 30 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let stream: MediaStream;
|
let stream: MediaStream;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Essai avec caméra arrière
|
stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
console.log('Basic camera access successful');
|
||||||
console.log('Back camera started successfully');
|
} catch (basicError) {
|
||||||
} catch (backCameraError) {
|
console.error('Basic camera access failed:', basicError);
|
||||||
console.warn('Caméra arrière non disponible, essai avec caméra avant:', backCameraError);
|
|
||||||
|
|
||||||
// Fallback vers caméra avant si arrière non disponible
|
// Vérifier le type d'erreur
|
||||||
const frontConstraints = {
|
if (basicError instanceof Error) {
|
||||||
video: {
|
if (basicError.name === 'NotAllowedError') {
|
||||||
facingMode: 'user',
|
throw new Error('Permission d\'accès à la caméra refusée. Autorisez l\'accès dans votre navigateur.');
|
||||||
width: { ideal: 1280, max: 1920 },
|
} else if (basicError.name === 'NotFoundError') {
|
||||||
height: { ideal: 720, max: 1080 },
|
throw new Error('Aucune caméra trouvée sur cet appareil.');
|
||||||
aspectRatio: { ideal: 16/9 },
|
} else if (basicError.name === 'NotReadableError') {
|
||||||
frameRate: { ideal: 30 }
|
throw new Error('Caméra déjà utilisée par une autre application.');
|
||||||
|
} else if (basicError.name === 'OverconstrainedError') {
|
||||||
|
throw new Error('Contraintes de caméra non supportées.');
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
stream = await navigator.mediaDevices.getUserMedia(frontConstraints);
|
|
||||||
console.log('Front camera started successfully');
|
|
||||||
} catch (frontCameraError) {
|
|
||||||
console.warn('Caméra avant non disponible, essai basique:', frontCameraError);
|
|
||||||
|
|
||||||
// Fallback minimal pour compatibilité maximale
|
|
||||||
stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: true
|
|
||||||
});
|
|
||||||
console.log('Basic camera started successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error(`Erreur d'accès à la caméra: ${basicError}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
streamRef.current = stream;
|
streamRef.current = stream;
|
||||||
@@ -72,20 +56,45 @@ export function useCamera() {
|
|||||||
|
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.srcObject = stream;
|
videoRef.current.srcObject = stream;
|
||||||
|
console.log('Video stream attached to video element');
|
||||||
|
|
||||||
// Attendre que la vidéo soit prête
|
// Attendre que la vidéo soit prête
|
||||||
await new Promise<void>((resolve) => {
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
videoRef.current!.onloadedmetadata = () => {
|
const video = videoRef.current!;
|
||||||
console.log('Video metadata loaded');
|
|
||||||
resolve();
|
const handleLoadedMetadata = () => {
|
||||||
|
console.log('Video metadata loaded, dimensions:', video.videoWidth, 'x', video.videoHeight);
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
video.removeEventListener('error', handleError);
|
||||||
|
resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleError = (error: Event) => {
|
||||||
|
console.error('Video error:', error);
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
video.removeEventListener('error', handleError);
|
||||||
|
reject(new Error('Erreur lors du chargement de la vidéo'));
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
video.addEventListener('error', handleError);
|
||||||
|
|
||||||
|
// Timeout de sécurité
|
||||||
|
setTimeout(() => {
|
||||||
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
video.removeEventListener('error', handleError);
|
||||||
|
resolve(true); // Continuer même si metadata pas encore chargé
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Camera error:', error);
|
console.error('Camera start error:', error);
|
||||||
throw new Error('Impossible d\'accéder à la caméra. Vérifiez les permissions et la disponibilité des caméras.');
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error('Erreur inconnue lors de l\'accès à la caméra');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export function useCameraTest() {
|
||||||
|
const [testResults, setTestResults] = useState<{
|
||||||
|
hasMediaDevices: boolean;
|
||||||
|
hasGetUserMedia: boolean;
|
||||||
|
hasEnumerateDevices: boolean;
|
||||||
|
videoDevicesCount: number;
|
||||||
|
permissions: string;
|
||||||
|
error?: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const runCameraTest = useCallback(async () => {
|
||||||
|
const results = {
|
||||||
|
hasMediaDevices: !!navigator.mediaDevices,
|
||||||
|
hasGetUserMedia: !!(navigator.mediaDevices?.getUserMedia),
|
||||||
|
hasEnumerateDevices: !!(navigator.mediaDevices?.enumerateDevices),
|
||||||
|
videoDevicesCount: 0,
|
||||||
|
permissions: 'unknown' as string,
|
||||||
|
error: undefined as string | undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test 1: Check basic API availability
|
||||||
|
if (!navigator.mediaDevices) {
|
||||||
|
results.error = 'navigator.mediaDevices not available';
|
||||||
|
setTestResults(results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices.getUserMedia) {
|
||||||
|
results.error = 'getUserMedia not available';
|
||||||
|
setTestResults(results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Check permissions
|
||||||
|
try {
|
||||||
|
const permission = await navigator.permissions?.query({ name: 'camera' as PermissionName });
|
||||||
|
results.permissions = permission?.state || 'unknown';
|
||||||
|
} catch (e) {
|
||||||
|
results.permissions = 'query_failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Enumerate devices
|
||||||
|
if (navigator.mediaDevices.enumerateDevices) {
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
results.videoDevicesCount = videoDevices.length;
|
||||||
|
|
||||||
|
if (videoDevices.length === 0) {
|
||||||
|
results.error = 'No video devices found';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
results.error = `Device enumeration failed: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Try basic camera access
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||||
|
stream.getTracks().forEach(track => track.stop()); // Clean up immediately
|
||||||
|
results.error = undefined; // Camera works!
|
||||||
|
} catch (e: any) {
|
||||||
|
results.error = `Camera access failed: ${e.name} - ${e.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
results.error = `Test failed: ${e.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestResults(results);
|
||||||
|
return results;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
testResults,
|
||||||
|
runCameraTest
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -259,6 +259,11 @@ Preferred communication style: Simple, everyday language.
|
|||||||
- ✅ Enhanced error handling with device-specific camera availability checks
|
- ✅ Enhanced error handling with device-specific camera availability checks
|
||||||
- ✅ Camera permission detection and clear error messages for troubleshooting
|
- ✅ Camera permission detection and clear error messages for troubleshooting
|
||||||
- ✅ Support for both front and rear cameras on tablets and mobile devices
|
- ✅ Support for both front and rear cameras on tablets and mobile devices
|
||||||
|
- ✅ Camera diagnostic system with detailed test functionality
|
||||||
|
- ✅ File upload fallback when camera access fails
|
||||||
|
- ✅ Comprehensive error handling with specific messages for different failure types
|
||||||
|
- ✅ HTTPS requirement detection for secure camera access
|
||||||
|
- ✅ Multiple capture methods: camera → file upload for maximum compatibility
|
||||||
|
|
||||||
### Production Deployment System (January 11, 2025)
|
### Production Deployment System (January 11, 2025)
|
||||||
- ✅ Complete production deployment configuration with Docker Compose
|
- ✅ Complete production deployment configuration with Docker Compose
|
||||||
|
|||||||
Reference in new issue
Block a user