import { useState } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import { useNavigate } from "@tanstack/react-router"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import { authWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { trackEvent } from "@executor-js/react/api/analytics"; import { Button } from "@executor-js/react/components/button"; import { AUTH_PATHS } from "../../auth/api"; import { acceptInvitation, pendingInvitationsAtom, useAuth } from "../auth"; import { CreateOrganizationFields, useCreateOrganizationForm, } from "../components/create-organization-form"; type PendingInvitation = { id: string; organizationId: string; organizationName: string; createdAt: string; inviter: { email: string; name: string | null } | null; }; const formatRelativeTime = (iso: string): string => { const then = new Date(iso).getTime(); if (Number.isNaN(then)) return ""; const diffMs = Date.now() - then; const min = Math.floor(diffMs / 60_000); if (min < 1) return "just now"; if (min < 60) return `${min}m ago`; const hrs = Math.floor(min / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); if (days < 7) return `${days}d ago`; const weeks = Math.floor(days / 7); if (weeks < 5) return `${weeks}w ago`; const months = Math.floor(days / 30); if (months < 12) return `${months}mo ago`; return `${Math.floor(days / 365)}y ago`; }; export const CreateOrgPage = () => { const auth = useAuth(); const navigate = useNavigate(); const invitationsResult = useAtomValue(pendingInvitationsAtom); const doAccept = useAtomSet(acceptInvitation, { mode: "promiseExit" }); const [acceptingId, setAcceptingId] = useState(null); const [errorByInvitationId, setErrorByInvitationId] = useState>({}); const handleAccept = async (invitation: PendingInvitation) => { setAcceptingId(invitation.id); setErrorByInvitationId((prev) => { if (!(invitation.id in prev)) return prev; const next = { ...prev }; delete next[invitation.id]; return next; }); const exit = await doAccept({ payload: { invitationId: invitation.id }, reactivityKeys: authWriteKeys, }); setAcceptingId(null); trackEvent("org_invitation_accepted", { success: Exit.isSuccess(exit) }); if (!Exit.isSuccess(exit)) { setErrorByInvitationId((prev) => ({ ...prev, [invitation.id]: "Couldn't accept this invitation. Try again or ask the inviter to resend.", })); } }; const suggestedName = auth.status === "authenticated" && auth.user.name != null && auth.user.name.trim() !== "" ? `${auth.user.name}'s Organization` : ""; const form = useCreateOrganizationForm({ defaultName: suggestedName, onSuccess: () => { void navigate({ to: "/setup-mcp" }); }, }); // Almost no new user has a pending invitation, so we never show a loading // placeholder for them — the create form renders immediately and stays put. // Invitations (when they exist) just appear above it once the fetch resolves; // a refetch (the create-org submit invalidates the auth-keyed atom) keeps the // last value, so nothing flickers. const invitations = AsyncResult.match(invitationsResult, { onInitial: () => [] as readonly PendingInvitation[], onFailure: () => [] as readonly PendingInvitation[], onSuccess: ({ value }) => value.invitations, }); const count = invitations.length; const sole = count === 1 ? invitations[0]! : null; return (

Step 1 of 2

{count > 0 ? "You've been invited" : "Create your organization"}

{count === 0 && (

Organizations group your sources, secrets, and teammates. You can invite others once it's set up.

)}
{sole && ( void handleAccept(sole)} /> )} {count > 1 && ( void handleAccept(inv)} /> )}
{/* oxlint-disable-next-line react/forbid-elements */}
); }; // --------------------------------------------------------------------------- const InviterAttribution = ({ invitation }: { invitation: PendingInvitation }) => { const inviterLabel = invitation.inviter ? invitation.inviter.name && invitation.inviter.name.length > 0 ? `${invitation.inviter.name} (${invitation.inviter.email})` : invitation.inviter.email : null; const time = formatRelativeTime(invitation.createdAt); return (

{inviterLabel ? ( <> Invited by {inviterLabel} {time && · {time}} ) : ( <>Invited{time ? ` ${time}` : ""} )}

); }; // --------------------------------------------------------------------------- const SingleInvitationView = ({ invitation, accepting, error, onAccept, }: { invitation: PendingInvitation; accepting: boolean; error: string | null; onAccept: () => void; }) => (

{invitation.organizationName}

{error &&

{error}

}
); // --------------------------------------------------------------------------- const MultiInvitationsView = ({ invitations, acceptingId, errorByInvitationId, onAccept, }: { invitations: readonly PendingInvitation[]; acceptingId: string | null; errorByInvitationId: Record; onAccept: (invitation: PendingInvitation) => void; }) => (
    {invitations.map((invitation) => { const isAccepting = acceptingId === invitation.id; const isOtherAccepting = acceptingId !== null && !isAccepting; const error = errorByInvitationId[invitation.id] ?? null; return (
  • {invitation.organizationName}

    {error &&

    {error}

    }
  • ); })}
); // --------------------------------------------------------------------------- const CreateOrgSection = ({ isPrimary, form, }: { isPrimary: boolean; form: ReturnType; }) => { const [expanded, setExpanded] = useState(isPrimary); if (!isPrimary && !expanded) { return (
{/* oxlint-disable-next-line react/forbid-elements */}
); } return (
{ form.setName(name); if (form.error) form.setError(null); }} error={form.error} onSubmit={() => void form.submit()} />
); };