Files
qlockify-frontend-deployment/src/pages/auth/SignupOtpPage.tsx

164 lines
4.8 KiB
TypeScript

import { Loader2 } from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import { Navigate, useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { 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 { formatCooldown, getApiErrorMessage, getOtpRemainingSeconds, handleThrottleError } from "./utils"
export function SignupOtpPage() {
const navigate = useNavigate()
const { t, lang } = useTranslation()
const {
state,
setCode,
setCooldown,
clearCooldown,
setOtpDelivery,
clearOtpDelivery,
} = useAuthFlow()
const isRtl = lang === "fa"
const autoSendStartedRef = useRef(false)
const [isSendingOtp, setIsSendingOtp] = useState(false)
const [isContinuing, setIsContinuing] = useState(false)
const [now, setNow] = useState(Date.now())
if (!state.signup.mobile) {
return <Navigate to="/auth/signup" replace />
}
useEffect(() => {
if (!state.signup.otpExpiresAt || getOtpRemainingSeconds(state.signup.otpExpiresAt) <= 0) {
return
}
const timer = window.setInterval(() => {
setNow(Date.now())
}, 1000)
return () => window.clearInterval(timer)
}, [state.signup.otpExpiresAt])
const sendSignupOtp = async () => {
setIsSendingOtp(true)
try {
const response = await sendOtp(state.signup.mobile, "register")
clearCooldown("signupOtpSend")
setCode("signup", "")
setOtpDelivery("signup", response.expires_in_seconds)
setNow(Date.now())
toast.success(t.login.toasts.verifySent)
} catch (error) {
clearOtpDelivery("signup")
if (
!handleThrottleError({
error,
cooldownKey: "signupOtpSend",
setCooldown,
formatTime: (seconds) => formatCooldown(seconds, isRtl),
throttleCopy: t.login.throttle,
})
) {
toast.error(getApiErrorMessage(error, t.login.toasts.failedOtp))
}
} finally {
setIsSendingOtp(false)
}
}
useEffect(() => {
if (!state.signup.pendingOtpSend || autoSendStartedRef.current) {
return
}
autoSendStartedRef.current = true
void sendSignupOtp()
}, [state.signup.pendingOtpSend])
const otpRemainingSeconds = state.signup.otpExpiresAt
? Math.max(0, Math.ceil((state.signup.otpExpiresAt - now) / 1000))
: 0
const alert = useMemo(() => {
if (state.cooldowns.signupOtpSend <= 0) {
return null
}
const formatted = formatCooldown(state.cooldowns.signupOtpSend, isRtl)
return {
title: t.login.throttle.title,
description: t.login.throttle.otpSendMessage(formatted),
}
}, [isRtl, state.cooldowns.signupOtpSend, t.login.throttle])
const expiryMessage =
otpRemainingSeconds > 0
? t.login.otpExpiresIn(formatCooldown(otpRemainingSeconds, isRtl))
: state.signup.otpExpiresAt
? t.login.otpExpired
: null
const continueToPassword = async (code: string) => {
if (code.length !== 5) {
toast.error(t.login.toasts.enterOtp)
return
}
setIsContinuing(true)
setCode("signup", code)
navigate("/auth/signup/password")
}
const isBusy = isSendingOtp || isContinuing
return (
<AuthPanel
title={t.login.signupVerifyTitle}
description={t.login.sentCodeDesc(state.signup.mobile)}
alert={alert}
>
<form
onSubmit={(event) => {
event.preventDefault()
void continueToPassword(state.signup.code)
}}
className="grid gap-4"
>
<AuthOtpInput
id="signup-otp"
value={state.signup.code}
disabled={isBusy}
onChange={(value) => setCode("signup", value)}
onComplete={(value) => void continueToPassword(value)}
/>
{expiryMessage ? (
<p className="text-center text-sm text-slate-500 dark:text-slate-400">{expiryMessage}</p>
) : null}
<Button type="submit" className="h-11 w-full" disabled={isBusy}>
{(isSendingOtp || isContinuing) && <Loader2 className="me-2 h-4 w-4 animate-spin" />}
{isSendingOtp ? t.login.sendingOtp : t.login.continueToPassword}
</Button>
<Button
type="button"
variant="outline"
className="h-11 w-full"
disabled={isBusy || otpRemainingSeconds > 0 || state.cooldowns.signupOtpSend > 0}
onClick={() => void sendSignupOtp()}
>
{state.cooldowns.signupOtpSend > 0
? t.login.throttle.countdownLabel(formatCooldown(state.cooldowns.signupOtpSend, isRtl))
: t.login.resendOtp}
</Button>
</form>
</AuthPanel>
)
}