import { useEffect, useState } from 'react'; import { AlertCircle, CheckCircle2, Loader2, Lock, Mail } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import langbotIcon from '@/app/assets/langbot-logo.webp'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { LanguageSelector } from '@/components/ui/language-selector'; import { ThemeToggle } from '@/components/ui/theme-toggle'; import type { Workspace, WorkspaceInvitation, } from '@/app/infra/entities/workspace'; import { backendClient, bootstrapWorkspaceSession, clearPendingInvitationToken, clearUserInfo, getPendingInvitationToken, setPendingInvitationToken, } from '@/app/infra/http'; type InvitationView = { invitation: WorkspaceInvitation; workspace: Workspace; }; const TERMINAL_INVITATION_ERROR_CODES = new Set([ 'invitation_invalid', 'invitation_expired', 'invitation_revoked', 'invitation_used', 'invitation_email_mismatch', ]); function invitationErrorKey( code: string | null | undefined, fallback: 'workspace.invitationInvalid' | 'workspace.invitationAcceptFailed', ) { switch (code) { case 'invitation_invalid': return 'workspace.invitationInvalid'; case 'invitation_expired': return 'workspace.invitationExpired'; case 'invitation_revoked': return 'workspace.invitationAlreadyRevoked'; case 'invitation_used': return 'workspace.invitationAlreadyUsed'; case 'invitation_email_mismatch': return 'workspace.invitationEmailMismatch'; default: return fallback; } } function captureInvitationTokenFromFragment(): string | null { if (typeof window === 'undefined') return null; const fragment = new URLSearchParams(window.location.hash.slice(1)); const token = fragment.get('token')?.trim(); if (!token) return getPendingInvitationToken(); setPendingInvitationToken(token); window.history.replaceState( null, document.title, `${window.location.pathname}${window.location.search}`, ); return token; } export default function AcceptInvitationPage() { const { t } = useTranslation(); const navigate = useNavigate(); const [invitationHash, setInvitationHash] = useState(() => typeof window === 'undefined' ? '' : window.location.hash, ); const [token, setToken] = useState(null); const [view, setView] = useState(null); const [status, setStatus] = useState< 'loading' | 'ready' | 'submitting' | 'success' | 'error' >('loading'); const [errorMessage, setErrorMessage] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [passwordRegistrationEnabled, setPasswordRegistrationEnabled] = useState(false); useEffect(() => { const handleHashChange = () => setInvitationHash(window.location.hash); window.addEventListener('hashchange', handleHashChange); return () => window.removeEventListener('hashchange', handleHashChange); }, []); useEffect(() => { setStatus('loading'); setView(null); setErrorMessage(''); const invitationToken = captureInvitationTokenFromFragment(); const deferredErrorCode = new URLSearchParams(window.location.search).get( 'error', ); setToken(invitationToken); backendClient .getAccountInfo() .then((info) => { setPasswordRegistrationEnabled(info.password_login_enabled !== false); }) .catch(() => setPasswordRegistrationEnabled(false)); if (!invitationToken) { setErrorMessage(t('workspace.invitationMissing')); setStatus('error'); return; } let cancelled = false; backendClient .inspectWorkspaceInvitation(invitationToken) .then((response) => { if (cancelled) return; setView(response); if (deferredErrorCode) { setErrorMessage( t( invitationErrorKey( deferredErrorCode, 'workspace.invitationAcceptFailed', ), ), ); setStatus('error'); } else { setStatus('ready'); } }) .catch((error: { code?: string; msg?: string }) => { if (cancelled) return; clearPendingInvitationToken(); setErrorMessage( t(invitationErrorKey(error.code, 'workspace.invitationInvalid')), ); setStatus('error'); }); return () => { cancelled = true; }; }, [invitationHash, t]); async function finishAcceptance(registration?: { email: string; password: string; }) { if (!token) return; setStatus('submitting'); setErrorMessage(''); try { const response = await backendClient.acceptWorkspaceInvitation( token, registration, ); if (registration) { clearPendingInvitationToken(); toast.success(t('workspace.invitationAccepted')); navigate('/login?invitation=1', { replace: true }); return; } clearPendingInvitationToken(); const workspaceResult = await bootstrapWorkspaceSession({ preferredWorkspaceUuid: response.workspace_uuid, }); if (workspaceResult.status !== 'ready') { throw new Error('Accepted Workspace could not be initialized'); } setStatus('success'); toast.success(t('workspace.invitationAccepted')); window.setTimeout(() => navigate('/home', { replace: true }), 600); } catch (error) { const apiError = error as { code?: string; msg?: string }; if (apiError.code === 'account_exists_login_required') { setStatus('ready'); setErrorMessage(t('workspace.existingAccountLoginRequired')); return; } setErrorMessage( t( invitationErrorKey(apiError.code, 'workspace.invitationAcceptFailed'), ), ); setStatus( apiError.code && TERMINAL_INVITATION_ERROR_CODES.has(apiError.code) ? 'error' : 'ready', ); } } function registerAndAccept() { if (!view) return; if (password.length < 8) { setErrorMessage(t('workspace.passwordMinimum')); return; } if (password !== confirmPassword) { setErrorMessage(t('workspace.passwordMismatch')); return; } void finishAcceptance({ email: view.invitation.normalized_email, password, }); } function logoutAndReturn() { if (token) setPendingInvitationToken(token); clearUserInfo(); if (typeof window !== 'undefined') { localStorage.removeItem('token'); localStorage.removeItem('userEmail'); } navigate('/login?invitation=1', { replace: true }); } function returnToLogin() { clearUserInfo(); if (typeof window !== 'undefined') { localStorage.removeItem('token'); localStorage.removeItem('userEmail'); } navigate('/login', { replace: true }); } const hasLoginToken = typeof window !== 'undefined' && Boolean(localStorage.getItem('token')); return (
LangBot {t('workspace.acceptInvitation')} {view ? t('workspace.invitedToWorkspace', { workspace: view.workspace.name, }) : t('workspace.checkingInvitation')}
{status === 'loading' && (
)} {status === 'success' && (

{t('workspace.invitationAccepted')}

)} {status === 'error' && (
{errorMessage}
)} {(status === 'ready' || status === 'submitting') && view && (
{errorMessage && (
{errorMessage}
)} {hasLoginToken ? (
{t('workspace.authenticatedInvitationNotice')}
) : passwordRegistrationEnabled ? ( <>
setPassword(event.target.value)} className="pl-10" autoComplete="new-password" />
setConfirmPassword(event.target.value) } autoComplete="new-password" />
) : ( )}
)}
); }