import { agentNativePath, appApiPath, } from "@agent-native/core/client/api-path"; import { useT } from "@agent-native/core/client/i18n"; import { IconExternalLink, IconCheck, IconCircle, IconLoader2, IconUpload, IconPlugOff, IconRefresh, } from "@tabler/icons-react"; import { useState, useEffect, useCallback, useRef } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { openNotionOAuthUrl, useDisconnectNotion, useNotionConnection, } from "@/hooks/use-notion"; import { cn } from "@/lib/utils"; // ─── Notion SVG icon ──────────────────────────────────────────────────────── function NotionIcon({ className }: { className?: string }) { return ( ); } // ─── OAuth wizard steps ───────────────────────────────────────────────────── const OAUTH_STEPS = [ { titleKey: "sidebar.notionCreateIntegration", descriptionKey: "sidebar.notionCreateIntegrationDescription", url: "https://www.notion.so/profile/integrations", linkTextKey: "sidebar.notionOpenIntegrations", }, { titleKey: "sidebar.notionConfigurePublicIntegration", descriptionKey: "sidebar.notionConfigurePublicIntegrationDescription", showRedirectUri: true, }, { titleKey: "sidebar.notionCopyOAuthCredentials", descriptionKey: "sidebar.notionCopyOAuthCredentialsDescription", showUpload: true, }, { titleKey: "sidebar.notionConnectWorkspace", descriptionKey: "sidebar.notionConnectWorkspaceDescription", showConnect: true, }, ]; interface EnvKeyStatus { key: string; label: string; required: boolean; configured: boolean; } // ─── Component ────────────────────────────────────────────────────────────── export function NotionButton() { const t = useT(); const { data: connection, refetch } = useNotionConnection(); const disconnectNotion = useDisconnectNotion(); const [open, setOpen] = useState(false); const [showWizard, setShowWizard] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [saving, setSaving] = useState(false); const [envStatus, setEnvStatus] = useState([]); const isConnected = connection?.connected ?? false; const needsCredentials = connection?.error === "missing_credentials"; const fetchEnvStatus = useCallback(async () => { try { const res = await fetch(agentNativePath("/_agent-native/env-status")); if (res.ok) setEnvStatus(await res.json()); } catch {} }, []); useEffect(() => { if (showWizard) fetchEnvStatus(); }, [showWizard, fetchEnvStatus]); const oauthConfigured = envStatus.filter((k) => k.key.startsWith("NOTION_CLIENT")).length > 0 && envStatus .filter((k) => k.key.startsWith("NOTION_CLIENT")) .every((k) => k.configured); const redirectUri = typeof window !== "undefined" ? `${window.location.origin}${appApiPath("/api/notion/callback")}` : ""; const pollRef = useRef>(undefined); const pollTimeoutRef = useRef>(undefined); // Cleanup polling on unmount useEffect(() => { return () => { if (pollRef.current) clearInterval(pollRef.current); if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current); }; }, []); async function handleConnect() { if (connection?.error === "missing_credentials") { if (needsCredentials) { setShowWizard(true); return; } toast.error(t("sidebar.notionOAuthNotConfigured")); return; } const popup = window.open("about:blank", "_blank"); if (!popup) { toast.error(t("sidebar.notionOAuthNotConfigured")); return; } popup.opener = null; try { popup.location.href = await openNotionOAuthUrl(); } catch (error) { popup.close(); toast.error( error instanceof Error ? error.message : t("sidebar.notionOAuthNotConfigured"), ); return; } // Clear any existing poll before starting a new one if (pollRef.current) clearInterval(pollRef.current); if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current); // Poll for connection pollRef.current = setInterval(async () => { const result = await refetch(); if (result.data?.connected) { clearInterval(pollRef.current); pollRef.current = undefined; setShowWizard(false); setOpen(false); } }, 2000); // Stop polling after 5 minutes pollTimeoutRef.current = setTimeout(() => { if (pollRef.current) clearInterval(pollRef.current); pollRef.current = undefined; }, 300_000); } async function handleDisconnect() { try { await disconnectNotion.mutateAsync({}); toast.success(t("sidebar.notionDisconnectedWorkspace")); } catch (err) { toast.error( err instanceof Error ? err.message : t("sidebar.notionDisconnectFailed"), ); } } async function handleJsonUpload(file: File) { setSaving(true); try { const text = await file.text(); const json = JSON.parse(text); const clientId = json.client_id || json.oauth_client_id; const clientSecret = json.client_secret || json.oauth_client_secret; if (!clientId || !clientSecret) { throw new Error(t("sidebar.notionCredentialsNotFound")); } const res = await fetch(agentNativePath("/_agent-native/env-vars"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scope: "workspace", vars: [ { key: "NOTION_CLIENT_ID", value: clientId }, { key: "NOTION_CLIENT_SECRET", value: clientSecret }, ], }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || t("sidebar.notionCredentialsSaveFailed")); } await fetchEnvStatus(); toast.success(t("sidebar.notionCredentialsSavedReloading")); setTimeout(() => window.location.reload(), 1500); } catch (err) { toast.error( err instanceof Error ? err.message : t("sidebar.notionParseJsonFailed"), ); } finally { setSaving(false); } } // ─── Wizard UI ────────────────────────────────────────────────────────── if (showWizard) { return ( {t("sidebar.notionConnect")} e.preventDefault()} >

{t("sidebar.notionConnect")}

{t("sidebar.notionConfigureOAuthAuthorize")}

{/* ─── OAuth setup ────────────────────────────────── */}
{OAUTH_STEPS.map((step, i) => { const completed = i < currentStep || (i === 2 && oauthConfigured); const active = i === currentStep; return (
{active && (

{t(step.descriptionKey)}

{step.url && ( {step.linkTextKey ? t(step.linkTextKey) : null} )} {step.showRedirectUri && (
{redirectUri}
)} {step.showUpload && (
{envStatus .filter((k) => k.key.startsWith("NOTION_CLIENT")) .map((k) => (
{k.configured ? ( ) : ( )} {k.label}
))}
)} {step.showConnect && ( )} {i < OAUTH_STEPS.length - 1 && ( )}
)}
); })}
); } // ─── Connected state ──────────────────────────────────────────────────── return ( {isConnected ? t("sidebar.notionConnectedTooltip", { name: connection?.workspaceName ?? t("sidebar.connected"), }) : t("sidebar.notionConnect")} e.preventDefault()} > {isConnected ? ( <>

{connection?.workspaceName ?? "Notion"}

{t("sidebar.connectedViaOAuth")}

) : (

{t("sidebar.notionConnect")}

{t("sidebar.notionSyncDocumentsDescription")}

)}
); }