'use client' import Image from 'next/image' import Link from 'next/link' import { useEffect, useRef, useState } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useDashboardI18n } from '@/components/I18nProvider' import PublicShell from '@/components/layout/PublicShell' import { adminUrl, marketplaceUrl } from '@/lib/urls' import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api' import { toPublicDashboardPath } from '@/lib/dashboardPaths' const DASHBOARD_LOGO_SRC = '/dashboard/rentalcardrive.png' function notifyParent(message: Record) { if (typeof window === 'undefined' || window.parent === window) return window.parent.postMessage(message, '*') } export default function SignInPageClient({ embedded = false }: { embedded?: boolean }) { const { language, theme, setLanguage, setTheme } = useDashboardI18n() const pathname = usePathname() const router = useRouter() const searchParams = useSearchParams() const requestedLanguage = searchParams.get('lang') const requestedTheme = searchParams.get('theme') const initializedFromQuery = useRef(false) const dict = { en: { brandName: 'Space Agency', title: 'Sign in', subtitle: 'Enter your credentials to access your account.', email: 'Email', password: 'Password', signIn: 'Sign in', signingIn: 'Signing in…', verify: 'Verify code', verifying: 'Verifying…', authCode: 'Authentication code', enterCode: 'Enter the 6-digit code from your authenticator app.', back: 'Back to credentials', forgotPassword: 'Forgot your password?', invalidCredentials: 'Invalid email or password.', passwordNotSet: 'No password set yet. Check your invitation email or use "Forgot your password?"', unexpectedError: 'Something went wrong. Please try again.', }, fr: { brandName: 'Espace agence', title: 'Connexion', subtitle: 'Saisissez vos identifiants pour accéder à votre compte.', email: 'Email', password: 'Mot de passe', signIn: 'Connexion', signingIn: 'Connexion…', verify: 'Vérifier le code', verifying: 'Vérification…', authCode: "Code d'authentification", enterCode: "Entrez le code à 6 chiffres de votre application d'authentification.", back: 'Retour aux identifiants', forgotPassword: 'Mot de passe oublié ?', invalidCredentials: 'Adresse e-mail ou mot de passe invalide.', passwordNotSet: `Aucun mot de passe défini. Vérifiez votre e-mail d'invitation ou utilisez « Mot de passe oublié ? »`, unexpectedError: 'Une erreur est survenue. Veuillez réessayer.', }, ar: { brandName: 'مساحة الوكالة', title: 'تسجيل الدخول', subtitle: 'أدخل بياناتك للوصول إلى حسابك.', email: 'البريد الإلكتروني', password: 'كلمة المرور', signIn: 'تسجيل الدخول', signingIn: 'جارٍ تسجيل الدخول…', verify: 'تحقق من الرمز', verifying: 'جارٍ التحقق…', authCode: 'رمز المصادقة', enterCode: 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.', back: 'العودة إلى بيانات الدخول', forgotPassword: 'نسيت كلمة المرور؟', invalidCredentials: 'البريد الإلكتروني أو كلمة المرور غير صحيحة.', passwordNotSet: 'لم يتم تعيين كلمة مرور بعد. تحقق من بريد الدعوة أو استخدم "نسيت كلمة المرور؟"', unexpectedError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.', }, }[language] useEffect(() => { if (initializedFromQuery.current) return if ( (requestedLanguage === 'en' || requestedLanguage === 'fr' || requestedLanguage === 'ar') && requestedLanguage !== language ) { setLanguage(requestedLanguage) } if ( (requestedTheme === 'light' || requestedTheme === 'dark') && requestedTheme !== theme ) { setTheme(requestedTheme) } initializedFromQuery.current = true }, [language, requestedLanguage, requestedTheme, setLanguage, setTheme, theme]) useEffect(() => { if (!initializedFromQuery.current) return const params = new URLSearchParams(searchParams.toString()) let changed = false if (params.get('lang') !== language) { params.set('lang', language) changed = true } if (params.get('theme') !== theme) { params.set('theme', theme) changed = true } // useSearchParams() can return empty params during hydration without a Suspense boundary. // Always preserve embedded=1 when the component was server-rendered as embedded so // the router.replace doesn't strip it and trigger an unintended redirect. if (embedded && params.get('embedded') !== '1') { params.set('embedded', '1') changed = true } if (!changed) return const nextQuery = params.toString() router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, { scroll: false }) }, [embedded, language, pathname, router, searchParams, theme]) useEffect(() => { const currentPath = window.location.pathname + (window.location.search || '') notifyParent({ type: 'rentaldrivego:embedded-path', path: currentPath }) }, [pathname, searchParams]) return (

RentalDriveGo

{dict.brandName}

{dict.subtitle}

{(['en', 'fr', 'ar'] as const).map((lang) => ( ))}
) } function LocalSignInForm({ dict, }: { dict: { email: string password: string signIn: string signingIn: string verify: string verifying: string authCode: string enterCode: string back: string forgotPassword: string invalidCredentials: string passwordNotSet: string unexpectedError: string } }) { const searchParams = useSearchParams() const { setLanguage } = useDashboardI18n() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [totpCode, setTotpCode] = useState('') const [step, setStep] = useState<'credentials' | 'totp'>('credentials') const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const adminNext = searchParams.get('next') || '/dashboard' const employeeRedirect = searchParams.get('redirect') || '/dashboard' function redirectAdmin(token: string) { const hash = new URLSearchParams({ token, next: adminNext }).toString() window.location.href = `${adminUrl}/auth-redirect#${hash}` } async function handleCredentials(e: React.FormEvent) { e.preventDefault() setLoading(true) setError(null) try { const empRes = await fetch(`${API_BASE}/auth/employee/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }) const empJson = await empRes.json() if (empRes.ok && empJson?.data?.employee) { const targetPath = toPublicDashboardPath(employeeRedirect) if (empJson?.data?.employee) { localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(empJson.data.employee)) const prefLang = empJson.data.employee?.preferredLanguage if (prefLang === 'en' || prefLang === 'fr' || prefLang === 'ar') { setLanguage(prefLang) document.cookie = `rentaldrivego-language=${prefLang}; path=/; max-age=31536000; samesite=lax` } } window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed')) notifyParent({ type: 'rentaldrivego:employee-login', path: targetPath }) // Use a document navigation here so the authenticated dashboard bootstraps // from a fresh request after the token cookie and localStorage are set. window.location.replace(targetPath) return } if (empJson?.error === 'password_not_set') { setError(dict.passwordNotSet) return } const adminRes = await fetch(`${API_BASE}/admin/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }) const adminJson = await adminRes.json() if (adminRes.ok && adminJson?.data?.admin) { window.location.href = `${adminUrl}/dashboard` return } if (adminRes.status === 401 && adminJson?.error === 'totp_required') { setStep('totp') return } setError(dict.invalidCredentials) } catch { setError(dict.unexpectedError) } finally { setLoading(false) } } async function handleTotp(e: React.FormEvent) { e.preventDefault() setLoading(true) setError(null) try { const normalizedCode = totpCode.trim().toUpperCase() const secondFactor = /^\d{6}$/.test(normalizedCode) ? { totpCode: normalizedCode } : { recoveryCode: normalizedCode } const adminRes = await fetch(`${API_BASE}/admin/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password, ...secondFactor }), }) const adminJson = await adminRes.json() if (adminRes.ok && adminJson?.data?.admin) { window.location.href = `${adminUrl}/dashboard` return } setError(dict.invalidCredentials) } catch { setError(dict.unexpectedError) } finally { setLoading(false) } } return ( <> {error ? (
{error}
) : null} {step === 'credentials' ? (
setEmail(e.target.value)} placeholder="owner@company.com" className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" />
setPassword(e.target.value)} placeholder="••••••••" className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 pr-10 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" />
{dict.forgotPassword}
) : (
{dict.enterCode}
setTotpCode(e.target.value.replace(/[^0-9A-Za-z-]/g, '').toUpperCase())} placeholder="000000 or XXXX-XXXX-XXXX" className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-center text-xl tracking-[0.45em] text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" />
)} ) }