"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(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 ( {isOpen && (
{/* Backdrop */} {/* Modal Container */} {/* Top Bar / Header */}

احراز هویت کنینا

ورود امن به حساب کاربری

{/* Modal Body */}
{/* 1. MAIN FLOW: Login or Register */} {subView === "main" && ( {/* Primary Tab: Login vs Register */}
{/* Sub-Tabs for Login Mode: Password vs OTP */} {authMode === "login" && (
روش ورود:
)} {/* Form for Login (Password Method) */} {authMode === "login" && loginMethod === "password" && (
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" />
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" />
)} {/* Form for Login (OTP Method) */} {authMode === "login" && loginMethod === "otp" && (
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" />

کد تایید ۵ رقمی برای این شماره پیامک خواهد شد.

)} {/* Form for Register */} {authMode === "register" && (
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" />
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" />
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" />
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" />
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" />
)} {/* B2B Portal Action Link */} {isB2BEnabled && (
)}
)} {/* 2. OTP VERIFICATION SUB-VIEW */} {subView === "otp-verify" && (
تایید شماره تلفن
{toPersian(phoneNumber)}
{ e.preventDefault(); triggerVerifyOtp(otpCode); }} className="space-y-4">
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" />

در صورت دریافت پیامک، کد به صورت خودکار شناسایی و وارد می‌شود.

{countdown > 0 ? ( امکان ارسال مجدد کد تا {toPersian(countdown.toString())} ثانیه دیگر ) : ( )}
)} {/* 3. FORGOT PASSWORD PHONE SUB-VIEW */} {subView === "forgot-phone" && (
بازیابی رمز عبور
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" />
)} {/* 4. FORGOT PASSWORD OTP SUB-VIEW */} {subView === "forgot-otp" && (
کد تایید بازیابی رمز
{toPersian(phoneNumber)}
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" />
)} {/* 5. RESET PASSWORD SUB-VIEW */} {subView === "reset-password" && (
تنظیم رمز عبور جدید
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" />
)} {/* 6. B2B WHOLESALE REQUEST SUB-VIEW */} {subView === "b2b" && (
ثبت‌نام خریدار عمده و همکار
ثبت شماره نظام دامپزشکی یا پروانه کسب جهت دسترسی به قیمت‌های همکاری و سفارشات عمده الزامی است.
{ e.preventDefault(); toast.success("درخواست احراز هویت خریدار عمده با موفقیت ثبت شد و پس از بررسی فعال خواهد گردید."); setSubView("main"); }} className="space-y-3 font-vazir" >
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" />
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" />
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" />
{ 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 ? (
تصویر مدرک بارگذاری شد
) : (
بارگذاری تصویر کارت نظام یا پروانه کسب
)}
)}
)}
); }