import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useParams } from "react-router-dom"; import { accessApi } from "../api/access"; import { authApi } from "../api/auth"; import { healthApi } from "../api/health"; import { queryKeys } from "../lib/queryKeys"; import { Button } from "@/components/ui/button"; import type { JoinRequest } from "@paperclip/shared"; type JoinType = "human" | "agent"; function dateTime(value: string) { return new Date(value).toLocaleString(); } export function InviteLandingPage() { const queryClient = useQueryClient(); const params = useParams(); const token = (params.token ?? "").trim(); const [joinType, setJoinType] = useState("human"); const [agentName, setAgentName] = useState(""); const [adapterType, setAdapterType] = useState(""); const [capabilities, setCapabilities] = useState(""); const [result, setResult] = useState<{ kind: "bootstrap" | "join"; payload: unknown } | null>(null); const [error, setError] = useState(null); const healthQuery = useQuery({ queryKey: queryKeys.health, queryFn: () => healthApi.get(), retry: false, }); const sessionQuery = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), retry: false, }); const inviteQuery = useQuery({ queryKey: queryKeys.access.invite(token), queryFn: () => accessApi.getInvite(token), enabled: token.length > 0, retry: false, }); const invite = inviteQuery.data; const allowedJoinTypes = invite?.allowedJoinTypes ?? "both"; const availableJoinTypes = useMemo(() => { if (invite?.inviteType === "bootstrap_ceo") return ["human"] as JoinType[]; if (allowedJoinTypes === "both") return ["human", "agent"] as JoinType[]; return [allowedJoinTypes] as JoinType[]; }, [invite?.inviteType, allowedJoinTypes]); useEffect(() => { if (!availableJoinTypes.includes(joinType)) { setJoinType(availableJoinTypes[0] ?? "human"); } }, [availableJoinTypes, joinType]); const requiresAuthForHuman = joinType === "human" && healthQuery.data?.deploymentMode === "authenticated" && !sessionQuery.data; const acceptMutation = useMutation({ mutationFn: async () => { if (!invite) throw new Error("Invite not found"); if (invite.inviteType === "bootstrap_ceo") { return accessApi.acceptInvite(token, { requestType: "human" }); } if (joinType === "human") { return accessApi.acceptInvite(token, { requestType: "human" }); } return accessApi.acceptInvite(token, { requestType: "agent", agentName: agentName.trim(), adapterType: adapterType.trim() || undefined, capabilities: capabilities.trim() || null, }); }, onSuccess: async (payload) => { setError(null); await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session }); await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); const asBootstrap = payload && typeof payload === "object" && "bootstrapAccepted" in (payload as Record); setResult({ kind: asBootstrap ? "bootstrap" : "join", payload }); }, onError: (err) => { setError(err instanceof Error ? err.message : "Failed to accept invite"); }, }); if (!token) { return
Invalid invite token.
; } if (inviteQuery.isLoading || healthQuery.isLoading || sessionQuery.isLoading) { return
Loading invite...
; } if (inviteQuery.error || !invite) { return (

Invite not available

This invite may be expired, revoked, or already used.

); } if (result?.kind === "bootstrap") { return (

Bootstrap complete

The first instance admin is now configured. You can continue to the board.

); } if (result?.kind === "join") { const payload = result.payload as JoinRequest; return (

Join request submitted

Your request is pending admin approval. You will not have access until approved.

Request ID: {payload.id}
); } return (

{invite.inviteType === "bootstrap_ceo" ? "Bootstrap your Paperclip instance" : "Join this Paperclip company"}

Invite expires {dateTime(invite.expiresAt)}.

{invite.inviteType !== "bootstrap_ceo" && (
{availableJoinTypes.map((type) => ( ))}
)} {joinType === "agent" && invite.inviteType !== "bootstrap_ceo" && (