146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { Suspense, useEffect, useState } from "react";
|
|||
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|||
|
|
import { ChevronRight } from "lucide-react";
|
|||
|
|
import { Button } from "@/components/ui/Button";
|
|||
|
|
import { OtpInput } from "@/components/ui/OtpInput";
|
|||
|
|
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
|||
|
|
import { requestOtp } from "@/lib/api";
|
|||
|
|
import { formatApiError } from "@/lib/hooks";
|
|||
|
|
import { useAuth } from "@/lib/auth-context";
|
|||
|
|
import { stepByOrder } from "@/lib/onboarding-steps";
|
|||
|
|
|
|||
|
|
const RESEND_SECONDS = 60;
|
|||
|
|
|
|||
|
|
function formatCountdown(seconds: number) {
|
|||
|
|
const m = Math.floor(seconds / 60);
|
|||
|
|
const s = seconds % 60;
|
|||
|
|
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function VerifyForm() {
|
|||
|
|
const router = useRouter();
|
|||
|
|
const searchParams = useSearchParams();
|
|||
|
|
const phone = searchParams.get("phone") ?? "";
|
|||
|
|
const { login } = useAuth();
|
|||
|
|
|
|||
|
|
const [otp, setOtp] = useState("");
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
const [countdown, setCountdown] = useState(RESEND_SECONDS);
|
|||
|
|
const [resending, setResending] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (countdown <= 0) return;
|
|||
|
|
const t = setTimeout(() => setCountdown((c) => c - 1), 1000);
|
|||
|
|
return () => clearTimeout(t);
|
|||
|
|
}, [countdown]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!phone) router.replace("/login");
|
|||
|
|
}, [phone, router]);
|
|||
|
|
|
|||
|
|
async function handleVerify(code: string) {
|
|||
|
|
if (code.length !== 4) return;
|
|||
|
|
setLoading(true);
|
|||
|
|
setError(null);
|
|||
|
|
try {
|
|||
|
|
const user = await login(phone, code);
|
|||
|
|
if (user.is_onboarded) {
|
|||
|
|
router.push("/home");
|
|||
|
|
} else {
|
|||
|
|
const stepSlug = stepByOrder(user.current_step)?.slug ?? "basic-info";
|
|||
|
|
router.push(`/onboarding/${stepSlug}`);
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(formatApiError(err, "کد وارد شده اشتباهه"));
|
|||
|
|
setOtp("");
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function handleResend() {
|
|||
|
|
setResending(true);
|
|||
|
|
setError(null);
|
|||
|
|
try {
|
|||
|
|
await requestOtp(phone);
|
|||
|
|
setCountdown(RESEND_SECONDS);
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(formatApiError(err));
|
|||
|
|
} finally {
|
|||
|
|
setResending(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="flex-1 flex flex-col min-h-dvh px-6 pt-[calc(env(safe-area-inset-top)+16px)] pb-[calc(env(safe-area-inset-bottom)+32px)]">
|
|||
|
|
<button
|
|||
|
|
onClick={() => router.push("/login")}
|
|||
|
|
className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary) self-start"
|
|||
|
|
aria-label="بازگشت"
|
|||
|
|
>
|
|||
|
|
<ChevronRight size={20} className="text-(--color-content-secondary)" />
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<div className="flex flex-col items-center text-center gap-2 pt-6">
|
|||
|
|
<span
|
|||
|
|
dir="ltr"
|
|||
|
|
className={`font-[var(--font-display)] font-extrabold text-[44px] tabular-nums tracking-wide ${
|
|||
|
|
countdown > 0 ? "text-(--color-brand-500)" : "text-(--color-content-disabled)"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
{formatCountdown(countdown)}
|
|||
|
|
</span>
|
|||
|
|
<h1 className="font-[var(--font-display)] font-extrabold text-[20px] pt-2">
|
|||
|
|
کد تایید رو وارد کن
|
|||
|
|
</h1>
|
|||
|
|
<p className="text-[14px] text-(--color-content-secondary)">
|
|||
|
|
کد ۴ رقمی ارسالشده به{" "}
|
|||
|
|
<span dir="ltr" className="font-bold text-(--color-content-primary)">
|
|||
|
|
{phone}
|
|||
|
|
</span>
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex flex-col gap-6 pt-10">
|
|||
|
|
<OtpInput value={otp} onChange={setOtp} onComplete={handleVerify} disabled={loading} />
|
|||
|
|
<ErrorBanner message={error} />
|
|||
|
|
<Button
|
|||
|
|
size="lg"
|
|||
|
|
isBlock
|
|||
|
|
isLoading={loading}
|
|||
|
|
disabled={otp.length !== 4}
|
|||
|
|
onClick={() => handleVerify(otp)}
|
|||
|
|
>
|
|||
|
|
تایید کد
|
|||
|
|
</Button>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-center gap-1.5 text-[13px]">
|
|||
|
|
<span className="text-(--color-content-tertiary)">کد نیومد؟</span>
|
|||
|
|
{countdown > 0 ? (
|
|||
|
|
<span className="font-bold text-(--color-content-secondary)">صبر کن...</span>
|
|||
|
|
) : (
|
|||
|
|
<button
|
|||
|
|
onClick={handleResend}
|
|||
|
|
disabled={resending}
|
|||
|
|
className="font-bold text-(--color-brand-600) hover:text-(--color-brand-700) disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
دوباره بفرست
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function VerifyPage() {
|
|||
|
|
return (
|
|||
|
|
<Suspense>
|
|||
|
|
<VerifyForm />
|
|||
|
|
</Suspense>
|
|||
|
|
);
|
|||
|
|
}
|