import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { useMutation } from "@tanstack/react-query"; import { UserPlus, AlertCircle, CheckCircle, LogIn } from "lucide-react"; import { Button } from "@/client/components/ui/button"; import { BUTTON_BASE_CLASSES, BUTTON_SIZE_CLASSES, BUTTON_VARIANT_CLASSES, } from "@/client/components/ui/button-classes"; import { Skeleton } from "@/client/components/ui/skeleton"; import { api, toApiError } from "@/client/lib/api"; import { useAuthStore, selectIsAuthenticated } from "@/client/stores/auth-store"; import { ensureWorkspaceLocalOwner } from "@/client/stores/bootstrap"; import { directoryCommands } from "@/client/stores/db/workspace-directory"; import { useDocumentTitle } from "@/client/hooks/use-document-title"; import type { InvitePreview } from "@/shared/types"; function buildOidcStartUrl(token: string): string { const params = new URLSearchParams({ return_to: `/invite/${token}?accept=1` }); return `/api/v1/oidc/start?${params.toString()}`; } export function InvitePage() { const { token } = useParams({ strict: false }) as { token: string }; const { accept: acceptMarker } = useSearch({ from: "/invite/$token" }); const navigate = useNavigate(); useDocumentTitle("Accept Invite"); const isAuthenticated = useAuthStore(selectIsAuthenticated); const [invite, setInvite] = useState(null); const [loadError, setLoadError] = useState(null); const [isLoadingInvite, setIsLoadingInvite] = useState(true); useEffect(() => { let cancelled = false; async function loadInvite() { try { const data = await api.invites.get(token); if (!cancelled) { setInvite(data); } } catch (err) { if (!cancelled) { setLoadError(toApiError(err).message); } } finally { if (!cancelled) setIsLoadingInvite(false); } } loadInvite(); return () => { cancelled = true; }; }, [token]); const acceptMutation = useMutation({ mutationFn: () => api.invites.accept(token), onSuccess: async (result) => { useAuthStore.getState().setAuth(result.accessToken, result.user); await ensureWorkspaceLocalOwner(result.user.id, true); const workspaceData = await api.workspaces.list(); await directoryCommands.replaceAll(workspaceData); const joined = workspaceData.find((w) => w.id === result.workspace_id); if (joined) { navigate({ to: "/$workspaceSlug", params: { workspaceSlug: joined.slug } }); } else { navigate({ to: "/" }); } }, }); const autoAcceptedRef = useRef(false); useEffect(() => { if (autoAcceptedRef.current) return; if (acceptMarker !== "1") return; if (!isAuthenticated) return; autoAcceptedRef.current = true; acceptMutation.mutate(); }, [acceptMarker, isAuthenticated, acceptMutation]); const acceptError = acceptMutation.error ? toApiError(acceptMutation.error).message : null; if (isLoadingInvite) { return (
); } if (loadError) { return (

Invalid Invite

{loadError}

); } const oidcStartUrl = buildOidcStartUrl(token); return (

You're invited

Join {invite?.workspace_name} as a {invite?.role}

{acceptError && ( )} {!isAuthenticated ? ( Sign in with tessera to accept ) : ( )}
); }