import { useCallback, useMemo } from "react"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { AuthTemplateSlug, IntegrationSlug } from "@executor-js/sdk/shared"; import type { IntegrationAccountHandoff } from "@executor-js/sdk/client"; import { TriangleAlert } from "lucide-react"; import { AccountsSection } from "@executor-js/react/components/accounts-section"; import { HealthCheckEditor, type HealthCheckLivePreview, } from "@executor-js/react/components/health-check-editor"; import { useOrganizationId } from "@executor-js/react/api/organization-context"; import { defaultConnectionOwnerForHost } from "@executor-js/react/plugins/connection-owner"; import { Alert, AlertDescription, AlertTitle } from "@executor-js/react/components/alert"; import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; import type { AuthMethod, Placement } from "@executor-js/react/lib/auth-placements"; import { useCustomMethodActions, type AuthMethodsCodec, type ConfigureAuthMethods, } from "@executor-js/react/lib/custom-auth-methods"; import { authMethodsFromConfig, openApiWireAuthInput, templateFromPlacements, } from "@executor-js/plugin-openapi/react"; import type { Authentication } from "@executor-js/plugin-openapi"; import { googleConfigAtom, googleConfigure } from "./atoms"; import { googleAudienceWarningsForUrls } from "../sdk/presets"; const GOOGLE_AUDIENCE_WARNING: Readonly> = { "workspace-admin": "This connection includes Google Workspace admin APIs. Connecting requires a Workspace admin account; personal Gmail accounts cannot grant these scopes.", "unsupported-user": "This connection includes APIs that Google does not grant through standard user OAuth consent. Those tools may fail to authorize.", }; const NO_AUTH_METHOD: AuthMethod = { id: "none", label: "No authentication", kind: "none", source: "spec", template: AuthTemplateSlug.make("none"), placements: [], }; export default function GoogleAccountsPanel(props: { readonly sourceId: string; readonly integrationName: string; readonly accountHandoff?: IntegrationAccountHandoff | null; }) { const { sourceId, integrationName, accountHandoff } = props; const slug = IntegrationSlug.make(sourceId); const configResult = useAtomValue(googleConfigAtom(slug)); const doConfigure = useAtomSet(googleConfigure, { mode: "promiseExit" }); const existingTemplate = useMemo(() => { if (!AsyncResult.isSuccess(configResult) || configResult.value == null) return []; return (configResult.value.authenticationTemplate ?? []) as readonly Authentication[]; }, [configResult]); const methods = useMemo(() => { const declared = authMethodsFromConfig(existingTemplate); return declared.length > 0 ? declared : [NO_AUTH_METHOD]; }, [existingTemplate]); const configure = useCallback>( async (input) => { const exit = await doConfigure({ params: { slug }, payload: { authenticationTemplate: input.authenticationTemplate.map(openApiWireAuthInput), ...(input.mode ? { mode: input.mode } : {}), }, reactivityKeys: integrationWriteKeys, }); return Exit.map(exit, (result) => result.authenticationTemplate as readonly Authentication[]); }, [doConfigure, slug], ); const codec = useMemo>( () => ({ toAuthMethods: authMethodsFromConfig, templatesFromPlacements: (placements: readonly Placement[]) => [ templateFromPlacements(placements), ], slugOf: (template: Authentication) => String(template.slug), }), [], ); const { createCustomMethod, removeCustomMethod } = useCustomMethodActions({ existing: existingTemplate, codec, configure, }); const audienceWarnings = useMemo(() => { if (!AsyncResult.isSuccess(configResult) || configResult.value == null) return []; const urls = configResult.value.googleDiscoveryUrls ?? []; return googleAudienceWarningsForUrls(urls).flatMap((audience: string) => { const message = GOOGLE_AUDIENCE_WARNING[audience]; return message ? [message] : []; }); }, [configResult]); // Live-preview context for the health-check edit sheet (apiKey methods only; // OAuth methods can't be previewed with a pasted key). const organizationId = useOrganizationId(); const livePreview = useMemo(() => { const templates = methods .filter((m) => m.kind === "apikey") .map((m) => ({ template: m.template, label: m.label })); if (templates.length === 0) return undefined; return { owner: defaultConnectionOwnerForHost(organizationId), templates }; }, [methods, organizationId]); return (
{audienceWarnings.length > 0 && ( Some Google APIs need special consent {audienceWarnings.map((message: string) => (

{message}

))}
)} {/* Google auto-configures the People identity probe at add time; this is where an admin sees, adjusts, or clears it. */}
); }