import { useEffect, useRef, useState } from "react"; import type { ConnectAccount, ConnectController } from "@opengeni/connect"; import { useConnect } from "./hooks/use-connect"; import { ConnectAccountIdentity } from "./connect-account-identity"; import { ConnectionSkeleton } from "./connection-skeleton"; import type { CapabilityCatalogItem, OpenGeniClient } from "@opengeni/sdk"; import { useOpenGeniText } from "./messages/runtime"; export type ConnectAccountsProps = { controller: ConnectController; className?: string; /** Enables explicit account-bound reconnect. Host supplies the exact destination. */ returnUrl?: string; /** Uses authenticated catalogue assets and exact connection references for service identity. */ client?: OpenGeniClient | undefined; /** Reconcile surrounding service state after a confirmed account disconnect. */ onDisconnected?: (() => void) | undefined; }; /** Local credential revocation only. Unknown or changed versions must be * refreshed; this component never retries a destructive operation implicitly. */ export function ConnectAccounts(props: ConnectAccountsProps) { const [scope, setScope] = useState(props.controller); const [generation, setGeneration] = useState(0); if (scope !== props.controller) { setScope(props.controller); setGeneration(generation + 1); } return ; } function ScopedAccounts({ controller, className, returnUrl, client, onDisconnected, }: ConnectAccountsProps) { const t = useOpenGeniText(); const view = useConnect(controller); const [accounts, setAccounts] = useState(null); const [selected, setSelected] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(false); const [reload, setReload] = useState(0); const [catalog, setCatalog] = useState<{ client: OpenGeniClient; items: CapabilityCatalogItem[]; } | null>(null); const mutation = useRef(null); useEffect(() => () => mutation.current?.abort(), []); useEffect(() => { const abort = new AbortController(); setAccounts(null); setCatalog(null); setSelected(null); setError(false); void Promise.resolve() .then(() => { abort.signal.throwIfAborted(); return Promise.all([ controller.transport.accounts(controller.workspaceId, { signal: abort.signal }), client?.listCapabilities(controller.workspaceId).catch(() => null) ?? null, ]); }) .then(([result, metadata]) => { if (!abort.signal.aborted) { setAccounts(structuredClone(result.filter((account) => account.status !== "disabled"))); if (client && metadata) setCatalog({ client, items: metadata.items }); } }) .catch(() => { if (!abort.signal.aborted) setError(true); }); return () => abort.abort(); }, [client, controller, reload]); const disconnect = async (account: ConnectAccount) => { const expectedVersion = account.version; if ( mutation.current || view.busy || expectedVersion === undefined || !Number.isSafeInteger(expectedVersion) || expectedVersion < 1 ) return; const abort = new AbortController(); mutation.current = abort; setBusy(true); setError(false); try { await controller.transport.disconnect(controller.workspaceId, account.id, { expectedVersion, signal: abort.signal, }); if (!abort.signal.aborted) { setReload((value) => value + 1); onDisconnected?.(); } } catch { if (!abort.signal.aborted) { // Outcome may be unknown. Drop the inventory so a second click cannot // replay against the stale selection; let the host reload live state. setAccounts(null); setError(true); } } finally { if (!abort.signal.aborted) { mutation.current = null; setBusy(false); setSelected(null); } } }; const reconnect = async (account: ConnectAccount) => { if (!returnUrl || mutation.current || view.busy) return; setError(false); try { await controller.begin({ providerId: account.providerId, ownership: account.ownership, reconnectAccountId: account.id, returnUrl, idempotencyKey: crypto.randomUUID(), }); } catch { setError(true); } }; return ( {t("connectAccounts.yourConnections")} {error && {t("connectAccounts.accountStateCouldNotBe")}} {!accounts && !error && } {accounts?.length === 0 && {t("connectAccounts.noConnectedAccounts")}} {accounts && accounts.length > 0 && ( {accounts.map((account) => ( {account.status === "connected" ? t("connectAccounts.connected") : account.status === "auth_needed" ? t("connectAccounts.reconnectNeeded") : t("connectAccounts.disconnected")} {returnUrl && account.status === "auth_needed" && ( void reconnect(account)} > {t("connectAccounts.reconnect")} )} {selected === `${account.providerId}:${account.id}` ? ( {t("connectAccounts.thisRemovesLocalOpengeniAccess")} void disconnect(account)} > {t("connectAccounts.confirmDisconnect")} setSelected(null)}> {t("connectAccounts.keepAccount")} ) : ( setSelected(`${account.providerId}:${account.id}`)} > {t("connectAccounts.disconnect")} )} ))} )} setReload((value) => value + 1)} > {t("connectAccounts.reloadAccounts")} ); }
{t("connectAccounts.accountStateCouldNotBe")}
{t("connectAccounts.noConnectedAccounts")}
{t("connectAccounts.thisRemovesLocalOpengeniAccess")}