import { useMemo, useState } from "react"; import { AuthTemplateSlug, IntegrationSlug, type AuthMethodDescriptor, type Connection, type Owner, } from "@executor-js/sdk/shared"; import { CheckCircle2Icon, RotateCcwIcon, XCircleIcon } from "lucide-react"; import { toast } from "sonner"; import { useOrganizationId } from "../api/organization-context"; import { normalizeEmail, type ProviderAccount } from "../lib/provider-accounts"; import { cn } from "../lib/utils"; import { useOAuthPopupFlow, type OAuthStartPayload } from "../plugins/oauth-sign-in"; import { Button } from "./button"; import { Badge } from "./badge"; import { Checkbox } from "./checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./dialog"; import { Label } from "./label"; import { connectionLabelForHost, connectionNameFrom } from "./add-account-modal"; export type EnableServiceIntegration = { readonly slug: IntegrationSlug; readonly name: string; readonly kind: string; readonly authMethods: readonly AuthMethodDescriptor[]; }; export type EnableServiceStepStatus = "pending" | "done" | "failed"; export type EnableServiceQueueStep = { readonly integration: TIntegration; readonly status: EnableServiceStepStatus; }; export type EnableServiceQueue = { readonly steps: readonly EnableServiceQueueStep[]; readonly activeIndex: number; }; export const createEnableServicesQueue = ( integrations: readonly TIntegration[], ): EnableServiceQueue => ({ steps: integrations.map((integration) => ({ integration, status: "pending" })), activeIndex: 0, }); const nextPendingIndex = ( steps: readonly EnableServiceQueueStep[], from: number, ): number => { const next = steps.findIndex((step, index) => index > from && step.status !== "done"); return next === -1 ? steps.length : next; }; export const applyEnableServiceStepResult = ( queue: EnableServiceQueue, slug: IntegrationSlug, status: EnableServiceStepStatus, ): EnableServiceQueue => { const stepIndex = queue.steps.findIndex((step) => step.integration.slug === slug); if (stepIndex === -1) return queue; const steps = queue.steps.map((step, index) => index === stepIndex ? { ...step, status } : step, ); return { steps, activeIndex: status === "done" && stepIndex === queue.activeIndex ? nextPendingIndex(steps, stepIndex) : queue.activeIndex, }; }; export const retryEnableServiceStep = ( queue: EnableServiceQueue, slug: IntegrationSlug, ): EnableServiceQueue => { const stepIndex = queue.steps.findIndex((step) => step.integration.slug === slug); if (stepIndex === -1) return queue; return { steps: queue.steps.map((step, index) => index === stepIndex ? { ...step, status: "pending" } : step, ), activeIndex: stepIndex, }; }; export const activeEnableServiceStep = ( queue: EnableServiceQueue, ): EnableServiceQueueStep | null => queue.steps[queue.activeIndex] ?? null; const oauthMethodFor = (integration: EnableServiceIntegration): AuthMethodDescriptor | null => integration.authMethods.find((method) => method.kind === "oauth") ?? null; const mirrorConnectionFor = ( account: ProviderAccount, ): Connection | null => account.connections.find((entry) => entry.connection.oauthClient != null)?.connection ?? null; export const buildEnableServiceOAuthStartPayload = (input: { readonly account: ProviderAccount; readonly integration: EnableServiceIntegration; readonly organizationId: string | null; }): OAuthStartPayload | null => { const loginHint = normalizeEmail(input.account.label); if (loginHint === null) return null; const method = oauthMethodFor(input.integration); if (!method) return null; const mirrored = mirrorConnectionFor(input.account); if (!mirrored?.oauthClient) return null; const owner = input.account.owner as Owner; // Seed the label with the account email's localpart ("rhys Google Calendar") // so two same-owner accounts enabling the same service don't collide on // (owner, integration, name) and clobber each other's connection. const accountLabel = `${loginHint.split("@")[0] ?? ""} ${input.integration.name}`; const identityLabel = connectionLabelForHost( accountLabel, owner, input.integration.name, input.organizationId, ); return { client: mirrored.oauthClient, clientOwner: mirrored.oauthClientOwner ?? mirrored.owner, owner, name: connectionNameFrom(accountLabel, owner, input.integration.name, input.organizationId), integration: input.integration.slug, template: AuthTemplateSlug.make(method.template), identityLabel, loginHint, }; }; /** Run one queue step: build the payload and open exactly one OAuth popup. * The completion callbacks only record the step result; advancing to the next * service always requires a fresh Continue click, so a finishing popup can * never auto-open the next one. */ export const continueEnableServiceStep = (input: { readonly account: ProviderAccount; readonly integration: EnableServiceIntegration; readonly organizationId: string | null; readonly start: (input: { readonly payload: OAuthStartPayload; readonly onSuccess: () => void; readonly onError: () => void; }) => void; readonly markActive: (status: EnableServiceStepStatus) => void; }): void => { const payload = buildEnableServiceOAuthStartPayload({ account: input.account, integration: input.integration, organizationId: input.organizationId, }); if (!payload) { input.markActive("failed"); toast.error("This service cannot reuse the selected OAuth app"); return; } input.start({ payload, onSuccess: () => { input.markActive("done"); toast.success(`${input.integration.name} connected`); }, onError: () => { input.markActive("failed"); }, }); }; const serviceKey = (integration: EnableServiceIntegration): string => String(integration.slug); export function EnableServicesModal(props: { readonly open: boolean; readonly account: ProviderAccount | null; readonly integrations: readonly EnableServiceIntegration[]; readonly onOpenChange: (open: boolean) => void; }) { if (!props.open || !props.account) return null; return ; } function EnableServicesModalBody(props: { readonly account: ProviderAccount; readonly integrations: readonly EnableServiceIntegration[]; readonly onOpenChange: (open: boolean) => void; readonly open: boolean; }) { const organizationId = useOrganizationId(); const defaultSelected = useMemo( () => new Set( props.integrations .filter((integration) => oauthMethodFor(integration) !== null) .map((integration) => serviceKey(integration)), ), [props.integrations], ); const [selected, setSelected] = useState>(defaultSelected); const [queue, setQueue] = useState | null>(null); const oauthPopup = useOAuthPopupFlow({ popupName: "enable-provider-service", detectPopupClosed: false, startErrorMessage: "Failed to start service connection", }); const active = queue ? activeEnableServiceStep(queue) : null; const selectedIntegrations = props.integrations.filter((integration) => selected.has(serviceKey(integration)), ); const complete = queue ? queue.steps.every((step) => step.status === "done") : false; const loginHint = normalizeEmail(props.account.label); const toggleSelected = (integration: EnableServiceIntegration, checked: boolean) => { setSelected((current) => { const next = new Set(current); if (checked) next.add(serviceKey(integration)); else next.delete(serviceKey(integration)); return next; }); }; const startQueue = () => { setQueue(createEnableServicesQueue(selectedIntegrations)); }; const markActive = (status: EnableServiceStepStatus) => { if (!queue || !active) return; setQueue(applyEnableServiceStepResult(queue, active.integration.slug, status)); }; const handleContinue = () => { if (!active) return; continueEnableServiceStep({ account: props.account, integration: active.integration, organizationId, start: (input) => void oauthPopup.start(input), markActive, }); }; const handleRetry = () => { if (!queue || !active) return; setQueue(retryEnableServiceStep(queue, active.integration.slug)); }; return ( Add services {loginHint ?? props.account.label} {queue === null ? (
{props.integrations.map((integration) => { const key = serviceKey(integration); const disabled = oauthMethodFor(integration) === null || loginHint === null; return ( ); })}
) : (
{queue.steps.map((step, index) => (

{step.integration.name}

{index + 1} of {queue.steps.length}

{step.status === "done" ? ( Done ) : step.status === "failed" ? ( Failed ) : ( Pending )}
))}
{complete ? (
Selected services are connected.
) : active ? (

Connecting {active.integration.name} ({queue.activeIndex + 1} of{" "} {queue.steps.length})

{active.status === "failed" ? (

Retry this service when ready.

) : null}
) : null}
)} {queue === null ? ( ) : complete ? ( ) : active?.status === "failed" ? ( ) : ( )}
); }