"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, } 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 } 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(); const [view, setView] = useState< | "login" | "register" | "otp-phone" | "otp" | "forgot-password" | "forgot-otp" | "reset-password" | "wholesale-request" >("login"); const [medicalLicense, setMedicalLicense] = useState(""); const [businessName, setBusinessName] = useState(""); const [documentUploaded, setDocumentUploaded] = useState(false); 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 [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]); // Robust focus retry on view === "otp" useEffect(() => { if (view === "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)); } }, [view]); // Reset modal state on open/close useEffect(() => { if (!isOpen) { Promise.resolve().then(() => { setView("login"); setPhoneNumber(""); setPassword(""); setNewPassword(""); setEmail(""); setFirstName(""); setLastName(""); setOtpCode(""); 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 (view !== "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) { triggerVerifyOtp(clean); } } } }) .catch(() => {}); } // Direct DOM input / change listener for mobile keyboard autofill compatibility 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) { 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); } }; }, [view, 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) { triggerVerifyOtp(clean); } }; const handleLogin = 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 || !lastName || !cleanPhone || !password) { 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 handleSendOtp = 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("کد تایید پیامک شد"); } setView("otp"); setCountdown(120); } catch (err: unknown) { const errObj = err as { message?: string }; toast.error(errObj.message || "خطا در ارسال کد تایید"); } finally { setIsLoading(false); } }; const handleVerifyOtp = async (e: React.FormEvent) => { e.preventDefault(); triggerVerifyOtp(otpCode); }; 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("کد بازیابی رمز پیامک شد"); } setView("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); setView("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 && (
{/* Header */}
ورود امن به کنینا
{view === "login" && (
setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider" dir="ltr" />
setPassword(e.target.value)} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider" dir="ltr" />
حساب کاربری ندارید؟
{isB2BEnabled && (
)}
)} {view === "wholesale-request" && ( { e.preventDefault(); toast.success( "درخواست احراز هویت خریدار عمده با موفقیت ارسال شد و پس از بررسی ادمین تایید خواهد شد.", ); setView("login"); }} className="space-y-3" >
ثبت شماره نظام دامپزشکی یا پروانه کسب جهت دسترسی به قیمت‌ها و سفارش حجمی الزامی است.
setBusinessName(e.target.value)} placeholder="مثال: کلینیک دامپزشکی رازی" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm" />
setMedicalLicense(e.target.value)} placeholder="مثال: ۹۸۷۶۵۴۳۲۱" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono" 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 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono" dir="ltr" />
{ setDocumentUploaded(true); toast.success("تصویر پروانه با موفقیت بارگذاری شد."); }} className={`p-4 border-2 border-dashed rounded-xl text-center cursor-pointer transition-all ${ documentUploaded ? "border-green-500 bg-green-50" : "border-medical-gray-300 hover:border-canina-blue" }`} > {documentUploaded ? (
تصویر مدرک بارگذاری شد
) : (
بارگذاری تصویر کارت نظام / پروانه
)}
)} {view === "register" && (
setFirstName(e.target.value)} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm" />
setLastName(e.target.value)} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm" />
setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider" dir="ltr" />
setEmail(e.target.value)} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono" dir="ltr" />
setPassword(e.target.value)} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 pr-3 pl-10 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono" dir="ltr" />
)} {view === "otp-phone" && (
setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))} disabled={isLoading} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-lg text-left font-mono tracking-widest" dir="ltr" />
)} {view === "otp" && (
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 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-center text-2xl font-mono tracking-[0.5em]" dir="ltr" />

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

{countdown > 0 ? ( ارسال مجدد کد تا {toPersian(countdown.toString())} ثانیه دیگر ) : ( )}
)} {view === "forgot-password" && (
setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider" dir="ltr" />
)} {view === "forgot-otp" && (
setOtpCode(toEnglishDigits(e.target.value).replace(/[^0-9]/g, "").slice(0, 5))} onInput={(e) => setOtpCode(toEnglishDigits((e.target as HTMLInputElement).value).replace(/[^0-9]/g, "").slice(0, 5))} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-center font-mono font-bold text-xl tracking-[0.4em]" dir="ltr" />
)} {view === "reset-password" && (
setNewPassword(e.target.value)} className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono" dir="ltr" />
)}
)}
); }