import { IconAlertTriangle, IconKey, IconLoader2, IconUserPlus, IconAt, IconUsersGroup, } from "@tabler/icons-react"; import { ReactNode, useState } from "react"; import { ErrorReportActions } from "../ErrorReportActions.js"; import { useT } from "../i18n.js"; import { useAcceptInvitation, useCreateOrg, useJoinByDomain, useOrg, } from "./hooks.js"; export interface RequireActiveOrgProps { children: ReactNode; /** * Override the heading shown on the create-org pane. Default: "Create your organization". */ title?: string; /** * Override the description shown below the heading. Default explains that * an org is required to use the app. */ description?: string; /** Optional extra classes on the blocking pane wrapper. */ className?: string; } /** * Guards its children behind the user having an active organization. * * When the user has no active org, renders a blocking, centered pane in place * of `children` with: * 1. Any pending invitations (one-click accept), and * 2. A "Create your organization" form. * * As soon as an org is joined or created, `useOrg` refetches and `children` * renders normally. * * The pane fills whatever box this component is rendered into — it does **not** * position itself `fixed` over the viewport. Place it inside your app shell so * ambient UI (agent sidebar, global nav) stays accessible while the user * completes org setup. */ export function RequireActiveOrg({ children, title, description, className, }: RequireActiveOrgProps) { const t = useT(); const { data: org, isLoading, isError, error, refetch } = useOrg(); if (isLoading) return null; // Network / server failure on the org lookup — do NOT fall through to the // create-org pane (that would lock out an existing member on a transient // 500). Render a retry state instead. Only treat a successful null orgId // response as "genuinely no org". if (isError) { return ( void refetch()} className={className} /> ); } if (org?.orgId) return <>{children}; return ( ); } function ErrorPane({ message, onRetry, className, }: { message: string; onRetry: () => void; className?: string; }) { const t = useT(); return (

{t("org.loadErrorTitle")}

{message}

); } function CreateOrgPane({ pendingInvitations, domainMatches, email, title, description, className, }: { pendingInvitations: Array<{ id: string; orgId: string; orgName: string; invitedBy: string; }>; domainMatches: Array<{ orgId: string; orgName: string }>; email: string; title: string; description: string; className?: string; }) { const t = useT(); const createOrg = useCreateOrg(); const acceptInvitation = useAcceptInvitation(); const joinByDomain = useJoinByDomain(); const [name, setName] = useState(""); const hasInvites = pendingInvitations.length > 0; const hasDomainMatches = domainMatches.length > 0; const userDomain = email.split("@")[1] ?? ""; const [showCreateForm, setShowCreateForm] = useState( !hasDomainMatches && !hasInvites, ); const busy = createOrg.isPending || acceptInvitation.isPending || joinByDomain.isPending; return (

{title}

{description}

{hasDomainMatches && (
{domainMatches.length === 1 ? t("org.yourOrganization") : t("org.joinYourTeam")}
    {domainMatches.map((match) => (
  • {match.orgName}
    {t("org.openToDomainEmails", { domain: userDomain })}
  • ))}
)} {hasInvites && (
{t("org.pendingInvitations")}
    {pendingInvitations.map((inv) => (
  • {inv.orgName}
    {t("org.invitedBy", { name: inv.invitedBy })}
  • ))}
)} {(hasDomainMatches || hasInvites) && ( )} {showCreateForm && (
{ e.preventDefault(); const trimmed = name.trim(); if (!trimmed) return; try { await createOrg.mutateAsync(trimmed); } catch { /* surfaced below */ } }} className="space-y-3" > {(hasDomainMatches || hasInvites) && (

{t("org.createOrgVaultNotice")}

)} {createOrg.error && (
{(createOrg.error as Error).message}
)} {acceptInvitation.error && (
{(acceptInvitation.error as Error).message}
)} {joinByDomain.error && (
{(joinByDomain.error as Error).message}
)}
)}
); }