// File: monorepo/web/maplefile-frontend/src/pages/Anonymous/Login/RequestOTT.jsx // UIX version - Step 1: Request One-Time Token import { useState, useEffect, useRef, useCallback } from "react"; import { useNavigate, Link } from "react-router"; import { useServices } from "../../../services/Services"; import { Card, Input, Button, Alert, GDPRFooter, ProgressIndicator, Divider, NavBar, AuthPageHeader } from "../../../components/UIX"; import { useUIXTheme } from "../../../components/UIX/themes/useUIXTheme"; import { ArrowRightIcon, ShieldCheckIcon, SparklesIcon, } from "@heroicons/react/24/outline"; const RequestOTT = () => { const navigate = useNavigate(); const { authManager } = useServices(); const { getThemeClasses } = useUIXTheme(); // Refs for cleanup and preventing state updates after unmount const isMountedRef = useRef(true); const timeoutRef = useRef(null); const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(""); const [generalError, setGeneralError] = useState(""); const [fieldErrors, setFieldErrors] = useState({}); // Cleanup effect - prevents memory leaks useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; // Clear any pending timeouts if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; }, []); const handleSubmit = useCallback(async (e) => { e.preventDefault(); // Prevent state updates if component is unmounted if (!isMountedRef.current) return; setLoading(true); setGeneralError(""); setFieldErrors({}); setMessage(""); try { // Validate services if (!authManager) { throw new Error( "Authentication service not available. Please refresh the page.", ); } const trimmedEmail = email.trim().toLowerCase(); // Log only in development if (import.meta.env.DEV) { console.log("[RequestOTT] Requesting OTT via AuthManager"); } // Store email BEFORE making the request sessionStorage.setItem("loginEmail", trimmedEmail); // Make the OTT request let response; if (typeof authManager.requestOTT === "function") { response = await authManager.requestOTT(trimmedEmail); } else if (typeof authManager.requestOTP === "function") { response = await authManager.requestOTP({ email: trimmedEmail }); } else { // Generic error message - don't expose implementation details throw new Error("Authentication service is temporarily unavailable."); } // Check if component is still mounted before updating state if (!isMountedRef.current) return; setMessage(response.message || "Verification code sent successfully!"); // Store email in localStorage via authManager if available try { if ( authManager.setCurrentUserEmail && typeof authManager.setCurrentUserEmail === "function" ) { authManager.setCurrentUserEmail(trimmedEmail); } } catch (storageError) { // Silently handle storage errors - non-critical if (import.meta.env.DEV) { console.warn("[RequestOTT] Could not store email via authManager:", storageError); } } // Wait a moment to show the success message, then navigate // Store timeout ID for cleanup timeoutRef.current = setTimeout(() => { if (isMountedRef.current) { navigate("/login/verify-ott"); } }, 2000); } catch (err) { // Check if component is still mounted before updating state if (!isMountedRef.current) return; // Log only in development if (import.meta.env.DEV) { console.error("[RequestOTT] OTT request failed:", err); } // Parse RFC 9457 error response if (err.fieldErrors && Object.keys(err.fieldErrors).length > 0) { setFieldErrors(err.fieldErrors); setGeneralError("Please fix the following issues:"); } else if (err.message) { // Sanitize error messages - remove technical details let userMessage = err.message; // Remove technical prefixes userMessage = userMessage.replace(/^Failed to request OTT:\s*/i, ''); userMessage = userMessage.replace(/^Request failed:\s*/i, ''); userMessage = userMessage.replace(/^Error:\s*/i, ''); userMessage = userMessage.replace(/^Authentication service.*$/i, 'Service temporarily unavailable.'); // Replace generic validation message with friendlier one if (userMessage.includes('One or more fields failed validation')) { userMessage = 'Please check your email address and try again.'; } // Prevent information disclosure - sanitize technical errors if (userMessage.includes('method not found') || userMessage.includes('not defined') || userMessage.includes('undefined')) { userMessage = 'Service temporarily unavailable. Please try again later.'; } setGeneralError(userMessage); } else { setGeneralError("Could not send login code. Please try again."); } } finally { // Check if component is still mounted before updating state if (isMountedRef.current) { setLoading(false); } } }, [email, authManager, navigate]); // Include all dependencies const handleKeyPress = useCallback((e) => { if (e.key === "Enter" && email && !loading) { handleSubmit(e); } }, [email, loading, handleSubmit]); // Include all dependencies return (