Enable Paste of OTP into OTP Input Field #204

Merged
vikas.nale merged 1 commits from Kartik_Bug#489 into Issue_Jun_2W 2025-06-12 05:31:10 +00:00

View File

@ -17,11 +17,8 @@ const otpSchema = z.object({
const LoginWithOtp = () => { const LoginWithOtp = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [timeLeft, setTimeLeft] = useState(0); const [timeLeft, setTimeLeft] = useState(0);
const inputRefs = useRef([]); const inputRefs = useRef([]);
const { const {
@ -29,6 +26,7 @@ const LoginWithOtp = () => {
handleSubmit, handleSubmit,
formState: { errors, isSubmitted }, formState: { errors, isSubmitted },
getValues, getValues,
setValue
} = useForm({ } = useForm({
resolver: zodResolver(otpSchema), resolver: zodResolver(otpSchema),
}); });
@ -39,25 +37,23 @@ const LoginWithOtp = () => {
setLoading(true); setLoading(true);
try { try {
let requestedData = { const requestedData = {
email: username, email: username,
otp:finalOtp otp: finalOtp,
} };
const response = await AuthRepository.verifyOTP( requestedData ) const response = await AuthRepository.verifyOTP(requestedData);
localStorage.setItem("jwtToken", response.data.token); localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken); localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading( false );
localStorage.removeItem("otpUsername"); localStorage.removeItem("otpUsername");
localStorage.removeItem("otpSentTime"); localStorage.removeItem("otpSentTime");
navigate("/dashboard"); navigate("/dashboard");
} catch (err) { } catch (err) {
showToast("Invalid or expired OTP.", "error"); showToast("Invalid or expired OTP.", "error");
} finally {
setLoading(false); setLoading(false);
} }
}; };
const formatTime = (seconds) => { const formatTime = (seconds) => {
const min = Math.floor(seconds / 60).toString().padStart(2, "0"); const min = Math.floor(seconds / 60).toString().padStart(2, "0");
const sec = (seconds % 60).toString().padStart(2, "0"); const sec = (seconds % 60).toString().padStart(2, "0");
@ -69,11 +65,12 @@ useEffect(() => {
const now = Date.now(); const now = Date.now();
if (otpSentTime) { if (otpSentTime) {
const elapsed = Math.floor((now - Number(otpSentTime)) / 1000); // in seconds const elapsed = Math.floor((now - Number(otpSentTime)) / 1000);
const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0); // prevent negatives const remaining = Math.max(OTP_EXPIRY_SECONDS - elapsed, 0);
setTimeLeft(remaining); setTimeLeft(remaining);
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (timeLeft <= 0) return; if (timeLeft <= 0) return;
@ -92,7 +89,6 @@ useEffect(() => {
return () => clearInterval(timer); return () => clearInterval(timer);
}, [timeLeft]); }, [timeLeft]);
return ( return (
<AuthWrapper> <AuthWrapper>
<div className="otp-verification-wrapper"> <div className="otp-verification-wrapper">
@ -100,7 +96,7 @@ useEffect(() => {
<p className="mb-4">Please enter the 4-digit code sent to your email.</p> <p className="mb-4">Please enter the 4-digit code sent to your email.</p>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex justify-content-center gap-6 mb-3"> <div className="d-flex justify-content-center gap-3 mb-3">
{[1, 2, 3, 4].map((num, idx) => { {[1, 2, 3, 4].map((num, idx) => {
const { ref, onChange, ...rest } = register(`otp${num}`); const { ref, onChange, ...rest } = register(`otp${num}`);
@ -132,6 +128,24 @@ useEffect(() => {
inputRefs.current[idx - 1]?.focus(); inputRefs.current[idx - 1]?.focus();
} }
}} }}
onPaste={
idx === 0
? (e) => {
const pasteData = e.clipboardData.getData("Text").trim();
if (/^\d{4}$/.test(pasteData)) {
e.preventDefault();
pasteData.split("").forEach((char, i) => {
const ref = inputRefs.current[i];
if (ref) {
ref.value = char;
setValue(`otp${i + 1}`, char);
}
});
inputRefs.current[3]?.focus();
}
}
: undefined
}
style={{ width: "40px", height: "40px", fontSize: "15px" }} style={{ width: "40px", height: "40px", fontSize: "15px" }}
{...rest} {...rest}
/> />
@ -140,10 +154,7 @@ useEffect(() => {
</div> </div>
{isSubmitted && Object.values(errors).some((e) => e?.message) && ( {isSubmitted && Object.values(errors).some((e) => e?.message) && (
<div <div className="text-danger text-center mb-3" style={{ fontSize: "12px" }}>
className="text-danger text-center mb-3"
style={{ fontSize: "12px" }}
>
Please fill all four digits. Please fill all four digits.
</div> </div>
)} )}
@ -157,23 +168,21 @@ useEffect(() => {
</button> </button>
{timeLeft > 0 ? ( {timeLeft > 0 ? (
<p <p className="text-center text-muted mt-2" style={{ fontSize: "14px" }}>
className="text-center text-muted mt-2"
style={{ fontSize: "14px" }}
>
This OTP will expire in <strong>{formatTime(timeLeft)}</strong> This OTP will expire in <strong>{formatTime(timeLeft)}</strong>
</p> </p>
) : ( ) : (
<div> <div>
<p <p className="text-center text-danger mt-2 small-text m-0">
className="text-center text-danger mt-2 text small-text m-0"
>
OTP has expired. Please request a new one. OTP has expired. Please request a new one.
</p> </p>
<a className="text-primary cursor-pointer" onClick={()=>navigate('/auth/login')}>Try Again</a> <a
className="text-primary cursor-pointer"
onClick={() => navigate("/auth/login")}
>
Try Again
</a>
</div> </div>
)} )}
</form> </form>
</div> </div>