import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useState, useRef, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import showToast from "../../services/toastService"; import { AuthWrapper } from "./AuthWrapper"; import { OTP_EXPIRY_SECONDS } from "../../utils/constants"; import AuthRepository from "../../repositories/AuthRepository"; const otpSchema = z.object({ otp1: z.string().min(1, "Required"), otp2: z.string().min(1, "Required"), otp3: z.string().min(1, "Required"), otp4: z.string().min(1, "Required"), }); const LoginWithOtp = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); const [timeLeft, setTimeLeft] = useState(0); const inputRefs = useRef([]); const { register, handleSubmit, formState: { errors, isSubmitted }, getValues, setValue, trigger, } = useForm({ resolver: zodResolver(otpSchema), }); const onSubmit = async (data) => { const finalOtp = data.otp1 + data.otp2 + data.otp3 + data.otp4; const username = localStorage.getItem("otpUsername"); setLoading(true); try { let requestedData = { email: username, otp: finalOtp } const response = await AuthRepository.verifyOTP(requestedData) localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("refreshToken", response.data.refreshToken); setLoading(false); localStorage.removeItem("otpUsername"); localStorage.removeItem("otpSentTime"); navigate("/"); } catch (err) { showToast("Invalid or expired OTP.", "error"); setLoading(false); } }; const formatTime = (seconds) => { const min = Math.floor(seconds / 60).toString().padStart(2, "0"); const sec = (seconds % 60).toString().padStart(2, "0"); return `${min}:${sec}`; }; // Time Logic for OTP expiry useEffect(() => { const otpSentTime = localStorage.getItem("otpSentTime"); const now = Date.now(); if (otpSentTime) { const elapsed = Math.floor((now - Number(otpSentTime)) / 1000); //in seconds const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0); //prevent negatives setTimeLeft(remaining); } }, []); useEffect(() => { if (timeLeft <= 0) return; const timer = setInterval(() => { setTimeLeft((prev) => { if (prev <= 1) { clearInterval(timer); localStorage.removeItem("otpSentTime"); localStorage.removeItem("otpUsername"); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(timer); }, [timeLeft]); // Handle Paste Event const handlePaste = (e) => { e.preventDefault(); const pastedData = e.clipboardData.getData("text/plain").trim(); if (pastedData.match(/^\d{4}$/)) { for (let i = 0; i < pastedData.length; i++) { setValue(`otp${i + 1}`, pastedData[i], { shouldValidate: true }); if (inputRefs.current[i + 1]) { inputRefs.current[i + 1].focus(); } } trigger(["otp1", "otp2", "otp3", "otp4"]); } else { showToast("Invalid OTP format pasted. Please enter 4 digits") for (let i = 0; i < 4; i++) { setValue(`otp${i + 1}`, "") } } } return ( //

Verify Your OTP

Please enter the 4-digit code sent to your email.

{[1, 2, 3, 4].map((num, idx) => { const { ref, onChange, ...rest } = register(`otp${num}`); return ( { inputRefs.current[idx] = el; ref(el); }} onChange={(e) => { const val = e.target.value; onChange(e); if (/^\d$/.test(val) && idx < 3) { inputRefs.current[idx + 1]?.focus(); } else if (val === "" && idx > 0) { inputRefs.current[idx - 1]?.focus(); } }} onKeyDown={(e) => { if ( e.key === "Backspace" && !getValues()[`otp${num}`] && idx > 0 ) { inputRefs.current[idx - 1]?.focus(); } }} onPaste={idx === 0 ? handlePaste : undefined} style={{ width: "40px", height: "40px", fontSize: "15px" }} {...rest} /> ); })}
{isSubmitted && Object.values(errors).some((e) => e?.message) && (
Please fill all four digits.
)} {timeLeft > 0 ? (

This OTP will expire in {formatTime(timeLeft)}

) : (

OTP has expired. Please request a new one.

navigate('/auth/login')}>Try Again
)}
//
); }; export default LoginWithOtp;