import { useRef, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import DocumentUploadModal from './DocumentUploadModal'; import { useCamera } from '../features/faceScan/hooks/useCamera'; import { useFaceScan } from '../features/faceScan/hooks/useFaceScan'; import { useKycContext } from '../contexts/KycContext'; import { Toast } from '../components/Toast'; import { COMPLETED_STEPS } from '../services/kycApiService'; import '../index.css'; interface FaceScanModalProps { onClose: () => void; onComplete?: (capturedImage: string) => void; } function FaceScanModal({ onComplete }: FaceScanModalProps) { const faceCanvasRef = useRef(null); const navigate = useNavigate(); const { apiService } = useKycContext(); const [sessionError, setSessionError] = useState(null); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' | 'warning' } | null>(null); const [showRetryButton, setShowRetryButton] = useState(false); const [isRetrying, setIsRetrying] = useState(false); const [kycCompleted, setKycCompleted] = useState(false); const { videoRef, cameraReady, stopCamera } = useCamera(); const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, { onFaceUpload: async (blob: Blob) => { if (!apiService) { throw new Error('API service not initialized'); } try { await apiService.uploadFaceScan(blob); } catch (error: any) { const errorMessage = error?.message || ''; const errorData = (error as any)?.errorData || {}; const statusCode = (error as any)?.statusCode; // Check for "Face already registered" error in various formats // The API returns: { success: false, statusCode: 500, message: "Face already registered", errorData: {...} } const isFaceAlreadyRegistered = errorMessage.includes('Face already registered') || errorMessage.includes('already registered') || errorData?.message?.includes('Face already registered') || (statusCode === 500 && errorMessage.includes('Face already registered')); if (isFaceAlreadyRegistered) { setShowRetryButton(true); setToast({ message: 'Face already registered. Click Retry to register again.', type: 'warning', }); setState(prev => ({ ...prev, loading: false, allStepsCompleted: false, showDocumentUpload: false })); // Throw a special error to prevent continuing to document upload const faceRegisteredError = new Error('FACE_ALREADY_REGISTERED'); (faceRegisteredError as any).isFaceAlreadyRegistered = true; throw faceRegisteredError; } throw error; } }, onFaceCaptureComplete: (imageData: string) => { if (onComplete) { onComplete(imageData); } }, }); useEffect(() => { const checkSession = async () => { if (!apiService) return; try { const statusResponse = await apiService.getSessionStatus(); const { completed_steps, next_step, status } = statusResponse.data; // Check if session is active if (status !== 'ACTIVE') { throw new Error('Session expired or inactive'); } // Check if KYC is completed if (status === 'COMPLETED' || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) { setKycCompleted(true); return; } // If face_scan is already completed, skip to document upload if (completed_steps.includes(COMPLETED_STEPS.FACE)) { setState(prev => ({ ...prev, showDocumentUpload: true })); return; } // If next_step is not face_scan, redirect accordingly if (next_step !== COMPLETED_STEPS.FACE && next_step !== COMPLETED_STEPS.INITIATED) { if (next_step === COMPLETED_STEPS.DOCS) { setState(prev => ({ ...prev, showDocumentUpload: true })); return; } } setSessionError(null); } catch (error: any) { const message = error.message || 'Session expired or inactive'; setSessionError(message); setTimeout(() => { navigate('/qr', { replace: true }); }, 2000); } }; checkSession(); }, [apiService, navigate, setState]); useEffect(() => { setState(prev => ({ ...prev, cameraReady })); }, [cameraReady, setState]); useEffect(() => { if (cameraReady && apiService) { const config = apiService.getConfig(); if (config) { console.log('=== Camera Opened ==='); console.log('Session ID:', config.sessionId); console.log('Server Key:', config.serverKey); console.log('API Base URL:', config.apiBaseUrl); console.log('Device Type:', config.deviceType || 'auto-detected'); apiService.getSessionStatus() .then((statusResponse) => { console.log('=== Session Status API Response ==='); console.log('Full Response:', statusResponse); console.log('Session Status:', statusResponse.data?.status); console.log('Session ID:', statusResponse.data?.session_id); console.log('Completed Steps:', statusResponse.data?.completed_steps); console.log('Next Step:', statusResponse.data?.next_step); }) .catch((error) => { console.error('=== Session Status API Error ==='); console.error('Error fetching session status:', error); }); } } }, [cameraReady, apiService]); const handleRetry = async () => { if (!apiService || isRetrying) return; setIsRetrying(true); setShowRetryButton(false); setToast(null); try { await apiService.retrySession(); // Reset face scan state stopCamera(); setState({ cameraReady: false, livenessStage: 'CENTER', livenessReady: false, livenessFailed: false, modelLoading: true, modelLoaded: false, livenessInstruction: 'Look straight at the camera', loading: false, allStepsCompleted: false, capturedImage: null, showDocumentUpload: false, }); refs.centerHold.current = 0; refs.leftHold.current = 0; refs.rightHold.current = 0; refs.snapTriggered.current = false; refs.lastResultsAt.current = 0; refs.modelLoaded.current = false; refs.livenessFailed.current = false; // Reload to restart face scan setTimeout(() => { window.location.reload(); }, 500); } catch (error: any) { setIsRetrying(false); setShowRetryButton(true); setToast({ message: error?.message || 'Retry failed. Please try again.', type: 'error', }); } }; const handleRestart = () => { stopCamera(); setState({ cameraReady: false, livenessStage: 'CENTER', livenessReady: false, livenessFailed: false, modelLoading: true, modelLoaded: false, livenessInstruction: 'Look straight at the camera', loading: false, allStepsCompleted: false, capturedImage: null, showDocumentUpload: false, }); refs.centerHold.current = 0; refs.leftHold.current = 0; refs.rightHold.current = 0; refs.snapTriggered.current = false; refs.lastResultsAt.current = 0; refs.modelLoaded.current = false; refs.livenessFailed.current = false; setTimeout(() => { window.location.reload(); }, 100); }; // Show KYC completion message if (kycCompleted) { return (

KYC Completed

All steps have been completed successfully.

Please return to your desktop to continue.

); } if (state.showDocumentUpload) { return ( { if (onComplete && state.capturedImage) { onComplete(state.capturedImage); } }} /> ); } if (sessionError) { return (

Session Expired

{sessionError}

Redirecting to QR code page...

); } return ( <> {toast && ( setToast(null)} duration={6000} /> )}

Capture Face

{!state.modelLoading && state.modelLoaded && !state.livenessFailed && (
Face detection model loaded.
)} {state.modelLoading && !state.livenessFailed && (
Loading face detection model...
)}
{!state.livenessFailed && (
Liveness Check
{state.livenessInstruction}
1. Look Straight
2. Turn your face right
3. Turn your face left
)} {showRetryButton && ( )}
); } export default FaceScanModal;