import { IconLoader2 } from "@tabler/icons-react"; import { useState } from "react"; import { useOrg, useAcceptInvitation, useJoinByDomain } from "./hooks.js"; export interface InvitationBannerProps { className?: string; } /** * Top-of-app banner that surfaces: * - Pending org invitations (one-click Accept). * - Domain-match orgs the user can auto-join because their email domain * matches `organizations.allowed_domain` (one-click Join). Lets a new * signup at e.g. `someone@builder.io` see and join the existing * Builder.io org without going through the picker. * * Renders nothing when there's nothing to surface. */ export function InvitationBanner({ className }: InvitationBannerProps) { const { data: org } = useOrg(); const acceptInvitation = useAcceptInvitation(); const joinByDomain = useJoinByDomain(); const [joiningOrgId, setJoiningOrgId] = useState(null); const [acceptingInvitationId, setAcceptingInvitationId] = useState< string | null >(null); const pendingInvitations = org?.pendingInvitations ?? []; const domainMatches = org?.domainMatches ?? []; if (pendingInvitations.length === 0 && domainMatches.length === 0) { return null; } const error = acceptInvitation.error || joinByDomain.error; // While the join/accept request is in flight (and continuing until the // refreshed org data unmounts this banner), surface a prominent in-place // "Joining {orgName}…" state so the chat panel reflects the action instead // of looking unchanged until the view abruptly swaps. const joiningOrgName = (() => { if (joiningOrgId) { return ( domainMatches.find((m) => m.orgId === joiningOrgId)?.orgName ?? null ); } if (acceptingInvitationId) { return ( pendingInvitations.find((i) => i.id === acceptingInvitationId) ?.orgName ?? null ); } return null; })(); if (joiningOrgName) { return (
Joining {joiningOrgName}
); } return (
{pendingInvitations.map((inv, index) => (
0 ? "pt-2" : ""}`} > {inv.invitedBy} invited you to join {inv.orgName}
))}
{domainMatches.map((match) => { const isJoining = joinByDomain.isPending && joiningOrgId === match.orgId; return (
0 ? "mt-2 border-t border-blue-200/70 pt-2 dark:border-blue-800/50" : ""} flex items-center justify-between gap-4 text-sm`} > Your team is already using this app. Join{" "} {match.orgName} to collaborate.
); })} {error && (
{(error as Error).message}
)}
); }