import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useParams } from "@/lib/router"; 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 { AGENT_ADAPTER_TYPES } from "@paperclipai/shared"; import type { AgentAdapterType, JoinRequest } from "@paperclipai/shared"; type JoinType = "human" | "agent"; const joinAdapterOptions: AgentAdapterType[] = [ "openclaw", ...AGENT_ADAPTER_TYPES.filter((type): type is Exclude => type !== "openclaw"), ]; const adapterLabels: Record = { claude_local: "Claude (local)", codex_local: "Codex (local)", opencode_local: "OpenCode (local)", openclaw: "OpenClaw", cursor: "Cursor (local)", process: "Process", http: "HTTP", }; const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local", "opencode_local", "cursor"]); function dateTime(value: string) { return new Date(value).toLocaleString(); } function readNestedString(value: unknown, path: string[]): string | null { let current: unknown = value; for (const segment of path) { if (!current || typeof current !== "object") return null; current = (current as Record)[segment]; } return typeof current === "string" && current.trim().length > 0 ? current : null; } 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("claude_local"); 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, 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 & { claimSecret?: string; claimApiKeyPath?: string; onboarding?: Record; diagnostics?: Array<{ code: string; level: "info" | "warn"; message: string; hint?: string; }>; }; const claimSecret = typeof payload.claimSecret === "string" ? payload.claimSecret : null; const claimApiKeyPath = typeof payload.claimApiKeyPath === "string" ? payload.claimApiKeyPath : null; const onboardingSkillUrl = readNestedString(payload.onboarding, ["skill", "url"]); const onboardingSkillPath = readNestedString(payload.onboarding, ["skill", "path"]); const onboardingInstallPath = readNestedString(payload.onboarding, ["skill", "installPath"]); const onboardingTextUrl = readNestedString(payload.onboarding, ["textInstructions", "url"]); const onboardingTextPath = readNestedString(payload.onboarding, ["textInstructions", "path"]); const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : []; return (

Join request submitted

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

Request ID: {payload.id}
{claimSecret && claimApiKeyPath && (

One-time claim secret (save now)

{claimSecret}

POST {claimApiKeyPath}

)} {(onboardingSkillUrl || onboardingSkillPath || onboardingInstallPath) && (

Paperclip skill bootstrap

{onboardingSkillUrl &&

GET {onboardingSkillUrl}

} {!onboardingSkillUrl && onboardingSkillPath &&

GET {onboardingSkillPath}

} {onboardingInstallPath &&

Install to {onboardingInstallPath}

}
)} {(onboardingTextUrl || onboardingTextPath) && (

Agent-readable onboarding text

{onboardingTextUrl &&

GET {onboardingTextUrl}

} {!onboardingTextUrl && onboardingTextPath &&

GET {onboardingTextPath}

}
)} {diagnostics.length > 0 && (

Connectivity diagnostics

{diagnostics.map((diag, idx) => (

[{diag.level}] {diag.message}

{diag.hint &&

{diag.hint}

}
))}
)}
); } 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" && (