import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { Eye, EyeOff, Smartphone, Lock, KeyRound, ArrowLeft } from 'lucide-react' import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong } from '@meshkee/dashboard-ui' import { useAuth, BUSINESS_ACCESS_MESSAGE } from '../context/AuthContext' import { useTenantBranding } from '../context/TenantBrandingContext' import { ApiError } from '../lib/api' import { toE164CellNumber } from '../lib/cellNumber' import { getBusinessDomain } from '../lib/config' import { resetPassword, sendOtp } from '../services/authService' import { useT } from '../i18n/useT' import styles from './LoginPage.module.css' type AuthView = 'login' | 'forgot' | 'otp' type SmsStep = 'phone' | 'code' export function LoginPage() { const navigate = useNavigate() const { login, loginWithOtp } = useAuth() const { businessName, businessNameEn, logoUrl } = useTenantBranding() const businessDomain = getBusinessDomain() const t = useT() const [view, setView] = useState('login') const [smsStep, setSmsStep] = useState('phone') const [showPassword, setShowPassword] = useState(false) const [phone, setPhone] = useState('') const [password, setPassword] = useState('') const [smsCode, setSmsCode] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmNewPassword, setConfirmNewPassword] = useState('') const [codeSent, setCodeSent] = useState(false) const [countdown, setCountdown] = useState(0) const [error, setError] = useState('') const [info, setInfo] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) function clearMessages() { setError('') setInfo('') } function resetForm() { setPhone('') setPassword('') setSmsCode('') setNewPassword('') setConfirmNewPassword('') setSmsStep('phone') setCodeSent(false) setShowPassword(false) clearMessages() } function switchView(next: AuthView) { resetForm() setView(next) } function startCountdown() { setCountdown(60) const timer = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { clearInterval(timer) return 0 } return prev - 1 }) }, 1000) } function handleApiError(err: unknown, fallback: string) { if (err instanceof ApiError) { if (err.message === BUSINESS_ACCESS_MESSAGE) { setError(t('login.error.access')) } else if ( err.message.includes('E.164') || err.message.includes('cellNumber must be') ) { setError(t('login.error.cellEnglishDigits')) } else if ( err.message.includes('SMS provider is not configured') || err.message.includes('SMS provider is unreachable') || err.message.includes('SMS provider returned') || err.message.includes('Unable to send SMS') ) { setError(t('login.error.smsProvider')) } else { setError(err.message) } } else { setError(fallback) } } function resolveCellNumber(): string | null { if (/[۰-۹٠-٩]/.test(phone)) { setError(t('login.error.cellEnglishDigits')) return null } const cellNumber = toE164CellNumber(phone) if (!/^\+[1-9]\d{6,14}$/.test(cellNumber)) { setError(t('login.error.cellEnglishDigits')) return null } return cellNumber } async function handleSendCode() { clearMessages() const cellNumber = resolveCellNumber() if (!cellNumber) return setIsSubmitting(true) try { const result = await sendOtp(cellNumber, businessDomain) if (!result.enabled) { setInfo(result.message) } setCodeSent(true) setSmsStep('code') startCountdown() } catch (err) { handleApiError(err, t('login.error.sendCode')) } finally { setIsSubmitting(false) } } async function handleLogin(e: React.FormEvent) { e.preventDefault() clearMessages() const cellNumber = resolveCellNumber() if (!cellNumber) return setIsSubmitting(true) try { await login(cellNumber, password) navigate('/') } catch (err) { handleApiError(err, t('login.error.signIn')) } finally { setIsSubmitting(false) } } async function handleResetPassword(e: React.FormEvent) { e.preventDefault() clearMessages() if (!isPasswordStrong(newPassword)) { setError(t('forgot.error.strength')) return } if (newPassword !== confirmNewPassword) { setError(t('forgot.error.match')) return } const cellNumber = resolveCellNumber() if (!cellNumber) return setIsSubmitting(true) try { await resetPassword(cellNumber, smsCode, newPassword) setInfo(t('forgot.info.success')) setTimeout(() => switchView('login'), 2000) } catch (err) { handleApiError(err, t('forgot.error.verify')) } finally { setIsSubmitting(false) } } async function handleOtpLogin(e: React.FormEvent) { e.preventDefault() clearMessages() const cellNumber = resolveCellNumber() if (!cellNumber) return setIsSubmitting(true) try { await loginWithOtp(cellNumber, smsCode) navigate('/') } catch (err) { handleApiError(err, t('otp.error.signIn')) } finally { setIsSubmitting(false) } } return (
{logoUrl ? : null}
{businessName || businessDomain} {businessNameEn || businessDomain}
{view === 'login' && ( <>

{t('login.welcome')}

{t('login.subtitle')}

{error && (
{error}
)} {info &&
{info}
}
setPhone(e.target.value)} required disabled={isSubmitting} />
setPassword(e.target.value)} required minLength={8} disabled={isSubmitting} />
{t('login.or')}
)} {view === 'forgot' && ( <>

{t('forgot.title')}

{smsStep === 'phone' ? t('forgot.subtitlePhone') : t('forgot.subtitleCode')}

{error && (
{error}
)} {info &&
{info}
} {smsStep === 'phone' ? ( <>
setPhone(e.target.value)} required disabled={isSubmitting} />
) : ( <> {codeSent && (

{t('common.codeSent', { phone })}

)}
setSmsCode(e.target.value.replace(/\D/g, ''))} required disabled={isSubmitting} />
setNewPassword(e.target.value)} required minLength={8} disabled={isSubmitting} aria-describedby="forgot-password-strength" />
setConfirmNewPassword(e.target.value)} required minLength={8} disabled={isSubmitting} />
{countdown > 0 ? ( {t('common.resendIn', { seconds: countdown })} ) : ( )}
)}
)} {view === 'otp' && ( <>

{t('otp.title')}

{smsStep === 'phone' ? t('otp.subtitlePhone') : t('otp.subtitleCode')}

{error && (
{error}
)} {info &&
{info}
} {smsStep === 'phone' ? ( <>
setPhone(e.target.value)} required disabled={isSubmitting} />
) : ( <> {codeSent && (

{t('common.codeSent', { phone })}

)}
setSmsCode(e.target.value.replace(/\D/g, ''))} required disabled={isSubmitting} />
{countdown > 0 ? ( {t('common.resendIn', { seconds: countdown })} ) : ( )}
)}
)}
Powered by Meshkee.app
) }