import { useEffect, useState, useRef } from "react"; import { ConnectController, authorizeConnectAttempt, createBrowserConnectNavigation, type ConnectTransport, type ConnectInstallationTarget, type ConnectAttempt, } from "@opengeni/connect"; import { ConnectSetup, ConnectionLogo, isOwnerApprovalPending } from "../../../connect"; import "../../../../styles/connect.css"; import { Dialog, DialogHeader, DialogTitle, DialogDescription } from "../../../ui/dialog"; import { CapabilityDialogContent } from "./detail-dialog"; import { Button } from "../../../ui/button"; import { selectGitHubConnectAccount } from "../../lib/github-connect-account"; import { recordGitHubInstallRequest } from "../../lib/github-install-request"; import { beginIntegrationConnect, connectAttemptOutcome, integrationClassFromConnectProvider, type IntegrationConnectTracker, } from "../../lib/integration-connect-analytics"; export type NativeConnectRequest = { scope: { workspaceId: string; transport: ConnectTransport }; providerId: string; ownership: "personal" | "workspace"; returnUrl: string; idempotencyKey: string; reconnectAccountId?: string | undefined; installationTarget?: ConnectInstallationTarget | undefined; displayName?: string | undefined; description?: string | undefined; logoUrl?: string | undefined; authorizeLabel?: string | undefined; }; export function nativeConnectApiInput(request: NativeConnectRequest) { return { providerId: request.providerId, ownership: request.ownership, returnUrl: request.returnUrl, idempotencyKey: request.idempotencyKey, ...(request.reconnectAccountId ? { reconnectAccountId: request.reconnectAccountId } : {}), ...(request.installationTarget ? { installationTarget: request.installationTarget } : {}), }; } /** Native presentation only. Durable setup and provider callbacks are shared * with host products; legacy query-param callbacks remain read-compatible. */ export function NativeConnectSetup({ transport, workspaceId, request, onClose, onComplete, }: { transport: ConnectTransport; workspaceId: string; request: NativeConnectRequest; onClose(): void; onComplete(attempt: ConnectAttempt): void; }) { const [controller, setController] = useState(null); const [failed, setFailed] = useState(false); const [preparing, setPreparing] = useState(true); const [retry, setRetry] = useState(0); const [pending, setPending] = useState([]); const [resumed, setResumed] = useState(false); const heading = useRef(null); const opener = useRef( typeof document !== "undefined" && document.activeElement instanceof HTMLElement ? document.activeElement : null, ); const navigation = useRef(null); const completedAttempt = useRef(null); // Consent-gated connect journey: started when the dialog begins setup, // finished by the attempt's settled state, `abandoned` when closed first. const journey = useRef(null); useEffect(() => { const tracker = beginIntegrationConnect( integrationClassFromConnectProvider(request.providerId), "oauth", ); journey.current = tracker; return () => { tracker.finish("abandoned"); if (journey.current === tracker) journey.current = null; }; }, [request.providerId]); useEffect(() => { if (request.scope.workspaceId !== workspaceId || request.scope.transport !== transport) { setController(null); return; } const next = new ConnectController(transport, workspaceId); setController(next); setFailed(false); setPreparing(true); setPending([]); setResumed(false); const abort = new AbortController(); navigation.current?.abort(); navigation.current = new AbortController(); navigation.current = abort; let live = true; void transport .pending(workspaceId, { signal: abort.signal }) .then(async (attempts) => { if (!live) return; const matches = attempts.filter( (attempt) => attempt.providerId === request.providerId && attempt.ownership === request.ownership && attempt.installationTarget?.instanceKey === request.installationTarget?.instanceKey && (!request.reconnectAccountId || attempt.account?.id === request.reconnectAccountId), ); if (matches.length === 1) { setResumed(true); await next.recover(matches[0]!.id); } else if (matches.length > 1) setPending(matches); else await next.begin(nativeConnectApiInput(request)); }) .catch(() => { if (live) setFailed(true); }) .finally(() => { if (live) setPreparing(false); }); return () => { live = false; abort.abort(); navigation.current?.abort(); next.dispose(); }; }, [transport, workspaceId, request, retry]); useEffect(() => { if (!controller) return; const notify = () => { const attempt = controller.getSnapshot().attempt; const outcome = attempt ? connectAttemptOutcome(attempt) : null; if (outcome) journey.current?.finish(outcome); // A non-owner's request: remember it so the GitHub card can say it's // waiting for an owner rather than offering the same dead end again. if (attempt?.providerId === "github-app" && isOwnerApprovalPending(attempt)) recordGitHubInstallRequest(attempt.workspaceId); if (attempt?.state === "complete" && completedAttempt.current !== attempt.id) { completedAttempt.current = attempt.id; onComplete(attempt); } }; const unsubscribe = controller.subscribe(notify); notify(); return unsubscribe; }, [controller, onComplete]); if (request.scope.workspaceId !== workspaceId || request.scope.transport !== transport) return null; return ( { if (!open) onClose(); }} > { event.preventDefault(); heading.current?.focus(); }} onCloseAutoFocus={(event) => { if (opener.current?.isConnected) { event.preventDefault(); opener.current.focus(); } }} >
Connect{" "} {request.displayName ?? request.installationTarget?.displayName ?? request.providerId} {request.description ?? "Authorize access to use this connection in Opengeni."}
{failed && (
Could not start setup.{" "}
)} {preparing && (

Preparing connection…

)} {pending.length > 0 && controller && (

You have unfinished setup for this provider. Resume an attempt or connect a different account.

    {pending.map((attempt) => (
  • ))}
)} {resumed && controller && !preparing && !failed && ( )} {controller && !preparing && !failed && pending.length === 0 && ( { const signal = navigation.current?.signal; if (!signal) throw new Error("Connection setup is no longer active"); return selectGitHubConnectAccount(controller, accountId, window, signal); }, } : {})} onAuthorize={async (attempt) => { const result = await authorizeConnectAttempt( transport, attempt, createBrowserConnectNavigation(window), { mode: "popup", ...(navigation.current ? { signal: navigation.current.signal } : {}), }, ); if (result) await controller.recover(result.id); }} /> )}
); }