// Copyright: © 2026 TWWIM UG. All rights reserved. (www.twwim.com) /** * AcceptInvitationPage — landing page hit from the INVITATION_MEMBER email. * * Reads the `token` query param, prompts the invitee for a password * (optionally first/last name on a fresh account), POSTs to * /auth/accept-invitation, then immediately logs them in and routes to * their role's landing zone (OPERATOR → /dashboard/conversations, * ADMIN → /dashboard). */ import { useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; import { Mail, KeyRound } from 'lucide-react'; import { ACCEPT_INVITATION, LOGIN } from '@archer/api-interface/endpoints/customer-api'; import type { AcceptInvitationRequest, TokenResponse } from '@archer/api-interface'; import { apiClient } from '@/infrastructure/http/ApiClient'; import { AuthenticatedUserMapper } from '@/infrastructure/http/api/auth/mappers/AuthenticatedUserMapper'; import { authenticatedUserStore } from '@/infrastructure/storage/AuthenticatedUserStore'; import { tokenStorage } from '@/infrastructure/storage/LocalTokenStorage'; import { useTranslation } from '@/i18n/TranslationProvider'; interface AcceptInvitationPageProps { token: string; email?: string; } export function AcceptInvitationPage({ token, email: hintedEmail }: AcceptInvitationPageProps) { const { t } = useTranslation(); const navigate = useNavigate(); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [password, setPassword] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setError(null); setSubmitting(true); try { const acceptBody: AcceptInvitationRequest = { token, password, firstName: firstName.trim() || undefined, lastName: lastName.trim() || undefined, }; const accepted = await apiClient.post<{ success: true; authId: string; message: string }>( ACCEPT_INVITATION.path, acceptBody, ); if (!accepted.success) { throw new Error(t('acceptInvitation.rejected')); } // Auto-login after a successful accept so the invitee lands on their // dashboard directly instead of bouncing back to /login. const loginEmail = hintedEmail ?? promptForEmail(t); const loginResponse = await apiClient.post(LOGIN.path, { email: loginEmail, password, }); tokenStorage.setTokens(loginResponse.accessToken, loginResponse.refreshToken); const user = AuthenticatedUserMapper.fromTokenResponse(loginResponse); authenticatedUserStore.set(user); navigate({ to: '/dashboard' }); } catch (err) { setError(err instanceof Error ? err.message : t('acceptInvitation.failed')); } finally { setSubmitting(false); } }; return (
{t('acceptInvitation.badge')}

{t('acceptInvitation.title')}

{t('acceptInvitation.subtitle')}

{error && (
{error}
)}
setFirstName(event.target.value)} data-testid="accept-firstname" className="border border-gray-300 rounded-md px-3 py-2 text-sm" /> setLastName(event.target.value)} data-testid="accept-lastname" className="border border-gray-300 rounded-md px-3 py-2 text-sm" />
setPassword(event.target.value)} required minLength={12} data-testid="accept-password" className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm" />
); } function promptForEmail(t: (key: string) => string): string { const entered = typeof window !== 'undefined' ? window.prompt(t('acceptInvitation.emailPrompt')) : null; if (!entered) throw new Error(t('acceptInvitation.emailRequired')); return entered.trim(); }