canina/frontend/application/components/AuthModal.tsx
parsa aghaei c57f775cb4
Some checks failed
Deploy Canina / deploy (push) Successful in 3m53s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
fix(otp): support WebOTP sms pattern with {0} and {1} and robust code extraction
2026-09-05 19:36:49 +03:30

1119 lines
55 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 React, { useState, useEffect, useRef, useCallback } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
X,
Building2,
ShieldCheck,
Phone,
Key,
LogIn,
ArrowRight,
RefreshCw,
Lock,
Upload,
CheckCircle2,
Eye,
EyeOff,
UserPlus,
Sparkles,
Smartphone,
Mail,
User,
ArrowLeft,
KeyRound,
Check
} from "lucide-react";
import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import { authService } from "../lib/services/authService";
import { toast } from "sonner";
import { toPersian, toEnglishDigits, cn } from "../lib/utils";
import OtpInput5 from "./OtpInput5";
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
}
function extractOtpFromText(text: string): string {
if (!text) return "";
const clean = toEnglishDigits(text);
const match = clean.match(/(\d{5})/);
if (match) return match[1];
return clean.replace(/[^0-9]/g, "").slice(0, 5);
}
// Global OTP expiry timestamp cache by phone number so switching back and forth preserves timer
const otpCooldownExpiries: Record<string, number> = {};
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const { fetchProfile } = useUserStore();
// Auth navigation state
const [authMode, setAuthMode] = useState<"login" | "register">("login");
const [loginMethod, setLoginMethod] = useState<"otp" | "password">("otp");
const [subView, setSubView] = useState<"main" | "otp-verify" | "forgot-phone" | "forgot-otp" | "reset-password" | "b2b">("main");
// Form fields
const [phoneNumber, setPhoneNumber] = useState("");
const [password, setPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [email, setEmail] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [otpCode, setOtpCode] = useState("");
const [businessName, setBusinessName] = useState("");
const [medicalLicense, setMedicalLicense] = useState("");
const [documentUploaded, setDocumentUploaded] = useState(false);
// States
const [isLoading, setIsLoading] = useState(false);
const [countdown, setCountdown] = useState(0);
const [showPassword, setShowPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const phoneNumberRef = useRef(phoneNumber);
const subViewRef = useRef(subView);
const isOpenRef = useRef(isOpen);
// Guard against double-verify (onComplete fires, then form submit also fires)
const isVerifyingRef = useRef(false);
const isB2BEnabled = useSettingsStore((s) => s.getBoolean('b2bRegistrationOpen', true) && s.getBoolean('b2b_enabled', true));
useEffect(() => {
phoneNumberRef.current = phoneNumber;
}, [phoneNumber]);
useEffect(() => {
subViewRef.current = subView;
}, [subView]);
useEffect(() => {
isOpenRef.current = isOpen;
}, [isOpen]);
// Handle hardware / browser back button for Android & mobile navigation
// IMPORTANT: only depend on isOpen here — NOT subView. If we depend on subView,
// every subView change pushes a new history entry and back button never closes the modal.
useEffect(() => {
if (!isOpen) return;
// Push ONE history entry when the modal opens so back button hits this entry first
window.history.pushState({ __authModalOpen: true }, "");
const handlePopState = () => {
if (subViewRef.current !== "main") {
// Navigate back within the modal
if (subViewRef.current === "forgot-otp") {
setSubView("forgot-phone");
} else if (subViewRef.current === "reset-password") {
setSubView("forgot-phone");
} else {
setSubView("main");
}
// Push another sentinel so the next back also hits this handler
window.history.pushState({ __authModalOpen: true }, "");
} else {
// Already at main view — close the modal
onClose();
}
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
// Reset modal state on open/close
useEffect(() => {
if (!isOpen) {
Promise.resolve().then(() => {
setAuthMode("login");
setLoginMethod("otp");
setSubView("main");
setPhoneNumber("");
setPassword("");
setNewPassword("");
setEmail("");
setFirstName("");
setLastName("");
setOtpCode("");
setBusinessName("");
setMedicalLicense("");
setDocumentUploaded(false);
setIsLoading(false);
setCountdown(0);
setShowPassword(false);
setShowNewPassword(false);
});
}
}, [isOpen]);
// Cooldown countdown timer
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
const triggerVerifyOtp = useCallback(
async (codeToVerify: string) => {
const cleanCode = extractOtpFromText(codeToVerify);
const cleanPhone = toEnglishDigits(phoneNumberRef.current).replace(/[^0-9]/g, "");
if (cleanCode.length !== 5 || !cleanPhone) return;
// Prevent double-call from onComplete + form submit firing simultaneously
if (isVerifyingRef.current) return;
isVerifyingRef.current = true;
setIsLoading(true);
try {
const response = await authService.verifyOtp(cleanPhone, cleanCode);
if (response.success) {
await fetchProfile();
toast.success("ورود با موفقیت انجام شد");
onClose();
}
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "کد تایید نامعتبر است");
isVerifyingRef.current = false;
} finally {
setIsLoading(false);
}
},
[fetchProfile, onClose],
);
// Keep a stable ref to triggerVerifyOtp so the WebOTP effect doesn't need
// it as a dependency (which was causing premature AbortController cancellation).
const triggerVerifyOtpRef = useRef(triggerVerifyOtp);
useEffect(() => { triggerVerifyOtpRef.current = triggerVerifyOtp; }, [triggerVerifyOtp]);
// WebOTP API - auto-fill OTP from incoming SMS.
// Bug-fix: only depend on [subView] here — NOT triggerVerifyOtp.
// The old implementation had [subView, triggerVerifyOtp] as deps, which meant any
// reference change in triggerVerifyOtp (caused by onClose/fetchProfile re-creation)
// would abort() the pending WebOTP Promise just as the user was clicking "Allow".
// Using a ref breaks the dependency cycle while keeping the callback up-to-date.
useEffect(() => {
if (subView !== "otp-verify" && subView !== "forgot-otp") return;
if (typeof window === "undefined" || !("OTPCredential" in window)) return;
let isMounted = true;
const ac = new AbortController();
const credApi = (navigator as unknown as {
credentials?: { get?: (opts: unknown) => Promise<{ code?: string }> };
}).credentials;
credApi?.get?.({
otp: { transport: ["sms"] },
signal: ac.signal,
}).then((otp) => {
if (!isMounted || !otp) return;
// OTPCredential.code is the extracted code, but fallback to any string property
const rawCode = (otp as { code?: string })?.code || (otp as { id?: string })?.id || "";
const digits = extractOtpFromText(rawCode);
if (digits.length === 5) {
setOtpCode(digits);
// 400 ms delay so the user can see the filled boxes before verification
setTimeout(() => {
if (isMounted) triggerVerifyOtpRef.current(digits);
}, 400);
}
}).catch(() => {});
return () => {
isMounted = false;
ac.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subView]);
const handleOtpChange = (val: string) => {
const clean = extractOtpFromText(val);
setOtpCode(clean);
// Auto-verify is handled by OtpInput5's onComplete callback only
// to prevent double-submission
};
const handleSendOtp = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!/^09\d{9}$/.test(cleanPhone)) {
toast.error("لطفاً شماره موبایل ۱۱ رقمی معتبر وارد کنید (مثال: ۰۹۱۲۳۴۵۶۷۸۹)");
return;
}
// Check if cooldown is still active for this phone number
const now = Date.now();
const existingExpiry = otpCooldownExpiries[cleanPhone] || 0;
const remainingSeconds = Math.max(0, Math.ceil((existingExpiry - now) / 1000));
if (remainingSeconds > 0) {
setCountdown(remainingSeconds);
setSubView("otp-verify");
return;
}
setIsLoading(true);
try {
const res = await authService.sendOtp(cleanPhone);
const resObj = res as unknown as { code?: string };
if (resObj?.code) {
toast.info(`کد تایید (تست): ${resObj.code}`, { duration: 10000 });
} else {
toast.success("کد تایید با موفقیت پیامک شد");
}
setSubView("otp-verify");
otpCooldownExpiries[cleanPhone] = Date.now() + 120 * 1000;
setCountdown(120);
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ارسال کد تایید");
} finally {
setIsLoading(false);
}
};
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!cleanPhone || !password) return toast.error("شماره موبایل و رمز عبور الزامی است");
setIsLoading(true);
try {
const res = await authService.login(cleanPhone, password.trim());
if (res.success) {
await fetchProfile();
toast.success("با موفقیت وارد شدید");
onClose();
}
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "شماره موبایل یا رمز عبور اشتباه است");
} finally {
setIsLoading(false);
}
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!firstName?.trim() || !lastName?.trim() || !cleanPhone || !password) {
return toast.error("لطفاً تمامی فیلدهای الزامی را تکمیل کنید");
}
if (!/^09\d{9}$/.test(cleanPhone)) {
return toast.error("شماره موبایل نامعتبر است");
}
if (password.length < 6) {
return toast.error("رمز عبور باید حداقل ۶ کاراکتر باشد");
}
setIsLoading(true);
try {
const res = await authService.register({
firstName: firstName.trim(),
lastName: lastName.trim(),
mobile: cleanPhone,
email: email ? email.trim() : undefined,
password: password.trim(),
});
if (res.success) {
await fetchProfile();
toast.success("ثبت‌نام با موفقیت انجام شد");
onClose();
}
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ثبت‌نام");
} finally {
setIsLoading(false);
}
};
const handleResendOtp = async () => {
if (countdown > 0) return;
const cleanPhone = toEnglishDigits(phoneNumber).trim();
setIsLoading(true);
try {
await authService.sendOtp(cleanPhone);
toast.success("کد تایید جدید پیامک شد");
otpCooldownExpiries[cleanPhone] = Date.now() + 120 * 1000;
setCountdown(120);
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ارسال مجدد کد");
} finally {
setIsLoading(false);
}
};
const handleForgotSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!/^09\d{9}$/.test(cleanPhone)) {
toast.error("شماره موبایل نامعتبر است");
return;
}
setIsLoading(true);
try {
const res = await authService.sendOtp(cleanPhone);
const resObj = res as unknown as { code?: string };
if (resObj?.code) {
toast.info(`کد تایید (تست): ${resObj.code}`, { duration: 10000 });
} else {
toast.success("کد تایید جهت بازیابی رمز پیامک شد");
}
setSubView("forgot-otp");
setCountdown(120);
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ارسال کد");
} finally {
setIsLoading(false);
}
};
const handleForgotVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
const cleanCode = toEnglishDigits(otpCode).trim();
if (cleanCode.length !== 5) {
toast.error("کد تایید باید ۵ رقم باشد");
return;
}
setIsLoading(true);
try {
await authService.verifyOtp(toEnglishDigits(phoneNumber).trim(), cleanCode);
setSubView("reset-password");
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "کد تایید نامعتبر است");
} finally {
setIsLoading(false);
}
};
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
if (!newPassword || newPassword.length < 6) {
return toast.error("رمز عبور جدید باید حداقل ۶ کاراکتر باشد");
}
setIsLoading(true);
try {
await authService.updateProfile({ password: newPassword.trim() });
await fetchProfile();
toast.success("رمز عبور جدید با موفقیت تنظیم شد");
onClose();
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در تغییر رمز عبور");
} finally {
setIsLoading(false);
}
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 font-vazir overflow-y-auto" dir="rtl">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-medical-gray-900/60 backdrop-blur-md transition-opacity"
/>
{/* Modal Container */}
<motion.div
initial={{ opacity: 0, scale: 0.96, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 20 }}
className="relative w-full max-w-md bg-white rounded-t-[2.5rem] sm:rounded-[2rem] shadow-2xl overflow-hidden border border-medical-gray-100 z-10 max-h-[92vh] flex flex-col my-auto pb-safe font-vazir text-right"
dir="rtl"
>
{/* Top Bar / Header */}
<div className="px-6 pt-5 pb-3 border-b border-medical-gray-100/80 flex items-center justify-between bg-medical-gray-50/50 shrink-0">
<div className="flex items-center gap-2 text-medical-gray-900">
<div className="w-8 h-8 rounded-xl bg-canina-blue/10 text-canina-blue flex items-center justify-center">
<ShieldCheck className="w-4 h-4" />
</div>
<div>
<h3 className="text-sm font-black tracking-tight text-medical-gray-900 font-vazir">احراز هویت کنینا</h3>
<p className="text-[10px] text-medical-gray-400 font-bold font-vazir">ورود امن به حساب کاربری</p>
</div>
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-full bg-white border border-medical-gray-200 flex items-center justify-center text-medical-gray-400 hover:text-medical-gray-700 hover:bg-medical-gray-100 transition-all cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Modal Body */}
<div className="p-6 sm:p-7 overflow-y-auto">
<AnimatePresence mode="wait">
{/* 1. MAIN FLOW: Login or Register */}
{subView === "main" && (
<motion.div
key="main-auth"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-5"
>
{/* Primary Tab: Login vs Register */}
<div className="p-1 bg-medical-gray-100/90 rounded-2xl flex gap-1 font-vazir">
<button
type="button"
onClick={() => {
setAuthMode("login");
setPassword("");
}}
className={cn(
"flex-1 py-2.5 rounded-xl text-xs font-black transition-all flex items-center justify-center gap-1.5 cursor-pointer font-vazir",
authMode === "login"
? "bg-white text-canina-blue shadow-xs font-black"
: "text-medical-gray-500 hover:text-medical-gray-800"
)}
>
<LogIn className="w-3.5 h-3.5" />
<span>ورود به حساب</span>
</button>
<button
type="button"
onClick={() => {
setAuthMode("register");
setPassword("");
}}
className={cn(
"flex-1 py-2.5 rounded-xl text-xs font-black transition-all flex items-center justify-center gap-1.5 cursor-pointer font-vazir",
authMode === "register"
? "bg-white text-canina-blue shadow-xs font-black"
: "text-medical-gray-500 hover:text-medical-gray-800"
)}
>
<UserPlus className="w-3.5 h-3.5" />
<span>ثبتنام کاربر جدید</span>
</button>
</div>
{/* Sub-Tabs for Login Mode: Password vs OTP */}
{authMode === "login" && (
<div className="flex items-center justify-center gap-2 pt-1 pb-1 font-vazir">
<span className="text-[11px] font-bold text-medical-gray-400 font-vazir">روش ورود:</span>
<div className="inline-flex p-1 bg-medical-gray-50 border border-medical-gray-200 rounded-xl gap-1">
<button
type="button"
onClick={() => setLoginMethod("password")}
className={cn(
"px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all flex items-center gap-1 cursor-pointer font-vazir",
loginMethod === "password"
? "bg-canina-blue text-white shadow-xs font-black"
: "text-medical-gray-600 hover:text-medical-gray-900"
)}
>
<KeyRound className="w-3 h-3" />
<span>رمز عبور</span>
</button>
<button
type="button"
onClick={() => setLoginMethod("otp")}
className={cn(
"px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all flex items-center gap-1 cursor-pointer font-vazir",
loginMethod === "otp"
? "bg-canina-blue text-white shadow-xs font-black"
: "text-medical-gray-600 hover:text-medical-gray-900"
)}
>
<Smartphone className="w-3 h-3" />
<span>کد پیامکی یکبار مصرف</span>
</button>
</div>
</div>
)}
{/* Form for Login (Password Method) */}
{authMode === "login" && loginMethod === "password" && (
<form onSubmit={handlePasswordLogin} className="space-y-4 pt-1 font-vazir">
<div className="space-y-1.5">
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره تلفن همراه</label>
<div className="relative">
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
autoFocus
type="tel"
inputMode="numeric"
pattern="[0-9]*"
maxLength={11}
required
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider transition-all"
dir="ltr"
/>
</div>
</div>
<div className="space-y-1.5">
<div className="flex justify-between items-center pr-1">
<label className="text-[11px] font-black text-medical-gray-500 font-vazir">رمز عبور</label>
<button
type="button"
data-testid="forgot-password-link"
onClick={() => setSubView("forgot-phone")}
className="text-[11px] font-bold text-canina-blue hover:underline cursor-pointer font-vazir"
>
فراموشی رمز عبور؟
</button>
</div>
<div className="relative">
<Lock className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
required
placeholder="رمز عبور حساب کاربری..."
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider transition-all"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute left-3 top-1/2 -translate-y-1/2 p-1.5 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<button
type="submit"
disabled={isLoading || !phoneNumber || !password}
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer mt-2 font-vazir"
>
{isLoading ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<>
<LogIn className="w-4 h-4" />
<span>ورود با رمز عبور</span>
</>
)}
</button>
</form>
)}
{/* Form for Login (OTP Method) */}
{authMode === "login" && loginMethod === "otp" && (
<form onSubmit={handleSendOtp} className="space-y-4 pt-1 font-vazir">
<div className="space-y-1.5">
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره تلفن همراه</label>
<div className="relative">
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
autoFocus
type="tel"
inputMode="numeric"
pattern="[0-9]*"
maxLength={11}
required
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider transition-all"
dir="ltr"
/>
</div>
<p className="text-[10px] text-medical-gray-400 pr-1 font-vazir">کد تایید ۵ رقمی برای این شماره پیامک خواهد شد.</p>
</div>
<button
type="submit"
disabled={isLoading || phoneNumber.length < 11}
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer mt-2 font-vazir"
>
{isLoading ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<>
<span>دریافت کد تایید و ورود</span>
<ArrowLeft className="w-4 h-4" />
</>
)}
</button>
</form>
)}
{/* Form for Register */}
{authMode === "register" && (
<form onSubmit={handleRegister} className="space-y-3.5 pt-1 font-vazir">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label htmlFor="reg-fname" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">نام *</label>
<div className="relative">
<User className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
<input
id="reg-fname"
type="text"
required
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="نام..."
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-bold font-vazir placeholder:font-vazir"
/>
</div>
</div>
<div className="space-y-1">
<label htmlFor="reg-lname" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">نام خانوادگی *</label>
<input
id="reg-lname"
type="text"
required
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="نام خانوادگی..."
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-bold font-vazir placeholder:font-vazir"
/>
</div>
</div>
<div className="space-y-1">
<label htmlFor="reg-phone" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره همراه *</label>
<div className="relative">
<Phone className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
<input
id="reg-phone"
type="tel"
required
maxLength={11}
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir tracking-wider"
dir="ltr"
/>
</div>
</div>
<div className="space-y-1">
<label htmlFor="reg-email" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">ایمیل (اختیاری)</label>
<div className="relative">
<Mail className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
<input
id="reg-email"
type="email"
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir"
dir="ltr"
/>
</div>
</div>
<div className="space-y-1">
<label htmlFor="reg-pass" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">رمز عبور (حداقل ۶ کاراکتر) *</label>
<div className="relative">
<Lock className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
<input
id="reg-pass"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
required
placeholder="حداقل ۶ کاراکتر..."
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-9 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute left-2 top-1/2 -translate-y-1/2 p-1 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
</div>
<button
type="submit"
disabled={isLoading || !phoneNumber || !password || !firstName || !lastName}
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-xs hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer mt-3 font-vazir"
>
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>تکمیل و ایجاد حساب کاربری</>}
</button>
</form>
)}
{/* B2B Portal Action Link */}
{isB2BEnabled && (
<div className="pt-3 border-t border-medical-gray-100 font-vazir">
<button
type="button"
onClick={() => setSubView("b2b")}
className="w-full py-2.5 bg-amber-500/10 text-amber-900 border border-amber-300/60 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 hover:bg-amber-500/15 transition-all cursor-pointer font-vazir"
>
<Building2 className="w-4 h-4 text-amber-600" />
<span>ثبتنام و احراز هویت خریداران عمده و کلینیکها</span>
</button>
</div>
)}
</motion.div>
)}
{/* 2. OTP VERIFICATION SUB-VIEW */}
{subView === "otp-verify" && (
<motion.div
key="otp-verify-subview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-5"
>
<div className="flex items-center justify-between pb-1">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setSubView("main")}
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
>
<ArrowRight className="w-4 h-4" />
</button>
<span className="text-xs font-black text-medical-gray-900">تایید شماره تلفن</span>
</div>
<span className="text-[11px] font-mono text-canina-blue font-bold dir-ltr">{toPersian(phoneNumber)}</span>
</div>
<form onSubmit={(e) => { e.preventDefault(); triggerVerifyOtp(otpCode); }} className="space-y-4">
<div className="space-y-2">
<label className="text-[11px] font-black text-medical-gray-500 block text-center">
کد ۵ رقمی ارسالشده به موبایل خود را وارد نمایید
</label>
<OtpInput5
value={otpCode}
onChange={handleOtpChange}
disabled={isLoading}
autoFocus={true}
onComplete={(code) => triggerVerifyOtp(code)}
/>
<p className="text-[10px] text-medical-gray-400 text-center font-medium">
در صورت دریافت پیامک، کد به صورت خودکار شناسایی و وارد میشود.
</p>
</div>
<button
type="submit"
disabled={isLoading || otpCode.length < 5}
className="w-full py-4 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? (
<RefreshCw className="w-5 h-5 animate-spin" />
) : (
<>
<CheckCircle2 className="w-5 h-5" />
<span>تایید و ورود به سایت</span>
</>
)}
</button>
<div className="text-center pt-2">
{countdown > 0 ? (
<span className="text-xs font-bold text-medical-gray-400">
امکان ارسال مجدد کد تا {toPersian(countdown.toString())} ثانیه دیگر
</span>
) : (
<button
type="button"
onClick={handleResendOtp}
className="text-xs font-black text-canina-blue hover:underline cursor-pointer"
>
ارسال مجدد کد تایید پیامکی
</button>
)}
</div>
</form>
</motion.div>
)}
{/* 3. FORGOT PASSWORD PHONE SUB-VIEW */}
{subView === "forgot-phone" && (
<motion.div
key="forgot-phone-subview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-5 font-vazir"
>
<div className="flex items-center justify-between pb-1">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setSubView("main")}
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
>
<ArrowRight className="w-4 h-4" />
</button>
<span className="text-xs font-black text-medical-gray-900 font-vazir">بازیابی رمز عبور</span>
</div>
</div>
<form onSubmit={handleForgotSendOtp} className="space-y-4 font-vazir">
<div className="space-y-1.5">
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره موبایل حساب کاربری</label>
<div className="relative">
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
autoFocus
type="tel"
maxLength={11}
required
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider"
dir="ltr"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading || phoneNumber.length < 11}
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer font-vazir"
>
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>ارسال کد بازیابی</>}
</button>
</form>
</motion.div>
)}
{/* 4. FORGOT PASSWORD OTP SUB-VIEW */}
{subView === "forgot-otp" && (
<motion.div
key="forgot-otp-subview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-5 font-vazir"
>
<div className="flex items-center justify-between pb-1">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setSubView("forgot-phone")}
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
>
<ArrowRight className="w-4 h-4" />
</button>
<span className="text-xs font-black text-medical-gray-900 font-vazir">کد تایید بازیابی رمز</span>
</div>
<span className="text-[11px] font-mono text-canina-blue font-bold dir-ltr">{toPersian(phoneNumber)}</span>
</div>
<form onSubmit={handleForgotVerifyOtp} className="space-y-4 font-vazir">
<div className="space-y-2">
<label className="text-[11px] font-black text-medical-gray-500 block text-center font-vazir">
کد ۵ رقمی بازیابی پیامکشده را وارد کنید
</label>
<OtpInput5
value={otpCode}
onChange={(val) => setOtpCode(val)}
disabled={isLoading}
autoFocus={true}
/>
</div>
<button
type="submit"
disabled={isLoading || otpCode.length < 5}
className="w-full py-4 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer font-vazir"
>
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>تایید و ادامه</>}
</button>
</form>
</motion.div>
)}
{/* 5. RESET PASSWORD SUB-VIEW */}
{subView === "reset-password" && (
<motion.div
key="reset-password-subview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-5 font-vazir"
>
<div className="flex items-center gap-2 pb-1">
<span className="text-xs font-black text-medical-gray-900 font-vazir">تنظیم رمز عبور جدید</span>
</div>
<form onSubmit={handleResetPassword} className="space-y-4 font-vazir">
<div className="space-y-1.5">
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">رمز عبور جدید (حداقل ۶ کاراکتر)</label>
<div className="relative">
<Lock className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
autoFocus
type={showNewPassword ? "text" : "password"}
autoComplete="new-password"
required
placeholder="رمز جدید..."
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute left-3 top-1/2 -translate-y-1/2 p-1.5 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
tabIndex={-1}
>
{showNewPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<button
type="submit"
disabled={isLoading || newPassword.length < 6}
className="w-full py-4 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer font-vazir"
>
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>ثبت رمز عبور جدید و ورود</>}
</button>
</form>
</motion.div>
)}
{/* 6. B2B WHOLESALE REQUEST SUB-VIEW */}
{subView === "b2b" && (
<motion.div
key="b2b-subview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-4 font-vazir"
>
<div className="flex items-center justify-between pb-1">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setSubView("main")}
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
>
<ArrowRight className="w-4 h-4" />
</button>
<span className="text-xs font-black text-medical-gray-900 font-vazir">ثبتنام خریدار عمده و همکار</span>
</div>
</div>
<div className="p-3 bg-amber-50/80 border border-amber-200 rounded-xl text-[11px] text-amber-900 font-bold leading-relaxed font-vazir">
ثبت شماره نظام دامپزشکی یا پروانه کسب جهت دسترسی به قیمتهای همکاری و سفارشات عمده الزامی است.
</div>
<form
onSubmit={(e) => {
e.preventDefault();
toast.success("درخواست احراز هویت خریدار عمده با موفقیت ثبت شد و پس از بررسی فعال خواهد گردید.");
setSubView("main");
}}
className="space-y-3 font-vazir"
>
<div className="space-y-1">
<label className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">نام مجموعه / داروخانه / کلینیک *</label>
<input
type="text"
required
value={businessName}
onChange={(e) => setBusinessName(e.target.value)}
placeholder="مثال: کلینیک دامپزشکی آریا"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-bold font-vazir placeholder:font-vazir"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره نظام دامپزشکی / پروانه کسب *</label>
<input
type="text"
required
value={medicalLicense}
onChange={(e) => setMedicalLicense(e.target.value)}
placeholder="مثال: ۹۸۷۶۵۴۳۲۱"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir tracking-wider"
dir="ltr"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">موبایل مسئول *</label>
<input
type="tel"
required
maxLength={11}
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
placeholder="۰۹۱۲..."
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir tracking-wider"
dir="ltr"
/>
</div>
<div
onClick={() => {
setDocumentUploaded(true);
toast.success("تصویر مدرک با موفقیت بارگذاری شد.");
}}
className={cn(
"p-3.5 border-2 border-dashed rounded-xl text-center cursor-pointer transition-all font-vazir",
documentUploaded ? "border-emerald-500 bg-emerald-50" : "border-medical-gray-300 hover:border-canina-blue bg-medical-gray-50/50"
)}
>
{documentUploaded ? (
<div className="flex items-center justify-center gap-2 text-emerald-700 font-bold text-xs font-vazir">
<Check className="w-4 h-4" />
<span>تصویر مدرک بارگذاری شد</span>
</div>
) : (
<div className="flex flex-col items-center gap-1 text-medical-gray-500 text-xs font-vazir">
<Upload className="w-4 h-4 text-canina-blue" />
<span className="text-[11px] font-bold font-vazir">بارگذاری تصویر کارت نظام یا پروانه کسب</span>
</div>
)}
</div>
<button
type="submit"
disabled={!businessName || !medicalLicense || !phoneNumber}
className="w-full py-3 bg-canina-blue text-white rounded-xl font-black text-xs hover:bg-canina-blue/90 disabled:opacity-50 transition-all cursor-pointer mt-2 font-vazir"
>
ارسال درخواست بررسی به کارشناسان
</button>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}