import { Loader2 } from "lucide-react" import { useEffect, useMemo, useRef, useState } from "react" import { Link, Navigate, useNavigate } from "react-router-dom" import { toast } from "sonner" import { loginWithOtp, sendOtp } from "../../api/users" import { Button } from "../../components/ui/button" import { useAuthFlow } from "../../context/AuthFlowContext" import { useTranslation } from "../../hooks/useTranslation" import { AuthOtpInput } from "./AuthOtpInput" import { AuthPanel } from "./AuthPanel" import { completeAuthentication, formatCooldown, getApiErrorMessage, getOtpRemainingSeconds, handleThrottleError, } from "./utils" export function LoginOtpPage() { const navigate = useNavigate() const { t, lang } = useTranslation() const { state, setCode, setCooldown, clearCooldown, setOtpDelivery, clearOtpDelivery, } = useAuthFlow() const isRtl = lang === "fa" const autoSendStartedRef = useRef(false) const activeSubmitCodeRef = useRef("") const lastFailedCodeRef = useRef("") const [isSendingOtp, setIsSendingOtp] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [now, setNow] = useState(Date.now()) if (!state.login.mobile) { return } useEffect(() => { if (!state.login.otpExpiresAt || getOtpRemainingSeconds(state.login.otpExpiresAt) <= 0) { return } const timer = window.setInterval(() => { setNow(Date.now()) }, 1000) return () => window.clearInterval(timer) }, [state.login.otpExpiresAt]) const sendLoginOtp = async () => { setIsSendingOtp(true) try { const response = await sendOtp(state.login.mobile, "login") clearCooldown("loginOtpSend") clearCooldown("loginOtpVerify") lastFailedCodeRef.current = "" setCode("login", "") setOtpDelivery("login", response.expires_in_seconds) setNow(Date.now()) toast.success(t.login.toasts.verifySent) } catch (error) { clearOtpDelivery("login") if ( !handleThrottleError({ error, cooldownKey: "loginOtpSend", setCooldown, formatTime: (seconds) => formatCooldown(seconds, isRtl), throttleCopy: t.login.throttle, }) ) { toast.error(getApiErrorMessage(error, t.login.toasts.failedOtp)) } } finally { setIsSendingOtp(false) } } useEffect(() => { if (!state.login.pendingOtpSend || autoSendStartedRef.current) { return } autoSendStartedRef.current = true void sendLoginOtp() }, [state.login.pendingOtpSend]) const otpRemainingSeconds = state.login.otpExpiresAt ? Math.max(0, Math.ceil((state.login.otpExpiresAt - now) / 1000)) : 0 const throttleAlert = useMemo(() => { if (state.cooldowns.loginOtpVerify > 0) { const formatted = formatCooldown(state.cooldowns.loginOtpVerify, isRtl) return { title: t.login.throttle.title, description: t.login.throttle.otpLoginMessage(formatted), } } if (state.cooldowns.loginOtpSend > 0) { const formatted = formatCooldown(state.cooldowns.loginOtpSend, isRtl) return { title: t.login.throttle.title, description: t.login.throttle.otpSendMessage(formatted), } } return null }, [isRtl, state.cooldowns.loginOtpSend, state.cooldowns.loginOtpVerify, t.login.throttle]) const expiryMessage = otpRemainingSeconds > 0 ? t.login.otpExpiresIn(formatCooldown(otpRemainingSeconds, isRtl)) : state.login.otpExpiresAt ? t.login.otpExpired : null const submitCode = async (code: string) => { if (code.length !== 5) { toast.error(t.login.toasts.enterOtp) return } if (isSendingOtp || isSubmitting || code === activeSubmitCodeRef.current) { return } if (code === lastFailedCodeRef.current) { toast.error(t.login.toasts.invalidOtp) return } activeSubmitCodeRef.current = code setIsSubmitting(true) try { const data = await loginWithOtp(state.login.mobile, code) clearCooldown("loginOtpVerify") activeSubmitCodeRef.current = "" lastFailedCodeRef.current = "" completeAuthentication({ access: data.access, refresh: data.refresh, successMessage: t.login.toasts.successLogin, redirectTo: "/profile", navigate, }) } catch (error) { if ( !handleThrottleError({ error, cooldownKey: "loginOtpVerify", setCooldown, formatTime: (seconds) => formatCooldown(seconds, isRtl), throttleCopy: t.login.throttle, }) ) { lastFailedCodeRef.current = code toast.error(getApiErrorMessage(error, t.login.toasts.invalidOtp)) } } finally { activeSubmitCodeRef.current = "" setIsSubmitting(false) } } const handleResend = async () => { autoSendStartedRef.current = false await sendLoginOtp() } const isBusy = isSendingOtp || isSubmitting const isRepeatedInvalidCode = state.login.code.length === 5 && state.login.code === lastFailedCodeRef.current return ( { event.preventDefault() void submitCode(state.login.code) }} className="grid gap-4" > setCode("login", value)} onComplete={(value) => void submitCode(value)} /> {expiryMessage ? ( {expiryMessage} ) : null} 0} > {(isSubmitting || isSendingOtp) && } {isSubmitting ? t.login.verifyingOtp : isSendingOtp ? t.login.sendingOtp : t.login.verifyAndContinue} 0 || state.cooldowns.loginOtpSend > 0} onClick={() => void handleResend()} > {state.cooldowns.loginOtpSend > 0 ? t.login.throttle.countdownLabel(formatCooldown(state.cooldowns.loginOtpSend, isRtl)) : t.login.resendOtp} {t.login.usePasswordInstead} ) }
{expiryMessage}