"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 = {}; 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 ( {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">
triggerVerifyOtp(code)} />

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

{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(val)} disabled={isLoading} autoFocus={true} />
)} {/* 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 ? (
تصویر مدرک بارگذاری شد
) : (
بارگذاری تصویر کارت نظام یا پروانه کسب
)}
)}
)}
); }