import { useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { Eye, EyeOff, Smartphone, Lock, KeyRound, ArrowLeft, User } from 'lucide-react' import { useAuth, CUSTOMER_ACCESS_MESSAGE, } from '../context/AuthContext' import { useTenantBranding } from '../context/TenantBrandingContext' import { ApiError } from '../lib/api' import { toE164CellNumber } from '../lib/cellNumber' import { getTenantDomain } from '../lib/config' import { logout as logoutRequest, register, resetPassword, sendOtp, } from '../services/authService' import { LanguageSelect } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import meshkeeLogo from '../assets/meshkee-logo.png' import styles from './LoginPage.module.css' type AuthView = 'login' | 'signup' | 'forgot' | 'otp' type SmsStep = 'phone' | 'code' function safeRedirectPath(value: string | null) { if (!value || !value.startsWith('/') || value.startsWith('//')) { return '/' } return value } export function LoginPage() { const navigate = useNavigate() const [searchParams] = useSearchParams() const redirectTo = safeRedirectPath(searchParams.get('redirect')) const { login, loginWithOtp } = useAuth() const { businessName, businessNameEn, logoUrl } = useTenantBranding() const tenantDomain = getTenantDomain() 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 [confirmPassword, setConfirmPassword] = useState('') const [firstName, setFirstName] = useState('') const [lastName, setLastName] = useState('') const [smsCode, setSmsCode] = useState('') const [newPassword, setNewPassword] = 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('') setConfirmPassword('') setFirstName('') setLastName('') setSmsCode('') setNewPassword('') 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) { setError(err.message) } else { setError(fallback) } } async function handleSendCode() { clearMessages() setIsSubmitting(true) try { const cellNumber = toE164CellNumber(phone) const result = await sendOtp(cellNumber) 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() setIsSubmitting(true) try { const cellNumber = toE164CellNumber(phone) await login(cellNumber, password) navigate(redirectTo) } catch (err) { handleApiError(err, t('login.error.signIn')) } finally { setIsSubmitting(false) } } async function handleSignup(e: React.FormEvent) { e.preventDefault() clearMessages() if (password !== confirmPassword) { setError(t('signup.error.match')) return } if (password.length < 8) { setError(t('signup.error.length')) return } setIsSubmitting(true) try { const cellNumber = toE164CellNumber(phone) const data = await register({ cellNumber, password, firstName: firstName.trim(), lastName: lastName.trim(), domain: tenantDomain, }) if (data.user.customerBusinesses.length === 0) { logoutRequest() setError(CUSTOMER_ACCESS_MESSAGE) return } if (!data.user.cellVerifiedAt) { logoutRequest() setView('otp') setSmsStep('phone') setPassword('') setConfirmPassword('') setSmsCode('') setInfo(t('signup.info.verify')) try { const otpResult = await sendOtp(cellNumber) if (!otpResult.enabled) { setInfo(otpResult.message) } setCodeSent(true) setSmsStep('code') startCountdown() } catch (otpErr) { handleApiError(otpErr, t('login.error.sendCode')) } return } await login(cellNumber, password) navigate(redirectTo) } catch (err) { handleApiError(err, t('signup.error.create')) } finally { setIsSubmitting(false) } } async function handleResetPassword(e: React.FormEvent) { e.preventDefault() clearMessages() if (newPassword.length < 8) { setError(t('forgot.error.length')) return } setIsSubmitting(true) try { const cellNumber = toE164CellNumber(phone) 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() setIsSubmitting(true) try { const cellNumber = toE164CellNumber(phone) await loginWithOtp(cellNumber, smsCode) navigate(redirectTo) } catch (err) { handleApiError(err, t('otp.error.signIn')) } finally { setIsSubmitting(false) } } return (
{businessName || tenantDomain} {businessNameEn || tenantDomain}
{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')}

{t('login.noAccount')}{' '}

)} {view === 'signup' && ( <>

{t('signup.title')}

{t('signup.subtitle', { domain: tenantDomain })}

{error && (
{error}
)} {info &&
{info}
}
setFirstName(e.target.value)} required minLength={2} disabled={isSubmitting} />
setLastName(e.target.value)} required minLength={2} disabled={isSubmitting} />
setPhone(e.target.value)} required disabled={isSubmitting} />
setPassword(e.target.value)} required minLength={8} disabled={isSubmitting} />
setConfirmPassword(e.target.value)} required minLength={8} disabled={isSubmitting} />

{t('signup.hasAccount')}{' '}

)} {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} />
{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
) }