import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { AlertTriangle, ArrowLeft, ArrowRight, CheckCircle2, Command, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { SettingsMenu } from "../components/SettingsMenu"; import { ApiError } from "../api/client"; import { completeGoogleOAuthSignup, getGoogleOAuthFlow, sendGoogleOAuthClaimOtp, startGoogleLogin, verifyGoogleOAuthClaim, type GoogleOAuthFlowResponse, } from "../api/users"; import { useTranslation } from "../hooks/useTranslation"; import { setSessionTokens } from "../lib/session"; type GoogleStep = "loading" | "collect_mobile" | "claim_required" | "error"; type CooldownKey = "otpSend" | "otpVerify"; const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]; const toPersianDigits = (value: string) => value.replace(/\d/g, (digit) => PERSIAN_DIGITS[Number.parseInt(digit, 10)] ?? digit); export default function GoogleAuthCallback() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { t, lang } = useTranslation(); const isRtl = lang === "fa"; const flow = searchParams.get("flow") ?? ""; const [step, setStep] = useState("loading"); const [loading, setLoading] = useState(false); const [mobile, setMobile] = useState(""); const [otpCode, setOtpCode] = useState(""); const [googleEmail, setGoogleEmail] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const [cooldowns, setCooldowns] = useState>({ otpSend: 0, otpVerify: 0, }); useEffect(() => { if (!Object.values(cooldowns).some((value) => value > 0)) { return; } const timer = window.setInterval(() => { setCooldowns((current) => ({ otpSend: Math.max(0, current.otpSend - 1), otpVerify: Math.max(0, current.otpVerify - 1), })); }, 1000); return () => window.clearInterval(timer); }, [cooldowns]); const localizeDigits = (value: string) => (isRtl ? toPersianDigits(value) : value); const formatCooldown = (seconds: number) => { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; const formatted = minutes > 0 ? `${minutes}:${remainingSeconds.toString().padStart(2, "0")}` : `${remainingSeconds}s`; return localizeDigits(formatted); }; const setCooldown = (key: CooldownKey, seconds: number) => { setCooldowns((current) => ({ ...current, [key]: Math.max(current[key], seconds), })); }; const handleAuthenticated = (payload: Extract) => { setSessionTokens(payload.access, payload.refresh); toast.success(t.login.toasts.successLogin); navigate("/profile", { replace: true }); }; const applyFlowPayload = (payload: GoogleOAuthFlowResponse) => { if (payload.status === "authenticated") { handleAuthenticated(payload); return; } if (payload.status === "collect_mobile") { setGoogleEmail(payload.email); setStep("collect_mobile"); return; } if (payload.status === "claim_required") { setMobile(payload.mobile); setStep("claim_required"); } }; const handleThrottleError = (error: unknown, key: CooldownKey) => { if (!(error instanceof ApiError) || error.code !== "throttled") { return false; } const seconds = Math.max(1, error.retryAfterSeconds ?? 0); const formattedTime = formatCooldown(seconds); setCooldown(key, seconds); const message = key === "otpSend" ? t.login.throttle.otpSendMessage(formattedTime) : t.login.throttle.otpLoginMessage(formattedTime); toast.error(message, { description: t.login.throttle.countdownLabel(formattedTime), }); return true; }; useEffect(() => { if (!flow) { setErrorMessage(t.login.google.missingFlow); setStep("error"); return; } let cancelled = false; const loadFlow = async () => { setLoading(true); try { const payload = await getGoogleOAuthFlow(flow); if (!cancelled) { applyFlowPayload(payload); } } catch (error) { if (!cancelled) { setErrorMessage(error instanceof Error ? error.message : t.login.google.loadFailed); setStep("error"); } } finally { if (!cancelled) { setLoading(false); } } }; loadFlow(); return () => { cancelled = true; }; }, [flow]); const handleCompleteSignup = async (event: React.FormEvent) => { event.preventDefault(); if (!mobile) { toast.error(t.login.toasts.enterMobile); return; } setLoading(true); try { const payload = await completeGoogleOAuthSignup(flow, mobile); applyFlowPayload(payload); if (payload.status === "claim_required") { toast.success(t.login.google.claimOtpSent); } } catch (error) { toast.error(error instanceof Error ? error.message : t.login.google.completeFailed); } finally { setLoading(false); } }; const handleResendClaimOtp = async () => { setLoading(true); try { await sendGoogleOAuthClaimOtp(flow); setCooldowns((current) => ({ ...current, otpSend: 0 })); toast.success(t.login.google.claimOtpSent); } catch (error) { if (!handleThrottleError(error, "otpSend")) { toast.error(error instanceof Error ? error.message : t.login.toasts.failedOtp); } } finally { setLoading(false); } }; const handleVerifyClaim = async (event: React.FormEvent) => { event.preventDefault(); if (!otpCode) { toast.error(t.login.toasts.enterOtp); return; } setLoading(true); try { const payload = await verifyGoogleOAuthClaim(flow, otpCode); setCooldowns((current) => ({ ...current, otpVerify: 0 })); applyFlowPayload(payload); } catch (error) { if (!handleThrottleError(error, "otpVerify")) { toast.error(error instanceof Error ? error.message : t.login.toasts.invalidOtp); } } finally { setLoading(false); } }; const activeWarning = useMemo(() => { if (cooldowns.otpSend > 0) { return t.login.throttle.otpSendMessage(formatCooldown(cooldowns.otpSend)); } if (cooldowns.otpVerify > 0) { return t.login.throttle.otpLoginMessage(formatCooldown(cooldowns.otpVerify)); } return null; }, [cooldowns, isRtl, lang]); const BackIcon = isRtl ? ArrowRight : ArrowLeft; return (
{t.title || "Qlockify"}

"{t.login.brandingQuote}"

{step === "loading" && t.login.google.loadingTitle} {step === "collect_mobile" && t.login.google.collectMobileTitle} {step === "claim_required" && t.login.google.claimTitle} {step === "error" && t.login.google.errorTitle}

{step === "loading" && t.login.google.loadingDescription} {step === "collect_mobile" && t.login.google.collectMobileDescription(googleEmail || "-")} {step === "claim_required" && t.login.google.claimDescription(mobile)} {step === "error" && (errorMessage || t.login.google.loadFailed)}

{activeWarning && (

{t.login.throttle.title}

{activeWarning}

)}
{step === "loading" && (

{t.login.google.loadingDescription}

)} {step === "collect_mobile" && (

{t.login.google.googleAccount}

{googleEmail}

setMobile(event.target.value)} maxLength={11} disabled={loading} className={`h-11 ${isRtl ? "text-end" : "text-start"}`} />
)} {step === "claim_required" && (

{t.login.google.claimDescription(mobile)}

setOtpCode(event.target.value)} maxLength={6} disabled={loading} className="h-11 text-center text-lg tracking-widest" />
)} {step === "error" && (

{errorMessage || t.login.google.loadFailed}

)}
); }