[eric] onboarding: fix Connect doing nothing (extract shared useConnectIntegration: create->oauth->named popup->poll status; reused downward)

This commit is contained in:
ciregenz
2026-07-01 00:02:39 -07:00
parent 6d83b2b033
commit 481005fddf
2 changed files with 67 additions and 24 deletions
@@ -1,10 +1,11 @@
// D4: optional connect. Reuses the REAL shared connector catalog (icons + metadata), the REAL
// connection state from toolsSlice, and the REAL create+OAuth thunks. All connectable MCPs, scrollable.
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { INTEGRATIONS, Integration } from '@/shared/integrations/catalog';
import { fetchTools, createTool, startOAuth, startDeviceCodeLogin, ToolDefinition } from '@/shared/state/toolsSlice';
import { useConnectIntegration } from '@/shared/integrations/useConnectIntegration';
import { fetchTools, ToolDefinition } from '@/shared/state/toolsSlice';
import { useOnboardingSkin } from '../onboardingSkin';
import { Heading, Sub, PrimaryButton, GhostLink } from '../OnboardingAtoms';
@@ -14,30 +15,17 @@ function toolFor(integration: Integration, items: Record<string, ToolDefinition>
const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
const S = useOnboardingSkin();
const dispatch = useAppDispatch();
const connectIntegration = useConnectIntegration();
const items = useAppSelector((s) => s.tools.items);
const tool = toolFor(integration, items);
const connected = tool?.auth_status === 'connected';
const [connecting, setConnecting] = useState(false);
const connect = async () => {
try {
// The tool has to exist before OAuth (fresh installs have none) -> create from the catalog first.
let t = tool;
if (!t) {
t = await dispatch(createTool({
name: integration.name,
description: integration.description,
mcp_config: integration.mcp_config,
auth_type: integration.authType ?? 'oauth2',
})).unwrap();
}
if (integration.authType === 'device_code') {
await dispatch(startDeviceCodeLogin(t.id));
} else {
const res = await dispatch(startOAuth(t.id)).unwrap();
if (res.auth_url) window.open(res.auth_url, '_blank');
}
} catch { /* connect failed; leave as-is */ }
if (connecting) return;
setConnecting(true);
await connectIntegration(integration, tool);
setConnecting(false);
};
return (
@@ -50,7 +38,7 @@ const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
</div>
</div>
<span
onClick={connected ? undefined : connect}
onClick={connected || connecting ? undefined : connect}
style={{
marginLeft: 'auto',
flexShrink: 0,
@@ -60,11 +48,12 @@ const Row: React.FC<{ integration: Integration }> = ({ integration }) => {
border: `1px solid ${connected ? S.border : S.borderStrong}`,
borderRadius: 999,
padding: '6px 15px',
cursor: connected ? 'default' : 'pointer',
cursor: connected || connecting ? 'default' : 'pointer',
whiteSpace: 'nowrap',
opacity: connecting ? 0.6 : 1,
}}
>
{connected ? 'Connected' : 'Connect'}
{connected ? 'Connected' : connecting ? 'Connecting…' : 'Connect'}
</span>
</div>
);
@@ -0,0 +1,54 @@
// Shared connect primitive: create the tool (if needed) -> start OAuth / device-code -> open the
// popup -> poll status until connected. Lives in shared/ so both the Tools page and Onboarding use
// the SAME proven flow (downward abstraction), instead of each re-implementing it.
import { useCallback } from 'react';
import { useAppDispatch } from '@/shared/hooks';
import { createTool, startOAuth, startDeviceCodeLogin, fetchToolStatus, ToolDefinition } from '@/shared/state/toolsSlice';
import { Integration } from './catalog';
const POLL_MS = 2000;
const POLL_MAX = 60; // ~2 min
export interface ConnectResult {
status: 'connected' | 'cancelled' | 'error';
}
export function useConnectIntegration(): (integration: Integration, existing?: ToolDefinition) => Promise<ConnectResult> {
const dispatch = useAppDispatch();
return useCallback(async (integration: Integration, existing?: ToolDefinition): Promise<ConnectResult> => {
try {
// The tool must exist before OAuth (fresh installs have none) -> create from the catalog.
let tool = existing;
if (!tool) {
tool = await dispatch(createTool({
name: integration.name,
description: integration.description,
mcp_config: integration.mcp_config,
auth_type: integration.authType ?? 'oauth2',
})).unwrap();
}
if (integration.authType === 'device_code') {
await dispatch(startDeviceCodeLogin(tool.id));
return { status: 'connected' };
}
const { auth_url } = await dispatch(startOAuth(tool.id)).unwrap();
if (!auth_url) return { status: 'error' };
// Named popup + features = the pattern that actually opens in Electron (a bare _blank is swallowed).
window.open(auth_url, 'oauth', 'width=500,height=700,left=200,top=100');
// Poll until the OAuth round-trip flips auth_status to connected.
for (let i = 0; i < POLL_MAX; i++) {
await new Promise((r) => setTimeout(r, POLL_MS));
const t = await dispatch(fetchToolStatus(tool.id)).unwrap();
if (t.auth_status === 'connected') return { status: 'connected' };
}
return { status: 'cancelled' };
} catch {
return { status: 'error' };
}
}, [dispatch]);
}