TorbatYar/frontend/app/auth/verify-mobile/page.tsx
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

135 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Container } from "@/components/ui";
import { getStoredToken, loginWithRedirect } from "@/lib/auth";
import { identityApi } from "@/lib/identity";
import {
isValidMobile,
normalizeOtpCode,
normalizePhone,
persianToEnglish,
} from "@/lib/phone";
export default function VerifyMobilePage() {
const router = useRouter();
const [mobile, setMobile] = useState("");
const [code, setCode] = useState("");
const [countdown, setCountdown] = useState(0);
const [step, setStep] = useState<"mobile" | "code">("mobile");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const verifyingRef = useRef(false);
useEffect(() => {
if (!getStoredToken()) {
void loginWithRedirect();
}
}, []);
useEffect(() => {
if (countdown <= 0) return;
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
return () => clearTimeout(timer);
}, [countdown]);
const handleRequestOtp = async () => {
const token = getStoredToken();
if (!token) return;
setError("");
const normalized = normalizePhone(mobile);
if (!isValidMobile(normalized)) {
setError("شماره موبایل صحیح نیست");
return;
}
setMobile(normalized);
setLoading(true);
try {
const res = await identityApi.requestMobileVerify(normalized, token);
setCountdown(res.expires_in);
setStep("code");
} catch (e) {
setError(e instanceof Error ? e.message : "ارسال کد ناموفق بود");
} finally {
setLoading(false);
}
};
const handleVerify = useCallback(async () => {
const token = getStoredToken();
if (!token || verifyingRef.current) return;
const normalizedCode = normalizeOtpCode(code);
if (normalizedCode.length !== 4) {
setError("کد تأیید باید ۴ رقم باشد");
return;
}
verifyingRef.current = true;
setLoading(true);
let keepLoading = false;
try {
await identityApi.verifyMobile(mobile, normalizedCode, token);
keepLoading = true;
router.replace("/dashboard");
} catch (e) {
setError(e instanceof Error ? e.message : "تأیید ناموفق بود");
} finally {
if (!keepLoading) {
setLoading(false);
verifyingRef.current = false;
}
}
}, [code, mobile, router]);
return (
<Container className="py-16">
<section className="mx-auto max-w-md rounded-2xl border border-gray-100 bg-white p-8 shadow-sm">
<h1 className="text-2xl font-bold text-secondary">تأیید شماره موبایل</h1>
<p className="mt-2 text-sm text-gray-600">
برای استفاده از سامانه یکپارچه، تأیید شماره موبایل الزامی است.
</p>
{error && (
<p className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
)}
{step === "mobile" ? (
<div className="mt-6 space-y-4">
<input
type="tel"
dir="ltr"
placeholder="09123456789"
value={mobile}
onChange={(e) => setMobile(persianToEnglish(e.target.value))}
className="w-full rounded-xl border border-gray-200 px-4 py-2.5"
/>
<Button className="w-full" onClick={handleRequestOtp} loading={loading} loadingText="در حال ارسال...">
دریافت کد تأیید
</Button>
</div>
) : (
<div className="mt-6 space-y-4">
<input
type="text"
dir="ltr"
maxLength={4}
value={code}
onChange={(e) => setCode(normalizeOtpCode(e.target.value))}
className="w-full rounded-xl border border-gray-200 px-4 py-2.5 text-center font-mono tracking-widest"
/>
<Button
className="w-full"
onClick={handleVerify}
loading={loading}
loadingText="در حال تأیید..."
disabled={code.length !== 4}
>
تأیید موبایل
</Button>
</div>
)}
</section>
</Container>
);
}