"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AlertTriangle, ArrowRight, KeyRound, Loader2, MessageSquareMore, Smartphone, } from "lucide-react"; import AsyncSearchableCombobox from "@/components/AsyncSearchableCombobox"; import OtpCodeField from "@/components/OtpCodeField"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useAuth } from "@/contexts/AuthContext"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/lib/api"; import { Link, Navigate, useNavigate } from "@/lib/router"; import { resolveErrorMessage } from "@/lib/utils"; type AuthStep = "mobile" | "password" | "otp_login" | "otp_register" | "register_details"; const OTP_LENGTH = 5; const USERNAME_REGEX = /^[A-Za-z0-9._-]{3,30}$/; const sanitizeUsername = (value: string) => value.replace(/[^A-Za-z0-9._-]/g, ""); const sanitizeText = (value: string) => value.trim(); const normalizeDigits = (value: string) => value .replace(/[\u06F0-\u06F9]/g, (digit) => String(digit.charCodeAt(0) - 0x06f0)) .replace(/[\u0660-\u0669]/g, (digit) => String(digit.charCodeAt(0) - 0x0660)); const sanitizeMobile = (value: string) => normalizeDigits(value).replace(/[^\d]/g, ""); const createEmptyRegisterForm = () => ({ username: "", email: "", password: "", first_name: "", last_name: "", student_id: "", year_of_study: "", major: null as string | null, university: null as string | null, }); function GoogleIcon() { return ( ); } export default function Auth() { const navigate = useNavigate(); const { toast } = useToast(); const { login, loginWithOtp, isAuthenticated } = useAuth(); const [step, setStep] = useState("mobile"); const [authLoading, setAuthLoading] = useState(false); const [mobile, setMobile] = useState(""); const [password, setPassword] = useState(""); const [otpCode, setOtpCode] = useState(""); const [lookupState, setLookupState] = useState<{ exists: boolean; has_password: boolean } | null>(null); const [otpCooldowns, setOtpCooldowns] = useState>({ login: 0, register: 0, }); const [registerForm, setRegisterForm] = useState(createEmptyRegisterForm); useEffect(() => { const timer = window.setInterval(() => { setOtpCooldowns((current) => ({ login: Math.max(current.login - 1, 0), register: Math.max(current.register - 1, 0), })); }, 1000); return () => window.clearInterval(timer); }, []); const loadMajors = useCallback(async (params: { search: string; limit: number; offset: number }) => { const data = await api.getMajorsPaged(params); return { count: data.count, results: data.results.map((major) => ({ value: String(major.code), label: major.label })), }; }, []); const loadUniversities = useCallback(async (params: { search: string; limit: number; offset: number }) => { const data = await api.getUniversitiesPaged(params); return { count: data.count, results: data.results.map((university) => ({ value: String(university.code), label: university.label })), }; }, []); const majorsLoading = false; const universitiesLoading = false; const stepMeta = useMemo(() => { switch (step) { case "password": return { title: "رمز عبور حساب", description: "برای این شماره موبایل حساب فعال پیدا شد. رمز عبور را وارد کنید یا روش ورود را به کد پیامکی تغییر دهید.", }; case "otp_login": return { title: "ورود با کد پیامکی", description: "کد ۵ رقمی ارسال‌شده به موبایل را وارد کنید تا وارد حساب خود شوید.", }; case "otp_register": return { title: "تایید موبایل", description: "ابتدا موبایل شما با کد پیامکی تایید می‌شود، سپس فرم تکمیل اطلاعات نمایش داده خواهد شد.", }; case "register_details": return { title: "تکمیل اطلاعات حساب", description: "موبایل شما تایید شد. حالا اطلاعات تکمیلی و رمز عبور را وارد کنید تا ثبت‌نام نهایی شود.", }; default: return { title: "ورود و ثبت‌نام", description: "ابتدا شماره موبایل را وارد کنید تا مسیر مناسب برای ورود یا ثبت‌نام به شما نمایش داده شود.", }; } }, [step]); if (isAuthenticated) { return ; } const updateRegisterForm = >( key: K, value: ReturnType[K], ) => { setRegisterForm((current) => ({ ...current, [key]: value })); }; const resetToMobileStep = () => { setStep("mobile"); setPassword(""); setOtpCode(""); setLookupState(null); setRegisterForm(createEmptyRegisterForm()); }; const ensureValidMobile = (value: string) => { const normalizedMobile = sanitizeMobile(value); if (normalizedMobile.length !== 11 || !normalizedMobile.startsWith("09")) { toast({ title: "شماره موبایل نامعتبر است", description: "لطفاً شماره موبایل را به شکل 09xxxxxxxxx وارد کنید.", variant: "destructive", }); return null; } return normalizedMobile; }; const sendOtpAndAdvance = async (mode: "login" | "register", mobileValue = mobile) => { const normalizedMobile = ensureValidMobile(mobileValue); if (!normalizedMobile) { return; } try { setAuthLoading(true); const payload = await api.sendOtp({ mobile: normalizedMobile, mode }); setMobile(normalizedMobile); setOtpCode(""); setOtpCooldowns((current) => ({ ...current, [mode]: Math.min(payload.expires_in_seconds, 120), })); setStep(mode === "login" ? "otp_login" : "otp_register"); toast({ title: "کد تایید ارسال شد", description: payload.message, variant: "success", }); } catch (error: unknown) { toast({ title: "ارسال کد ناموفق بود", description: resolveErrorMessage(error, "ارسال کد پیامکی انجام نشد."), variant: "destructive", }); } finally { setAuthLoading(false); } }; const handleMobileStep = async (event: React.FormEvent) => { event.preventDefault(); const normalizedMobile = ensureValidMobile(mobile); if (!normalizedMobile) { return; } try { setAuthLoading(true); const lookup = await api.checkMobile(normalizedMobile); setMobile(normalizedMobile); setLookupState(lookup); setPassword(""); setOtpCode(""); if (lookup.exists && lookup.has_password) { setStep("password"); return; } await sendOtpAndAdvance(lookup.exists ? "login" : "register", normalizedMobile); } catch (error: unknown) { toast({ title: "بررسی شماره موبایل ناموفق بود", description: resolveErrorMessage(error, "امکان ادامه با این شماره موبایل وجود ندارد."), variant: "destructive", }); } finally { setAuthLoading(false); } }; const handlePasswordLogin = async (event: React.FormEvent) => { event.preventDefault(); if (!password) { toast({ title: "رمز عبور لازم است", description: "برای ادامه، رمز عبور حساب را وارد کنید.", variant: "destructive", }); return; } try { setAuthLoading(true); await login(mobile, password); toast({ title: "ورود انجام شد", description: "خوش آمدید. حساب شما آماده استفاده است.", variant: "success", }); navigate("/profile"); } catch (error: unknown) { toast({ title: "ورود ناموفق بود", description: resolveErrorMessage(error, "اطلاعات ورود صحیح نیست."), variant: "destructive", }); } finally { setAuthLoading(false); } }; const handleOtpLogin = async (event: React.FormEvent) => { event.preventDefault(); if (otpCode.length !== OTP_LENGTH) { toast({ title: "کد تایید ناقص است", description: "کد ۵ رقمی پیامک‌شده را کامل وارد کنید.", variant: "destructive", }); return; } try { setAuthLoading(true); await loginWithOtp(mobile, otpCode); toast({ title: "ورود پیامکی انجام شد", description: "با موفقیت وارد حساب کاربری خود شدید.", variant: "success", }); navigate("/profile"); } catch (error: unknown) { toast({ title: "ورود با کد ناموفق بود", description: resolveErrorMessage(error, "کد واردشده معتبر نیست."), variant: "destructive", }); } finally { setAuthLoading(false); } }; const handleRegisterOtpVerification = async (event: React.FormEvent) => { event.preventDefault(); if (otpCode.length !== OTP_LENGTH) { toast({ title: "کد تایید ناقص است", description: "کد ۵ رقمی پیامک‌شده را کامل وارد کنید.", variant: "destructive", }); return; } try { setAuthLoading(true); await api.verifyRegisterOtp({ mobile, code: otpCode }); setStep("register_details"); toast({ title: "موبایل تایید شد", description: "حالا اطلاعات تکمیلی حساب را وارد کنید.", variant: "success", }); } catch (error: unknown) { toast({ title: "تایید کد ناموفق بود", description: resolveErrorMessage(error, "کد واردشده معتبر نیست."), variant: "destructive", }); } finally { setAuthLoading(false); } }; const handleRegister = async (event: React.FormEvent) => { event.preventDefault(); if (!USERNAME_REGEX.test(registerForm.username)) { toast({ title: "نام کاربری نامعتبر است", description: "نام کاربری باید ۳ تا ۳۰ کاراکتر و فقط شامل حروف لاتین، عدد، نقطه، آندرلاین یا خط تیره باشد.", variant: "destructive", }); return; } if (registerForm.password.length < 8) { toast({ title: "رمز عبور کوتاه است", description: "رمز عبور باید حداقل ۸ کاراکتر داشته باشد.", variant: "destructive", }); return; } if (registerForm.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(registerForm.email.trim())) { toast({ title: "ایمیل نامعتبر است", description: "اگر ایمیل وارد می‌کنید، فرمت آن باید صحیح باشد.", variant: "destructive", }); return; } try { setAuthLoading(true); await api.register({ username: registerForm.username, mobile, code: otpCode, password: registerForm.password, email: registerForm.email ? sanitizeText(registerForm.email) : null, first_name: registerForm.first_name ? sanitizeText(registerForm.first_name) : null, last_name: registerForm.last_name ? sanitizeText(registerForm.last_name) : null, student_id: registerForm.student_id ? sanitizeMobile(registerForm.student_id) : null, year_of_study: registerForm.year_of_study ? Number(registerForm.year_of_study) : null, major: registerForm.major, university: registerForm.university, }); toast({ title: "ثبت‌نام کامل شد", description: "اکنون می‌توانید با موبایل و رمز عبور یا کد پیامکی وارد شوید.", variant: "success", }); setLookupState({ exists: true, has_password: true }); setPassword(""); setRegisterForm(createEmptyRegisterForm()); setOtpCode(""); setStep("password"); } catch (error: unknown) { toast({ title: "ثبت‌نام ناموفق بود", description: resolveErrorMessage(error, "اطلاعات ارسالی قابل پذیرش نیست."), variant: "destructive", }); } finally { setAuthLoading(false); } }; const renderStepHeader = () => step !== "mobile" ? (

شماره موبایل انتخاب‌شده

{mobile}

) : null; const renderMobileStep = () => (
setMobile(sanitizeMobile(event.target.value))} placeholder="09xxxxxxxxx" className="h-12 rounded-2xl" />
یا
); const renderPasswordStep = () => (
{renderStepHeader()}
setPassword(event.target.value)} className="h-12 rounded-2xl" />
رمز عبور را فراموش کرده‌ام
); const renderOtpStep = (mode: "login" | "register") => { const isLogin = mode === "login"; return (
{renderStepHeader()}
setOtpCode(normalizeDigits(value))} disabled={authLoading} />
{isLogin && lookupState?.has_password ? ( ) : null}
); }; const renderRegisterDetailsStep = () => (
{renderStepHeader()}
این شماره موبایل با موفقیت تایید شده است. برای ثبت‌نام فقط کافی است اطلاعات باقی‌مانده را تکمیل کنید.
updateRegisterForm("first_name", event.target.value)} className="h-12 rounded-2xl" />
updateRegisterForm("last_name", event.target.value)} className="h-12 rounded-2xl" />
updateRegisterForm("username", sanitizeUsername(event.target.value))} placeholder="latin.username" className="h-12 rounded-2xl" />
updateRegisterForm("password", event.target.value)} className="h-12 rounded-2xl" />
updateRegisterForm("email", event.target.value)} placeholder="user@example.com" className="h-12 rounded-2xl" />
updateRegisterForm("student_id", sanitizeMobile(event.target.value))} className="h-12 rounded-2xl" />
{universitiesLoading ? (
) : ( updateRegisterForm("university", value)} placeholder="انتخاب دانشگاه" searchPlaceholder="نام دانشگاه را بنویسید..." emptyText="دانشگاهی پیدا نشد" className="h-12 rounded-2xl" /> )}
{majorsLoading ? (
) : ( updateRegisterForm("major", value)} placeholder="انتخاب رشته" searchPlaceholder="نام رشته را بنویسید..." emptyText="رشته‌ای پیدا نشد" className="h-12 rounded-2xl" /> )}
updateRegisterForm("year_of_study", sanitizeMobile(event.target.value))} className="h-12 rounded-2xl" />

ایمیل در این مرحله اختیاری است و برای ارسال پیام استفاده نخواهد شد. اطلاع‌رسانی‌های مهم از طریق پیامک یا اعلان داخل سایت انجام می‌شوند.

); return (
{stepMeta.title} {stepMeta.description}

نکته مهم برای کاربران قبلی

اگر دسترسی به ایمیل ندارید و رمز عبور را فراموش کرده‌اید، در همین مرحله از دکمه گوگل استفاده کنید. اگر ایمیل حساب شما با گوگل یکسان باشد، می‌توانید حساب را بازیابی کرده و بعد موبایل خود را تایید کنید.

{step === "mobile" ? renderMobileStep() : null} {step === "password" ? renderPasswordStep() : null} {step === "otp_login" ? renderOtpStep("login") : null} {step === "otp_register" ? renderOtpStep("register") : null} {step === "register_details" ? renderRegisterDetailsStep() : null}
); }