From b7d5f7bc652ae77a17082e429ade05cd900c4e94 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 28 Sep 2026 22:06:42 +0330 Subject: [PATCH] feat: simplify phone-first authentication flow --- src/app/reset-password/page.tsx | 10 +- src/app/robots.ts | 1 - src/components/MobileBottomNav.tsx | 3 +- src/components/Navbar.tsx | 8 +- src/components/OtpCodeField.tsx | 3 + src/components/ui/button.tsx | 6 +- src/components/ui/navigation-menu.tsx | 2 +- src/components/ui/toggle.tsx | 2 +- src/lib/api.ts | 7 - src/lib/types.ts | 8 +- src/views/Auth.tsx | 266 +++++++++++++------------- src/views/ResetPasswordRequest.tsx | 235 ----------------------- 12 files changed, 150 insertions(+), 401 deletions(-) delete mode 100644 src/views/ResetPasswordRequest.tsx diff --git a/src/app/reset-password/page.tsx b/src/app/reset-password/page.tsx index 3d7579e..3e57ff9 100644 --- a/src/app/reset-password/page.tsx +++ b/src/app/reset-password/page.tsx @@ -1,11 +1,5 @@ -import type { Metadata } from "next"; -import ResetPasswordRequest from "@/views/ResetPasswordRequest"; - -export const metadata: Metadata = { - title: "بازیابی رمز عبور", - robots: { index: false, follow: false }, -}; +import { redirect } from "next/navigation"; export default function ResetPasswordPage() { - return ; + redirect("/auth"); } diff --git a/src/app/robots.ts b/src/app/robots.ts index 4ed1483..b7fc6b3 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -13,7 +13,6 @@ export default function robots(): MetadataRoute.Robots { "/logout", "/profile", "/payments/", - "/reset-password", ], }, ], diff --git a/src/components/MobileBottomNav.tsx b/src/components/MobileBottomNav.tsx index 06609f0..24ec1a8 100644 --- a/src/components/MobileBottomNav.tsx +++ b/src/components/MobileBottomNav.tsx @@ -20,8 +20,7 @@ const isProfileOrAuthPath = (pathname: string) => pathname === "/profile" || pathname.startsWith("/profile/") || pathname === "/auth" || - pathname.startsWith("/auth/") || - pathname.startsWith("/reset-password"); + pathname.startsWith("/auth/"); export default function MobileBottomNav({ siteContent }: { siteContent: SiteContent }) { const pathname = usePathname() || "/"; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b68a3a0..1f2ba63 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import { useMemo } from "react"; import Image from "next/image"; -import { LayoutDashboard, LogOut, RotateCcw, UserRound } from "lucide-react"; +import { LayoutDashboard, LogOut, UserRound } from "lucide-react"; import { Link, NavLink } from "@/lib/router"; import { useAuth } from "@/contexts/AuthContext"; import ModeToggle from "@/components/ModeToggle"; @@ -91,12 +91,6 @@ function ProfileAvatarMenu() { - - - تغییر یا بازیابی رمز - - - {isAdminUser ? ( diff --git a/src/components/OtpCodeField.tsx b/src/components/OtpCodeField.tsx index e3a66b9..141255b 100644 --- a/src/components/OtpCodeField.tsx +++ b/src/components/OtpCodeField.tsx @@ -9,6 +9,7 @@ type OtpCodeFieldProps = { disabled?: boolean; length?: number; className?: string; + ariaLabel?: string; }; export default function OtpCodeField({ @@ -17,6 +18,7 @@ export default function OtpCodeField({ disabled = false, length = 5, className, + ariaLabel = "کد تایید پیامکی", }: OtpCodeFieldProps) { return ( onChange(nextValue.replace(/[^\d]/g, ""))} disabled={disabled} inputMode="numeric" + aria-label={ariaLabel} containerClassName={cn("w-full justify-center", className)} > diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index cdedd4f..22418d2 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -11,9 +11,9 @@ const buttonVariants = cva( variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", + outline: "border border-input bg-background text-foreground hover:bg-secondary hover:text-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/75 hover:text-foreground", + ghost: "hover:bg-secondary hover:text-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx index c9fcd7a..66db5f9 100644 --- a/src/components/ui/navigation-menu.tsx +++ b/src/components/ui/navigation-menu.tsx @@ -35,7 +35,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; const NavigationMenuItem = NavigationMenuPrimitive.Item; const navigationMenuTriggerStyle = cva( - "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50", + "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-secondary hover:text-foreground focus:bg-secondary focus:text-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-secondary/70 data-[state=open]:bg-secondary/70", ); const NavigationMenuTrigger = React.forwardRef< diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx index de5dfc5..45a04eb 100644 --- a/src/components/ui/toggle.tsx +++ b/src/components/ui/toggle.tsx @@ -10,7 +10,7 @@ const toggleVariants = cva( variants: { variant: { default: "bg-transparent", - outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + outline: "border border-input bg-transparent hover:bg-secondary hover:text-foreground", }, size: { default: "h-10 px-3", diff --git a/src/lib/api.ts b/src/lib/api.ts index fdc4cf0..8351bd9 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -207,13 +207,6 @@ class ApiClient { }); } - async resetPassword(data: Types.PasswordResetSchema) { - return this.request('/api/auth/reset-password', { - method: 'POST', - body: JSON.stringify(data), - }); - } - async sendMobileVerificationOtp(data: Types.MobileOtpSendSchema) { return this.request('/api/auth/mobile/send-otp', { method: 'POST', diff --git a/src/lib/types.ts b/src/lib/types.ts index e26468e..5a8c110 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -172,7 +172,7 @@ export interface RegisterOtpVerifySchema { export interface OtpSendSchema { mobile: string; - mode: "register" | "login" | "reset_password" | "verify_mobile" | "google_claim"; + mode: "register" | "login" | "verify_mobile" | "google_claim"; } export interface OtpSendResponseSchema { @@ -194,12 +194,6 @@ export interface MobileLookupSchema { has_password: boolean; } -export interface PasswordResetSchema { - mobile: string; - code: string; - new_password: string; -} - export interface MobileOtpSendSchema { mobile: string; } diff --git a/src/views/Auth.tsx b/src/views/Auth.tsx index f76a25e..572a1af 100644 --- a/src/views/Auth.tsx +++ b/src/views/Auth.tsx @@ -1,8 +1,7 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - AlertTriangle, ArrowRight, KeyRound, Loader2, @@ -18,7 +17,7 @@ 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 { Navigate, useNavigate } from "@/lib/router"; import { resolveErrorMessage } from "@/lib/utils"; type AuthStep = "mobile" | "password" | "otp_login" | "otp_register" | "register_details"; @@ -75,6 +74,8 @@ export default function Auth() { const { login, loginWithOtp, isAuthenticated } = useAuth(); const [step, setStep] = useState("mobile"); const [authLoading, setAuthLoading] = useState(false); + const [formError, setFormError] = useState(""); + const authActionRef = useRef(false); const [mobile, setMobile] = useState(""); const [password, setPassword] = useState(""); const [otpCode, setOtpCode] = useState(""); @@ -86,14 +87,17 @@ export default function Auth() { const [registerForm, setRegisterForm] = useState(createEmptyRegisterForm); useEffect(() => { - const timer = window.setInterval(() => { + if (otpCooldowns.login <= 0 && otpCooldowns.register <= 0) { + return; + } + const timer = window.setTimeout(() => { setOtpCooldowns((current) => ({ login: Math.max(current.login - 1, 0), register: Math.max(current.register - 1, 0), })); }, 1000); - return () => window.clearInterval(timer); - }, []); + return () => window.clearTimeout(timer); + }, [otpCooldowns.login, otpCooldowns.register]); const loadMajors = useCallback(async (params: { search: string; limit: number; offset: number }) => { const data = await api.getMajorsPaged(params); @@ -117,28 +121,28 @@ export default function Auth() { switch (step) { case "password": return { - title: "رمز عبور حساب", - description: "برای این شماره موبایل حساب فعال پیدا شد. رمز عبور را وارد کنید یا روش ورود را به کد پیامکی تغییر دهید.", + title: "ورود با رمز عبور", + description: "برای این شماره حساب پیدا شد. رمز عبور را وارد کنید یا از کد پیامکی استفاده کنید.", }; case "otp_login": return { - title: "ورود با کد پیامکی", - description: "کد ۵ رقمی ارسال‌شده به موبایل را وارد کنید تا وارد حساب خود شوید.", + title: "تأیید شماره موبایل", + description: "کد پیامکی را وارد کنید تا وارد حساب خود شوید.", }; case "otp_register": return { - title: "تایید موبایل", - description: "ابتدا موبایل شما با کد پیامکی تایید می‌شود، سپس فرم تکمیل اطلاعات نمایش داده خواهد شد.", + title: "تأیید شماره موبایل", + description: "کد پیامکی را وارد کنید؛ سپس اطلاعات لازم برای ساخت حساب را تکمیل می‌کنید.", }; case "register_details": return { title: "تکمیل اطلاعات حساب", - description: "موبایل شما تایید شد. حالا اطلاعات تکمیلی و رمز عبور را وارد کنید تا ثبت‌نام نهایی شود.", + description: "شماره موبایل تأیید شد. اطلاعات حساب خود را وارد کنید تا ثبت‌نام کامل شود.", }; default: return { - title: "ورود و ثبت‌نام", - description: "ابتدا شماره موبایل را وارد کنید تا مسیر مناسب برای ورود یا ثبت‌نام به شما نمایش داده شود.", + title: "ورود یا ثبت‌نام", + description: "شماره موبایل را وارد کنید. اگر حساب رمز داشته باشد، ابتدا رمز را وارد می‌کنید؛ در غیر این صورت کد پیامکی دریافت می‌کنید.", }; } }, [step]); @@ -151,6 +155,7 @@ export default function Auth() { key: K, value: ReturnType[K], ) => { + setFormError(""); setRegisterForm((current) => ({ ...current, [key]: value })); }; @@ -159,30 +164,47 @@ export default function Auth() { setPassword(""); setOtpCode(""); setLookupState(null); - setRegisterForm(createEmptyRegisterForm()); + setFormError(""); + }; + + const beginAuthAction = () => { + if (authActionRef.current) { + return false; + } + authActionRef.current = true; + setAuthLoading(true); + setFormError(""); + return true; + }; + + const finishAuthAction = () => { + authActionRef.current = false; + setAuthLoading(false); }; const ensureValidMobile = (value: string) => { const normalizedMobile = sanitizeMobile(value); if (normalizedMobile.length !== 11 || !normalizedMobile.startsWith("09")) { - toast({ - title: "شماره موبایل نامعتبر است", - description: "لطفاً شماره موبایل را به شکل 09xxxxxxxxx وارد کنید.", - variant: "destructive", - }); + setFormError("شماره موبایل را به شکل 09xxxxxxxxx وارد کنید."); return null; } return normalizedMobile; }; - const sendOtpAndAdvance = async (mode: "login" | "register", mobileValue = mobile) => { + const sendOtpAndAdvance = async ( + mode: "login" | "register", + mobileValue = mobile, + actionAlreadyStarted = false, + ) => { const normalizedMobile = ensureValidMobile(mobileValue); if (!normalizedMobile) { return; } + if (!actionAlreadyStarted && !beginAuthAction()) { + return; + } try { - setAuthLoading(true); const payload = await api.sendOtp({ mobile: normalizedMobile, mode }); setMobile(normalizedMobile); setOtpCode(""); @@ -197,61 +219,50 @@ export default function Auth() { variant: "success", }); } catch (error: unknown) { - toast({ - title: "ارسال کد ناموفق بود", - description: resolveErrorMessage(error, "ارسال کد پیامکی انجام نشد."), - variant: "destructive", - }); + setFormError(resolveErrorMessage(error, "ارسال کد پیامکی انجام نشد.")); } finally { - setAuthLoading(false); + if (!actionAlreadyStarted) { + finishAuthAction(); + } } }; const handleMobileStep = async (event: React.FormEvent) => { event.preventDefault(); const normalizedMobile = ensureValidMobile(mobile); - if (!normalizedMobile) { + if (!normalizedMobile || !beginAuthAction()) { 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); + await sendOtpAndAdvance(lookup.exists ? "login" : "register", normalizedMobile, true); } catch (error: unknown) { - toast({ - title: "بررسی شماره موبایل ناموفق بود", - description: resolveErrorMessage(error, "امکان ادامه با این شماره موبایل وجود ندارد."), - variant: "destructive", - }); + setFormError(resolveErrorMessage(error, "امکان ادامه با این شماره موبایل وجود ندارد.")); } finally { - setAuthLoading(false); + finishAuthAction(); } }; const handlePasswordLogin = async (event: React.FormEvent) => { event.preventDefault(); if (!password) { - toast({ - title: "رمز عبور لازم است", - description: "برای ادامه، رمز عبور حساب را وارد کنید.", - variant: "destructive", - }); + setFormError("برای ادامه، رمز عبور حساب را وارد کنید."); + return; + } + if (!beginAuthAction()) { return; } try { - setAuthLoading(true); await login(mobile, password); toast({ title: "ورود انجام شد", @@ -260,29 +271,23 @@ export default function Auth() { }); navigate("/profile"); } catch (error: unknown) { - toast({ - title: "ورود ناموفق بود", - description: resolveErrorMessage(error, "اطلاعات ورود صحیح نیست."), - variant: "destructive", - }); + setFormError(resolveErrorMessage(error, "اطلاعات ورود صحیح نیست.")); } finally { - setAuthLoading(false); + finishAuthAction(); } }; const handleOtpLogin = async (event: React.FormEvent) => { event.preventDefault(); if (otpCode.length !== OTP_LENGTH) { - toast({ - title: "کد تایید ناقص است", - description: "کد ۵ رقمی پیامک‌شده را کامل وارد کنید.", - variant: "destructive", - }); + setFormError("کد ۵ رقمی پیامک‌شده را کامل وارد کنید."); + return; + } + if (!beginAuthAction()) { return; } try { - setAuthLoading(true); await loginWithOtp(mobile, otpCode); toast({ title: "ورود پیامکی انجام شد", @@ -291,29 +296,23 @@ export default function Auth() { }); navigate("/profile"); } catch (error: unknown) { - toast({ - title: "ورود با کد ناموفق بود", - description: resolveErrorMessage(error, "کد واردشده معتبر نیست."), - variant: "destructive", - }); + setFormError(resolveErrorMessage(error, "کد واردشده معتبر نیست.")); } finally { - setAuthLoading(false); + finishAuthAction(); } }; const handleRegisterOtpVerification = async (event: React.FormEvent) => { event.preventDefault(); if (otpCode.length !== OTP_LENGTH) { - toast({ - title: "کد تایید ناقص است", - description: "کد ۵ رقمی پیامک‌شده را کامل وارد کنید.", - variant: "destructive", - }); + setFormError("کد ۵ رقمی پیامک‌شده را کامل وارد کنید."); + return; + } + if (!beginAuthAction()) { return; } try { - setAuthLoading(true); await api.verifyRegisterOtp({ mobile, code: otpCode }); setStep("register_details"); toast({ @@ -322,13 +321,9 @@ export default function Auth() { variant: "success", }); } catch (error: unknown) { - toast({ - title: "تایید کد ناموفق بود", - description: resolveErrorMessage(error, "کد واردشده معتبر نیست."), - variant: "destructive", - }); + setFormError(resolveErrorMessage(error, "کد واردشده معتبر نیست.")); } finally { - setAuthLoading(false); + finishAuthAction(); } }; @@ -336,34 +331,24 @@ export default function Auth() { event.preventDefault(); if (!USERNAME_REGEX.test(registerForm.username)) { - toast({ - title: "نام کاربری نامعتبر است", - description: "نام کاربری باید ۳ تا ۳۰ کاراکتر و فقط شامل حروف لاتین، عدد، نقطه، آندرلاین یا خط تیره باشد.", - variant: "destructive", - }); + setFormError("نام کاربری باید ۳ تا ۳۰ کاراکتر و فقط شامل حروف لاتین، عدد، نقطه، آندرلاین یا خط تیره باشد."); return; } if (registerForm.password.length < 8) { - toast({ - title: "رمز عبور کوتاه است", - description: "رمز عبور باید حداقل ۸ کاراکتر داشته باشد.", - variant: "destructive", - }); + setFormError("رمز عبور باید حداقل ۸ کاراکتر داشته باشد."); return; } if (registerForm.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(registerForm.email.trim())) { - toast({ - title: "ایمیل نامعتبر است", - description: "اگر ایمیل وارد می‌کنید، فرمت آن باید صحیح باشد.", - variant: "destructive", - }); + setFormError("اگر ایمیل وارد می‌کنید، فرمت آن باید صحیح باشد."); + return; + } + if (!beginAuthAction()) { return; } try { - setAuthLoading(true); await api.register({ username: registerForm.username, mobile, @@ -378,24 +363,28 @@ export default function Auth() { university: registerForm.university, }); - toast({ - title: "ثبت‌نام کامل شد", - description: "اکنون می‌توانید با موبایل و رمز عبور یا کد پیامکی وارد شوید.", - variant: "success", - }); + const registrationPassword = registerForm.password; setLookupState({ exists: true, has_password: true }); - setPassword(""); + setPassword(registrationPassword); setRegisterForm(createEmptyRegisterForm()); setOtpCode(""); - setStep("password"); + + try { + await login(mobile, registrationPassword); + toast({ + title: "ثبت‌نام کامل شد", + description: "حساب شما ساخته شد و وارد سایت شدید.", + variant: "success", + }); + navigate("/profile"); + } catch { + setStep("password"); + setFormError("حساب ساخته شد، اما ورود خودکار کامل نشد. با همین رمز وارد شوید."); + } } catch (error: unknown) { - toast({ - title: "ثبت‌نام ناموفق بود", - description: resolveErrorMessage(error, "اطلاعات ارسالی قابل پذیرش نیست."), - variant: "destructive", - }); + setFormError(resolveErrorMessage(error, "اطلاعات ارسالی قابل پذیرش نیست.")); } finally { - setAuthLoading(false); + finishAuthAction(); } }; @@ -408,9 +397,9 @@ export default function Auth() { {mobile}

- ) : null; @@ -426,12 +415,20 @@ export default function Auth() { type="tel" dir="ltr" inputMode="numeric" + autoComplete="tel" + required + pattern="09[0-9]{9}" maxLength={11} + disabled={authLoading} value={mobile} - onChange={(event) => setMobile(sanitizeMobile(event.target.value))} + onChange={(event) => { + setMobile(sanitizeMobile(event.target.value)); + setFormError(""); + }} placeholder="09xxxxxxxxx" className="h-12 rounded-2xl" /> +

مثال: 09123456789

- - رمز عبور را فراموش کرده‌ام - ); @@ -528,7 +523,12 @@ export default function Auth() {
- setOtpCode(normalizeDigits(value))} disabled={authLoading} /> + { setOtpCode(normalizeDigits(value)); setFormError(""); }} + disabled={authLoading} + ariaLabel="کد ۵ رقمی پیامکی" + />
@@ -557,7 +557,7 @@ export default function Auth() {
{isLogin && lookupState?.has_password ? ( - ) : null} @@ -580,6 +580,7 @@ export default function Auth() { updateRegisterForm("first_name", event.target.value)} className="h-12 rounded-2xl" @@ -591,6 +592,7 @@ export default function Auth() { updateRegisterForm("last_name", event.target.value)} className="h-12 rounded-2xl" @@ -606,8 +608,11 @@ export default function Auth() { updateRegisterForm("username", sanitizeUsername(event.target.value))} + onChange={(event) => { updateRegisterForm("username", sanitizeUsername(event.target.value)); setFormError(""); }} placeholder="latin.username" className="h-12 rounded-2xl" /> @@ -619,8 +624,12 @@ export default function Auth() { updateRegisterForm("password", event.target.value)} + onChange={(event) => { updateRegisterForm("password", event.target.value); setFormError(""); }} className="h-12 rounded-2xl" /> @@ -634,6 +643,7 @@ export default function Auth() { updateRegisterForm("email", event.target.value)} placeholder="user@example.com" @@ -648,6 +658,7 @@ export default function Auth() { id="register-student-id" dir="ltr" inputMode="numeric" + disabled={authLoading} value={registerForm.student_id} onChange={(event) => updateRegisterForm("student_id", sanitizeMobile(event.target.value))} className="h-12 rounded-2xl" @@ -667,6 +678,7 @@ export default function Auth() { loadOptions={loadUniversities} value={registerForm.university} onChange={(value) => updateRegisterForm("university", value)} + disabled={authLoading} placeholder="انتخاب دانشگاه" searchPlaceholder="نام دانشگاه را بنویسید..." emptyText="دانشگاهی پیدا نشد" @@ -685,6 +697,7 @@ export default function Auth() { loadOptions={loadMajors} value={registerForm.major} onChange={(value) => updateRegisterForm("major", value)} + disabled={authLoading} placeholder="انتخاب رشته" searchPlaceholder="نام رشته را بنویسید..." emptyText="رشته‌ای پیدا نشد" @@ -702,6 +715,7 @@ export default function Auth() { id="register-year" dir="ltr" inputMode="numeric" + disabled={authLoading} value={registerForm.year_of_study} onChange={(event) => updateRegisterForm("year_of_study", sanitizeMobile(event.target.value))} className="h-12 rounded-2xl" @@ -740,17 +754,11 @@ export default function Auth() { {stepMeta.description} -
-
- -
-

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

-

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

-
+ {formError ? ( +
+

{formError}

-
+ ) : null} {step === "mobile" ? renderMobileStep() : null} {step === "password" ? renderPasswordStep() : null} diff --git a/src/views/ResetPasswordRequest.tsx b/src/views/ResetPasswordRequest.tsx deleted file mode 100644 index 971aee8..0000000 --- a/src/views/ResetPasswordRequest.tsx +++ /dev/null @@ -1,235 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { AlertTriangle, Loader2, ShieldCheck } from "lucide-react"; -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 { useToast } from "@/hooks/use-toast"; -import { api } from "@/lib/api"; -import { Link } from "@/lib/router"; -import { resolveErrorMessage } from "@/lib/utils"; - -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, ""); - -export default function ResetPasswordRequest() { - const { toast } = useToast(); - const [mobile, setMobile] = useState(""); - const [code, setCode] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [loading, setLoading] = useState(false); - const [cooldown, setCooldown] = useState(0); - - useEffect(() => { - if (cooldown <= 0) { - return; - } - const timer = window.setTimeout(() => setCooldown((current) => current - 1), 1000); - return () => window.clearTimeout(timer); - }, [cooldown]); - - const handleSendOtp = async () => { - try { - setLoading(true); - const response = await api.sendOtp({ - mobile: sanitizeMobile(mobile), - mode: "reset_password", - }); - setCooldown(Math.min(response.expires_in_seconds, 120)); - toast({ - title: "کد بازیابی ارسال شد", - description: response.message, - variant: "success", - }); - } catch (error: unknown) { - toast({ - title: "ارسال کد انجام نشد", - description: resolveErrorMessage(error, "امکان ارسال پیامک بازیابی وجود ندارد."), - variant: "destructive", - }); - } finally { - setLoading(false); - } - }; - - const handleResetPassword = async (event: React.FormEvent) => { - event.preventDefault(); - if (newPassword.length < 8) { - toast({ - title: "رمز عبور کوتاه است", - description: "رمز جدید باید حداقل ۸ کاراکتر داشته باشد.", - variant: "destructive", - }); - return; - } - if (newPassword !== confirmPassword) { - toast({ - title: "عدم تطابق رمزها", - description: "تکرار رمز عبور با رمز جدید یکسان نیست.", - variant: "destructive", - }); - return; - } - - try { - setLoading(true); - await api.resetPassword({ - mobile: sanitizeMobile(mobile), - code: normalizeDigits(code), - new_password: newPassword, - }); - toast({ - title: "رمز عبور تغییر کرد", - description: "اکنون می‌توانید با رمز جدید وارد شوید.", - variant: "success", - }); - setCode(""); - setNewPassword(""); - setConfirmPassword(""); - } catch (error: unknown) { - toast({ - title: "بازیابی ناموفق بود", - description: resolveErrorMessage(error, "کد تأیید یا رمز جدید قابل پذیرش نیست."), - variant: "destructive", - }); - } finally { - setLoading(false); - } - }; - - return ( -
-
- - - بازیابی حساب بدون ایمیل - - بازیابی رمز عبور اکنون با موبایل و کد پیامکی انجام می‌شود. اگر به موبایل ثبت‌شده هم دسترسی ندارید، از همان حساب گوگلی که قبلاً با ایمیل‌تان استفاده می‌کردید کمک بگیرید. - - - -
-
- -
-

اگر رمز را فراموش کرده‌اید

-

- می‌توانید از مسیر ورود با گوگل ادامه دهید؛ به شرطی که حساب گوگل شما با ایمیل قدیمی‌تان یکسان باشد. -

-
-
-
- -
-

گام‌های بازیابی

-
    -
  1. ۱. موبایل ثبت‌شده را وارد کنید.
  2. -
  3. ۲. کد پیامکی را دریافت و ثبت کنید.
  4. -
  5. ۳. رمز عبور جدید را تعیین کنید.
  6. -
-
-
-
- - - - تغییر رمز عبور با کد پیامکی - - این فرم جایگزین کامل بازیابی ایمیلی است. - - - -
-
- - setMobile(sanitizeMobile(event.target.value))} - placeholder="09xxxxxxxxx" - className="h-12 rounded-2xl" - /> -
- -
- - setCode(normalizeDigits(value))} disabled={loading} /> -
- -
-
- - setNewPassword(event.target.value)} - className="h-12 rounded-2xl" - /> -
-
- - setConfirmPassword(event.target.value)} - className="h-12 rounded-2xl" - /> -
-
- -
- - -
- -
- - بازگشت به صفحه ورود - -
-
-
-
-
-
- ); -}