import { useEffect, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared"; import type { AgentAdapterType, JoinRequest } from "@paperclipai/shared"; import { Button } from "@/components/ui/button"; import { CompanyPatternIcon } from "@/components/CompanyPatternIcon"; import { useCompany } from "@/context/CompanyContext"; import { Link, useNavigate, useParams } from "@/lib/router"; import { accessApi } from "../api/access"; import { authApi } from "../api/auth"; import { companiesApi } from "../api/companies"; import { healthApi } from "../api/health"; import { getAdapterLabel } from "../adapters/adapter-display-registry"; import { clearPendingInviteToken, rememberPendingInviteToken } from "../lib/invite-memory"; import { queryKeys } from "../lib/queryKeys"; import { formatDate } from "../lib/utils"; type AuthMode = "sign_in" | "sign_up"; type AuthFeedback = { tone: "error" | "info"; message: string }; const joinAdapterOptions: AgentAdapterType[] = [...AGENT_ADAPTER_TYPES]; const ENABLED_INVITE_ADAPTERS = new Set([ "claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor", ]); 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; } const fieldClassName = "w-full border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-zinc-500"; const panelClassName = "border border-zinc-800 bg-zinc-950/95 p-6"; const modeButtonBaseClassName = "flex-1 border px-3 py-2 text-sm transition-colors"; function formatHumanRole(role: string | null | undefined) { if (!role) return null; return role.charAt(0).toUpperCase() + role.slice(1); } function getAuthErrorCode(error: unknown) { if (!error || typeof error !== "object") return null; const code = (error as { code?: unknown }).code; return typeof code === "string" && code.trim().length > 0 ? code : null; } function getAuthErrorMessage(error: unknown) { if (!(error instanceof Error)) return null; const message = error.message.trim(); return message.length > 0 ? message : null; } function mapInviteAuthFeedback( error: unknown, authMode: AuthMode, email: string, ): AuthFeedback { const code = getAuthErrorCode(error); const message = getAuthErrorMessage(error); const emailLabel = email.trim().length > 0 ? email.trim() : "that email"; if (code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") { return { tone: "info", message: `An account already exists for ${emailLabel}. Sign in below to continue with this invite.`, }; } if (code === "INVALID_EMAIL_OR_PASSWORD") { return { tone: "error", message: "That email and password did not match an existing Paperclip account. Check both fields, or create an account first if you are new here.", }; } if (authMode === "sign_in" && message === "Request failed: 401") { return { tone: "error", message: "That email and password did not match an existing Paperclip account. Check both fields, or create an account first if you are new here.", }; } if (authMode === "sign_up" && message === "Request failed: 422") { return { tone: "info", message: `An account may already exist for ${emailLabel}. Try signing in instead.`, }; } return { tone: "error", message: message ?? "Authentication failed", }; } function isBootstrapAcceptancePayload(payload: unknown) { return Boolean( payload && typeof payload === "object" && "bootstrapAccepted" in (payload as Record), ); } function isApprovedHumanJoinPayload(payload: unknown, showsAgentForm: boolean) { if (!payload || typeof payload !== "object" || showsAgentForm) return false; const status = (payload as { status?: unknown }).status; return status === "approved"; } type AwaitingJoinApprovalPanelProps = { companyDisplayName: string; companyLogoUrl: string | null; companyBrandColor: string | null; invitedByUserName: string | null; claimSecret?: string | null; claimApiKeyPath?: string | null; onboardingTextUrl?: string | null; }; function InviteCompanyLogo({ companyDisplayName, companyLogoUrl, companyBrandColor, className, }: { companyDisplayName: string; companyLogoUrl: string | null; companyBrandColor: string | null; className?: string; }) { return ( ); } function AwaitingJoinApprovalPanel({ companyDisplayName, companyLogoUrl, companyBrandColor, invitedByUserName, claimSecret = null, claimApiKeyPath = null, onboardingTextUrl = null, }: AwaitingJoinApprovalPanelProps) { const approvalUrl = `${window.location.origin}/company/settings/access`; const approverLabel = invitedByUserName ?? "A company admin"; return (

Request to join {companyDisplayName}

Your request is still awaiting approval. {approverLabel} must approve your request to join.

Ask them to visit Company Settings → Access to approve your request.

Refresh this page after you've been approved — you'll be redirected automatically.

{claimSecret && claimApiKeyPath ? (
Claim secret
{claimSecret}
POST {claimApiKeyPath}
) : null} {onboardingTextUrl ? (
Onboarding: {onboardingTextUrl}
) : null}
); } export function InviteLandingPage() { const queryClient = useQueryClient(); const navigate = useNavigate(); const { setSelectedCompanyId } = useCompany(); const params = useParams(); const token = (params.token ?? "").trim(); const [authMode, setAuthMode] = useState("sign_up"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); 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 [authFeedback, setAuthFeedback] = useState(null); const [autoAcceptStarted, setAutoAcceptStarted] = useState(false); 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 companiesQuery = useQuery({ queryKey: queryKeys.companies.all, queryFn: () => companiesApi.list(), enabled: !!sessionQuery.data && !!inviteQuery.data?.companyId, retry: false, }); useEffect(() => { if (token) rememberPendingInviteToken(token); }, [token]); useEffect(() => { setAutoAcceptStarted(false); }, [token]); useEffect(() => { if (!companiesQuery.data || !inviteQuery.data?.companyId) return; const isMember = companiesQuery.data.some( (c) => c.id === inviteQuery.data!.companyId ); if (isMember) { clearPendingInviteToken(token); navigate("/", { replace: true }); } }, [companiesQuery.data, inviteQuery.data, token, navigate]); const invite = inviteQuery.data; const isCheckingExistingMembership = Boolean(sessionQuery.data) && Boolean(invite?.companyId) && companiesQuery.isLoading; const isCurrentMember = Boolean(invite?.companyId) && Boolean( companiesQuery.data?.some((company) => company.id === invite?.companyId), ); const companyName = invite?.companyName?.trim() || null; const companyDisplayName = companyName || "this Paperclip company"; const companyLogoUrl = invite?.companyLogoUrl?.trim() || null; const companyBrandColor = invite?.companyBrandColor?.trim() || null; const invitedByUserName = invite?.invitedByUserName?.trim() || null; const inviteMessage = invite?.inviteMessage?.trim() || null; const requestedHumanRole = formatHumanRole(invite?.humanRole); const inviteJoinRequestStatus = invite?.joinRequestStatus ?? null; const inviteJoinRequestType = invite?.joinRequestType ?? null; const requiresHumanAccount = healthQuery.data?.deploymentMode === "authenticated" && !sessionQuery.data && invite?.allowedJoinTypes !== "agent"; const showsAgentForm = invite?.inviteType !== "bootstrap_ceo" && invite?.allowedJoinTypes === "agent"; const shouldAutoAcceptHumanInvite = Boolean(sessionQuery.data) && !showsAgentForm && invite?.inviteType !== "bootstrap_ceo" && !inviteJoinRequestStatus && !isCheckingExistingMembership && !isCurrentMember && !result && error === null; const sessionLabel = sessionQuery.data?.user.name?.trim() || sessionQuery.data?.user.email?.trim() || "this account"; const authCanSubmit = email.trim().length > 0 && password.trim().length > 0 && (authMode === "sign_in" || (name.trim().length > 0 && password.trim().length >= 8)); const acceptMutation = useMutation({ mutationFn: async () => { if (!invite) throw new Error("Invite not found"); if (isCheckingExistingMembership) { throw new Error("Checking your company access. Try again in a moment."); } if (isCurrentMember) { throw new Error("This account already belongs to the company."); } if (invite.inviteType === "bootstrap_ceo" || invite.allowedJoinTypes !== "agent") { 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); clearPendingInviteToken(token); const asBootstrap = isBootstrapAcceptancePayload(payload); setResult({ kind: asBootstrap ? "bootstrap" : "join", payload }); await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session }); await queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); if (invite?.companyId && isApprovedHumanJoinPayload(payload, showsAgentForm)) { setSelectedCompanyId(invite.companyId, { source: "manual" }); navigate("/", { replace: true }); } }, onError: (err) => { setError(err instanceof Error ? err.message : "Failed to accept invite"); }, }); useEffect(() => { if (!shouldAutoAcceptHumanInvite || autoAcceptStarted || acceptMutation.isPending) return; setAutoAcceptStarted(true); setError(null); acceptMutation.mutate(); }, [acceptMutation, autoAcceptStarted, shouldAutoAcceptHumanInvite]); const authMutation = useMutation({ mutationFn: async () => { if (authMode === "sign_in") { await authApi.signInEmail({ email: email.trim(), password }); return; } await authApi.signUpEmail({ name: name.trim(), email: email.trim(), password, }); }, onSuccess: async () => { setAuthFeedback(null); rememberPendingInviteToken(token); await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session }); const companies = await queryClient.fetchQuery({ queryKey: queryKeys.companies.all, queryFn: () => companiesApi.list(), retry: false, }); if (invite?.companyId && companies.some((company) => company.id === invite.companyId)) { clearPendingInviteToken(token); setSelectedCompanyId(invite.companyId, { source: "manual" }); navigate("/", { replace: true }); return; } if (!invite || invite.inviteType !== "bootstrap_ceo") { return; } try { const payload = await acceptMutation.mutateAsync(); if (isBootstrapAcceptancePayload(payload)) { navigate("/", { replace: true }); } } catch { return; } }, onError: (err) => { const nextFeedback = mapInviteAuthFeedback(err, authMode, email); if (getAuthErrorCode(err) === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") { setAuthMode("sign_in"); setPassword(""); } setAuthFeedback(nextFeedback); }, }); const joinButtonLabel = useMemo(() => { if (!invite) return "Continue"; if (invite.inviteType === "bootstrap_ceo") return "Accept invite"; if (showsAgentForm) return "Submit request"; return sessionQuery.data ? "Accept invite" : "Continue"; }, [invite, sessionQuery.data, showsAgentForm]); if (!token) { return
Invalid invite token.
; } if (inviteQuery.isLoading || healthQuery.isLoading || sessionQuery.isLoading) { return
Loading invite...
; } if (isCheckingExistingMembership) { return
Checking your access...
; } if (inviteQuery.error || !invite) { return (

Invite not available

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

); } if ( inviteJoinRequestStatus === "approved" && inviteJoinRequestType === "human" && isCurrentMember ) { return
Opening company...
; } if (inviteJoinRequestStatus === "pending_approval") { return ( ); } if (inviteJoinRequestStatus) { return (

Invite not available

{inviteJoinRequestStatus === "rejected" ? "This join request was not approved." : "This invite has already been used."}

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

Bootstrap complete

); } if (result?.kind === "join") { const payload = result.payload as JoinRequest & { claimSecret?: string; claimApiKeyPath?: string; onboarding?: Record; }; const claimSecret = typeof payload.claimSecret === "string" ? payload.claimSecret : null; const claimApiKeyPath = typeof payload.claimApiKeyPath === "string" ? payload.claimApiKeyPath : null; const onboardingTextUrl = readNestedString(payload.onboarding, ["textInstructions", "url"]); const joinedNow = !showsAgentForm && payload.status === "approved"; return ( joinedNow ? (

You joined the company

) : ( ) ); } return (

You've been invited to join Paperclip

{invite.inviteType === "bootstrap_ceo" ? "Set up Paperclip" : `Join ${companyDisplayName}`}

{showsAgentForm ? "Review the invite details, then submit the agent information below to start the join request." : requiresHumanAccount ? "Create your Paperclip account first. If you already have one, switch to sign in and continue the invite with the same email." : "Your account is ready. Review the invite details, then accept it to continue."}

Company
{companyDisplayName}
Invited by
{invitedByUserName ?? "Paperclip board"}
Requested access
{showsAgentForm ? "Agent join request" : requestedHumanRole ?? "Company access"}
Invite expires
{formatDate(invite.expiresAt)}
{inviteMessage ? (
Message from inviter

{inviteMessage}

) : null} {sessionQuery.data ? (
Signed in as {sessionLabel}.
) : null}
{showsAgentForm ? (

Submit agent details

This invite will create an approval request for a new agent in {companyDisplayName}.