fix: polish authentication navigation and autofill

This commit is contained in:
2026-09-28 23:18:05 +03:30
parent b7d5f7bc65
commit 61698ccbd9
18 changed files with 474 additions and 282 deletions

View File

@@ -17,6 +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 { clearStoredAuthReturnTo, DEFAULT_AUTH_RETURN_TO, getSafeAuthReturnTo } from "@/lib/auth-return";
import { Navigate, useNavigate } from "@/lib/router";
import { resolveErrorMessage } from "@/lib/utils";
@@ -72,6 +73,9 @@ export default function Auth() {
const navigate = useNavigate();
const { toast } = useToast();
const { login, loginWithOtp, isAuthenticated } = useAuth();
const returnTo = typeof window === "undefined"
? DEFAULT_AUTH_RETURN_TO
: getSafeAuthReturnTo(new URLSearchParams(window.location.search).get("next"));
const [step, setStep] = useState<AuthStep>("mobile");
const [authLoading, setAuthLoading] = useState(false);
const [formError, setFormError] = useState("");
@@ -148,7 +152,7 @@ export default function Auth() {
}, [step]);
if (isAuthenticated) {
return <Navigate to="/profile" replace />;
return <Navigate to={returnTo} replace />;
}
const updateRegisterForm = <K extends keyof ReturnType<typeof createEmptyRegisterForm>>(
@@ -252,9 +256,15 @@ export default function Auth() {
}
};
const handlePasswordLogin = async (event: React.FormEvent) => {
const handlePasswordLogin = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!password) {
const formData = new FormData(event.currentTarget);
const submittedMobile = ensureValidMobile(String(formData.get("username") ?? ""));
const submittedPassword = String(formData.get("password") ?? "");
if (!submittedMobile) {
return;
}
if (!submittedPassword) {
setFormError("برای ادامه، رمز عبور حساب را وارد کنید.");
return;
}
@@ -263,13 +273,14 @@ export default function Auth() {
}
try {
await login(mobile, password);
await login(submittedMobile, submittedPassword);
toast({
title: "ورود انجام شد",
description: "خوش آمدید. حساب شما آماده استفاده است.",
variant: "success",
});
navigate("/profile");
clearStoredAuthReturnTo();
navigate(returnTo, { replace: true });
} catch (error: unknown) {
setFormError(resolveErrorMessage(error, "اطلاعات ورود صحیح نیست."));
} finally {
@@ -294,7 +305,8 @@ export default function Auth() {
description: "با موفقیت وارد حساب کاربری خود شدید.",
variant: "success",
});
navigate("/profile");
clearStoredAuthReturnTo();
navigate(returnTo, { replace: true });
} catch (error: unknown) {
setFormError(resolveErrorMessage(error, "کد واردشده معتبر نیست."));
} finally {
@@ -376,7 +388,8 @@ export default function Auth() {
description: "حساب شما ساخته شد و وارد سایت شدید.",
variant: "success",
});
navigate("/profile");
clearStoredAuthReturnTo();
navigate(returnTo, { replace: true });
} catch {
setStep("password");
setFormError("حساب ساخته شد، اما ورود خودکار کامل نشد. با همین رمز وارد شوید.");
@@ -388,14 +401,40 @@ export default function Auth() {
}
};
const renderStepHeader = () =>
const renderStepHeader = (includeUsernameField = false) =>
step !== "mobile" ? (
<div className="mb-6 flex items-center justify-between rounded-[1.5rem] border border-border/70 bg-muted/25 p-4">
<div className="text-right">
<p className="text-xs text-muted-foreground">شماره موبایل انتخاب‌شده</p>
<p className="mt-1 font-semibold" dir="ltr">
{mobile}
</p>
<div className="min-w-0 text-right">
{includeUsernameField ? (
<>
<Label htmlFor="auth-username" className="text-xs text-muted-foreground">
شماره موبایل انتخاب‌شده
</Label>
<Input
id="auth-username"
name="username"
type="text"
inputMode="tel"
autoComplete="username"
required
maxLength={11}
dir="ltr"
value={mobile}
onChange={(event) => {
setMobile(sanitizeMobile(event.target.value));
setFormError("");
}}
className="mt-1 h-auto rounded-md border-0 bg-transparent px-0 py-0 text-left text-base font-semibold shadow-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
</>
) : (
<>
<p className="text-xs text-muted-foreground">شماره موبایل انتخاب‌شده</p>
<p className="mt-1 font-semibold" dir="ltr">
{mobile}
</p>
</>
)}
</div>
<Button type="button" variant="ghost" className="rounded-xl text-sm" onClick={resetToMobileStep} disabled={authLoading}>
<ArrowRight className="ml-2 h-4 w-4" />
@@ -458,7 +497,7 @@ export default function Auth() {
type="button"
variant="outline"
className="h-12 w-full rounded-2xl border-border/70 bg-background/80"
onClick={() => void api.startGoogleLogin()}
onClick={() => void api.startGoogleLogin(returnTo)}
disabled={authLoading}
>
<GoogleIcon />
@@ -469,7 +508,7 @@ export default function Auth() {
const renderPasswordStep = () => (
<form className="space-y-4" onSubmit={handlePasswordLogin}>
{renderStepHeader()}
{renderStepHeader(true)}
<div>
<Label htmlFor="password" className="mb-2 block text-right">
@@ -477,6 +516,7 @@ export default function Auth() {
</Label>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required

View File

@@ -26,6 +26,7 @@ import {
toPersianDigits,
} from "@/lib/utils";
import { useAuth } from "@/contexts/AuthContext";
import { buildAuthPath } from "@/lib/auth-return";
const typeLabel: Record<string, string> = {
online: "آنلاین",
@@ -119,7 +120,7 @@ export default function EventDetail({ initialEvent = null }: EventDetailProps) {
description: "برای ثبت‌نام در رویداد باید وارد حساب کاربری خود شوید.",
variant: "destructive",
});
navigate("/auth");
navigate(buildAuthPath(`/events/${event.slug}`));
return;
}
@@ -158,7 +159,7 @@ export default function EventDetail({ initialEvent = null }: EventDetailProps) {
description: "برای ثبت‌نام در رویداد باید وارد حساب کاربری خود شوید.",
variant: "destructive",
});
navigate("/auth");
navigate(buildAuthPath(`/events/${event.slug}`));
return;
}

View File

@@ -11,6 +11,12 @@ import { Label } from "@/components/ui/label";
import { useAuth } from "@/contexts/AuthContext";
import { useToast } from "@/hooks/use-toast";
import { api } from "@/lib/api";
import {
clearStoredAuthReturnTo,
DEFAULT_AUTH_RETURN_TO,
buildAuthPath,
readStoredAuthReturnTo,
} from "@/lib/auth-return";
import { Link, useNavigate, useSearchParams } from "@/lib/router";
import type { GoogleFlowResponseSchema } from "@/lib/types";
import { resolveErrorMessage } from "@/lib/utils";
@@ -34,6 +40,8 @@ export default function GoogleAuthCallback() {
const [loading, setLoading] = useState(false);
const [otpCooldown, setOtpCooldown] = useState(0);
const [googleFlow, setGoogleFlow] = useState<GoogleFlowResponseSchema | null>(null);
const [flowError, setFlowError] = useState("");
const [returnTo, setReturnTo] = useState(DEFAULT_AUTH_RETURN_TO);
const [profileForm, setProfileForm] = useState({
mobile: "",
username: "",
@@ -62,6 +70,10 @@ export default function GoogleAuthCallback() {
};
}, []);
useEffect(() => {
setReturnTo(readStoredAuthReturnTo());
}, []);
useEffect(() => {
if (otpCooldown <= 0) {
return;
@@ -75,12 +87,14 @@ export default function GoogleAuthCallback() {
throw new Error("توکن‌های ورود گوگل دریافت نشد.");
}
await setSessionTokens(payload.access_token, payload.refresh_token);
const destination = readStoredAuthReturnTo();
clearStoredAuthReturnTo();
toast({
title: "ورود با گوگل کامل شد",
description: "حساب شما با موفقیت بازیابی یا متصل شد.",
variant: "success",
});
navigate("/profile", { replace: true });
navigate(destination, { replace: true });
}, [navigate, setSessionTokens, toast]);
const applyFlow = useCallback(async (payload: GoogleFlowResponseSchema) => {
@@ -110,11 +124,13 @@ export default function GoogleAuthCallback() {
return;
}
setFlowError("ورود با گوگل کامل نشد. دوباره تلاش کنید یا با موبایل وارد شوید.");
setStep("error");
}, [handleAuthenticatedFlow]);
useEffect(() => {
if (!flow) {
setFlowError("نشانی بازگشت گوگل ناقص است. از صفحه ورود دوباره شروع کنید.");
setStep("error");
return;
}
@@ -129,11 +145,7 @@ export default function GoogleAuthCallback() {
}
} catch (error: unknown) {
if (!cancelled) {
toast({
title: "اتصال گوگل کامل نشد",
description: resolveErrorMessage(error, "لینک گوگل نامعتبر یا منقضی شده است."),
variant: "destructive",
});
setFlowError("لینک ورود با گوگل معتبر نیست یا منقضی شده است. دوباره از صفحه ورود شروع کنید.");
setStep("error");
}
} finally {
@@ -146,7 +158,7 @@ export default function GoogleAuthCallback() {
return () => {
cancelled = true;
};
}, [applyFlow, flow, toast]);
}, [applyFlow, flow]);
const handleCompleteProfile = async (event: React.FormEvent) => {
event.preventDefault();
@@ -233,7 +245,7 @@ export default function GoogleAuthCallback() {
? "ایمیل گوگل شما با یک حساب قدیمی تطابق دارد. موبایل همان حساب را تأیید کنید تا بازیابی کامل شود."
: "این اولین ورود شما با گوگل است. اطلاعات تکمیلی را ثبت کنید تا حساب شما ساخته شود."
: step === "error"
? "لینک این فرایند منقضی شده یا از سمت گوگل کامل نشده است."
? "برای ادامه، ورود با گوگل را دوباره شروع کنید یا با شماره موبایل وارد شوید."
: "چند لحظه صبر کنید تا وضعیت حساب گوگل شما بررسی شود.";
return (
@@ -432,23 +444,21 @@ export default function GoogleAuthCallback() {
{step === "error" ? (
<div className="space-y-5">
<div className="rounded-[1.5rem] border border-amber-400/30 bg-amber-500/10 p-4 text-right text-sm leading-7 text-amber-900 dark:text-amber-100">
<div className="flex items-start justify-between gap-3">
<div role="alert" aria-live="assertive" className="rounded-[1.5rem] border border-destructive/40 bg-destructive/10 p-4 text-right text-sm leading-7 text-destructive dark:border-red-400/40 dark:bg-red-950/60 dark:text-red-200">
<div className="flex items-start justify-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0" />
<div>
<p className="font-semibold">فرایند گوگل کامل نشد</p>
<p className="mt-2">
دوباره از صفحه ورود شروع کنید. اگر مشکل ادامه داشت، از ورود با موبایل یا بازیابی با موبایل استفاده کنید.
</p>
<p className="mt-2">{flowError || "لینک این فرایند منقضی شده یا از سمت گوگل کامل نشده است."}</p>
</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<Button type="button" className="h-12 rounded-2xl" onClick={() => void api.startGoogleLogin()}>
<Button type="button" className="h-12 rounded-2xl" onClick={() => void api.startGoogleLogin(returnTo)}>
شروع دوباره با گوگل
</Button>
<Button asChild variant="outline" className="h-12 rounded-2xl">
<Link to="/auth">
<Link to={buildAuthPath(returnTo)}>
<ArrowLeft className="ml-2 h-4 w-4" />
بازگشت به ورود
</Link>

View File

@@ -23,6 +23,7 @@ import ConfirmAction from "@/components/ConfirmAction";
import Markdown from "@/components/Markdown";
import { Helmet } from "@/lib/helmet";
import { Link, Navigate } from "@/lib/router";
import { buildAuthPath } from "@/lib/auth-return";
import { useAuth } from "@/contexts/AuthContext";
import { api } from "@/lib/api";
import { blogPostPath } from "@/lib/blog-routes";
@@ -495,7 +496,7 @@ export default function Profile() {
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
</Helmet>
<Navigate to="/auth" replace />
<Navigate to={buildAuthPath("/profile")} replace />
</>
);
}