canina/frontend/application/components/AuthModal.tsx
parsa aghaei 63e10449ad
Some checks failed
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s
Deploy Canina / deploy (push) Has been cancelled
fix(auth): unify typography to Vazir and set password login as default
2026-08-26 14:23:07 +03:30

1102 lines
54 KiB
TypeScript
Raw 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";
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);
}
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">("password");
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 otpInputRef = useRef<HTMLInputElement>(null);
const phoneNumberRef = useRef(phoneNumber);
const isB2BEnabled = useSettingsStore((s) => s.getBoolean('b2bRegistrationOpen', true) && s.getBoolean('b2b_enabled', true));
useEffect(() => {
phoneNumberRef.current = phoneNumber;
}, [phoneNumber]);
// Focus retry when OTP input is shown
useEffect(() => {
if (subView === "otp-verify" || subView === "forgot-otp") {
const timers = [50, 150, 300, 500].map((delay) =>
setTimeout(() => {
if (otpInputRef.current) {
otpInputRef.current.focus({ preventScroll: true });
}
}, delay),
);
return () => timers.forEach((t) => clearTimeout(t));
}
}, [subView]);
// 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;
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 || "کد تایید نامعتبر است");
} finally {
setIsLoading(false);
}
},
[fetchProfile, onClose],
);
// WebOTP API SMS Auto-read
useEffect(() => {
if (subView !== "otp-verify" && subView !== "forgot-otp") return;
let isMounted = true;
const ac = new AbortController();
if (typeof window !== "undefined" && "OTPCredential" in window) {
(navigator as unknown as { credentials: { get: (opts: unknown) => Promise<{ code?: string }> } }).credentials
?.get({
otp: { transport: ["sms"] },
signal: ac.signal,
})
.then((otp) => {
if (!isMounted) return;
if (otp && typeof otp.code === "string") {
const clean = extractOtpFromText(otp.code);
if (clean) {
setOtpCode(clean);
if (otpInputRef.current) {
otpInputRef.current.value = clean;
}
if (clean.length === 5 && subView === "otp-verify") {
triggerVerifyOtp(clean);
}
}
}
})
.catch(() => {});
}
const inputEl = otpInputRef.current;
const handleDomAutofill = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target && target.value) {
const clean = extractOtpFromText(target.value);
setOtpCode(clean);
if (clean.length === 5 && subView === "otp-verify") {
triggerVerifyOtp(clean);
}
}
};
if (inputEl) {
inputEl.addEventListener("input", handleDomAutofill);
inputEl.addEventListener("change", handleDomAutofill);
}
return () => {
isMounted = false;
ac.abort();
if (inputEl) {
inputEl.removeEventListener("input", handleDomAutofill);
inputEl.removeEventListener("change", handleDomAutofill);
}
};
}, [subView, triggerVerifyOtp]);
const handleOtpChange = (val: string) => {
const clean = extractOtpFromText(val);
setOtpCode(clean);
if (otpInputRef.current && otpInputRef.current.value !== clean) {
otpInputRef.current.value = clean;
}
if (clean.length === 5 && subView === "otp-verify") {
triggerVerifyOtp(clean);
}
};
const handleSendOtp = async (e?: React.FormEvent) => {
if (e) 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("otp-verify");
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;
setIsLoading(true);
try {
await authService.sendOtp(toEnglishDigits(phoneNumber).trim());
toast.success("کد تایید جدید پیامک شد");
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>
<div className="relative">
<input
ref={otpInputRef}
id="otp-code-input"
name="one-time-code"
type="tel"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="one-time-code"
placeholder="• • • • •"
value={otpCode}
onChange={(e) => handleOtpChange(e.target.value)}
onInput={(e) => handleOtpChange((e.target as HTMLInputElement).value)}
onPaste={(e) => {
e.preventDefault();
const pasted = e.clipboardData.getData('text');
handleOtpChange(pasted);
}}
disabled={isLoading}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-center text-2xl font-mono tracking-[0.4em]"
dir="ltr"
/>
</div>
<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>
<div className="relative">
<input
ref={otpInputRef}
autoFocus
type="text"
inputMode="numeric"
autoComplete="one-time-code"
placeholder="• • • • •"
value={otpCode}
onChange={(e) => setOtpCode(toEnglishDigits(e.target.value).replace(/[^0-9]/g, "").slice(0, 5))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-center font-mono font-bold text-2xl tracking-[0.4em]"
dir="ltr"
/>
</div>
</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>
);
}