);
})}
{selectedAccount ? (
{
if (!open) setSelectedAccount(null);
}}
/>
) : null}
);
}
export function AccountsSection(props: {
readonly integration: IntegrationSlug;
readonly integrationName: string;
readonly methods: readonly AuthMethod[];
readonly accountHandoff?: IntegrationAccountHandoff | null;
/** When provided, Add connection shows a "+ Custom method" row. The plugin binds
* this to its own configure mutation. Omitted for plugins with fixed auth. */
readonly createCustomMethod?: CreateCustomMethod;
readonly removeCustomMethod?: (method: AuthMethod) => Promise;
}) {
const {
integration,
integrationName,
methods,
accountHandoff,
createCustomMethod,
removeCustomMethod,
} = props;
const [adding, setAdding] = useState(false);
const [editingConnection, setEditingConnection] = useState(null);
const [reconnectHandoff, setReconnectHandoff] = useState(null);
const ownerDisplay = useOwnerDisplay();
const canAddConnection = methods.length > 0 || createCustomMethod !== undefined;
useEffect(() => {
if (accountHandoff) {
setAdding(true);
}
}, [accountHandoff]);
// The integration's declared oauth scopes — what connections need granted. A
// connection granted fewer is flagged to reconnect (e.g. after a service was
// added widened the consent).
//
// Spec-derived oauth scopes are the full per-operation catalog union (e.g. an
// OpenAPI source like PostHog declares hundreds of scopes). Those are requested
// broadly but not individually required: a provider that narrows the grant to
// the user's actual access is healthy, not in need of reconnect. So only treat
// CUSTOM (user-configured) scopes as required here; never the spec catalog.
const oauthMethod = methods.find((m: AuthMethod) => m.kind === "oauth");
const declaredScopes = reconsentRequiredScopes(oauthMethod);
// Read both owners to decide between the grouped view and the empty CTA. The
// grouped sub-components re-read these (effect-atom dedupes) and self-hide.
const orgConnections = useAtomValue(connectionsForIntegrationAtom({ integration, owner: "org" }));
const userConnections = useAtomValue(
connectionsForIntegrationAtom({ integration, owner: "user" }),
);
// Mount the optimistic-add atoms so the section participates in the same
// optimistic surface the modal writes through (keeps the registry warm).
useAtomSet(addConnectionOptimistic("org"));
useAtomSet(addConnectionOptimistic("user"));
const totalCount = useMemo(() => {
const orgRows = AsyncResult.isSuccess(orgConnections) ? orgConnections.value.length : 0;
const userRows = AsyncResult.isSuccess(userConnections) ? userConnections.value.length : 0;
return orgRows + userRows;
}, [orgConnections, userConnections]);
const loading = !AsyncResult.isSuccess(orgConnections) && !AsyncResult.isSuccess(userConnections);
// When there are zero connections the dashed empty-state card below carries
// its own "Add connection" CTA, so the header button would be a redundant
// second copy of the same action. Show the header button only outside that
// state (populated, or still loading).
const showEmptyState = !loading && totalCount === 0;
const openAddConnection = () => {
trackEvent("connection_add_opened", {
integration_slug: String(integration),
has_oauth_method: methods.some((m: AuthMethod) => m.kind === "oauth"),
has_api_key_method: methods.some((m: AuthMethod) => m.kind !== "oauth" && m.kind !== "none"),
});
setAdding(true);
};
const modalState = reconnectHandoff ?? accountHandoff;
const modal = (
{
setAdding(open);
if (!open) setReconnectHandoff(null);
}}
initialState={modalState}
createCustomMethod={createCustomMethod}
removeCustomMethod={removeCustomMethod}
/>
);
return (
Connections
{!showEmptyState ? (
) : null}
{loading ? (
Loading accounts…
) : showEmptyState ? (
No connections yet
Add a connection to make this integration's tools available.