import React from 'react'; import { Button } from '@/components/ui/button'; import { toast } from '@/components/ui'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { useLinearAuthStore } from '@/stores/useLinearAuthStore'; import { cn } from '@/lib/utils'; import { openExternalUrl } from '@/lib/url'; import { useI18n } from '@/lib/i18n'; import { focusDesktopWindow, isDesktopShell } from '@/lib/desktop'; import { Icon } from '@/components/icon/Icon'; import { LinearProjectMapping } from './LinearProjectMapping'; import { LinearSessionComments } from './LinearSessionComments'; const AUTHORIZATION_WATCH_MS = 3 * 60_000; const AUTHORIZATION_POLL_MS = 1_500; type WorkspaceSnapshot = { connected: boolean; ids: string; currentId: string; currentAuthorizedAt: number; }; function snapshotWorkspaces(status: { connected?: boolean; organization?: { id?: string } | null; workspaces?: Array<{ id: string; current: boolean; authorizedAt?: number | null }>; } | null): WorkspaceSnapshot { const workspaces = status?.workspaces ?? []; const current = workspaces.find((entry) => entry.current); return { connected: Boolean(status?.connected), ids: workspaces.map((entry) => entry.id).slice().sort().join(','), currentId: current?.id || status?.organization?.id || '', currentAuthorizedAt: current?.authorizedAt ?? 0, }; } function authorizationCompleted(previous: WorkspaceSnapshot, next: WorkspaceSnapshot): boolean { if (!next.connected) return false; if (!previous.connected) return true; return next.ids !== previous.ids || next.currentId !== previous.currentId || next.currentAuthorizedAt !== previous.currentAuthorizedAt; } export const LinearSettings: React.FC = () => { const { t } = useI18n(); const runtimeLinear = getRegisteredRuntimeAPIs()?.linear; const status = useLinearAuthStore((state) => state.status); const isLoading = useLinearAuthStore((state) => state.isLoading); const hasChecked = useLinearAuthStore((state) => state.hasChecked); const refreshStatus = useLinearAuthStore((state) => state.refreshStatus); const setStatus = useLinearAuthStore((state) => state.setStatus); const [isBusy, setIsBusy] = React.useState(false); const [isWaiting, setIsWaiting] = React.useState(false); const [open, setOpen] = React.useState(false); const pollTimerRef = React.useRef(null); const stopWaiting = React.useCallback(() => { if (pollTimerRef.current != null) { window.clearInterval(pollTimerRef.current); pollTimerRef.current = null; } setIsWaiting(false); }, []); React.useEffect(() => { if (!runtimeLinear) { return; } if (!hasChecked) { void refreshStatus(runtimeLinear); } return () => { stopWaiting(); }; }, [hasChecked, refreshStatus, runtimeLinear, stopWaiting]); const startConnect = React.useCallback(async () => { if (!runtimeLinear) return; stopWaiting(); setIsBusy(true); const previous = snapshotWorkspaces(useLinearAuthStore.getState().status); try { const payload = await runtimeLinear.authStart(isDesktopShell() ? 'desktop' : 'web'); setIsWaiting(true); setOpen(true); void openExternalUrl(payload.authorizationUrl); const deadline = Date.now() + AUTHORIZATION_WATCH_MS; pollTimerRef.current = window.setInterval(() => { void (async () => { if (Date.now() > deadline) { stopWaiting(); toast.error(t('settings.integrations.linear.toast.authorizationFailed')); return; } const next = await refreshStatus(runtimeLinear, { force: true }); if (authorizationCompleted(previous, snapshotWorkspaces(next))) { stopWaiting(); toast.success(t('settings.integrations.linear.toast.connected')); void focusDesktopWindow(); } })(); }, AUTHORIZATION_POLL_MS); } catch (error) { console.error('Failed to start Linear connect:', error); toast.error(t('settings.integrations.linear.toast.startConnectFailed')); stopWaiting(); } finally { setIsBusy(false); } }, [refreshStatus, runtimeLinear, stopWaiting, t]); const activateWorkspace = React.useCallback(async (organizationId: string) => { if (!runtimeLinear || !organizationId) return; setIsBusy(true); try { const payload = await runtimeLinear.authActivate(organizationId); setStatus(payload); toast.success(t('settings.integrations.linear.toast.workspaceSwitched')); } catch (error) { console.error('Failed to switch Linear workspace:', error); toast.error(t('settings.integrations.linear.toast.workspaceSwitchFailed')); } finally { setIsBusy(false); } }, [runtimeLinear, setStatus, t]); const disconnect = React.useCallback(async () => { if (!runtimeLinear) return; setIsBusy(true); try { stopWaiting(); await runtimeLinear.authDisconnect(); toast.success(t('settings.integrations.linear.toast.disconnected')); await refreshStatus(runtimeLinear, { force: true }); } catch (error) { console.error('Failed to disconnect Linear:', error); toast.error(t('settings.integrations.linear.toast.disconnectFailed')); } finally { setIsBusy(false); } }, [refreshStatus, runtimeLinear, stopWaiting, t]); if (!runtimeLinear) { return null; } const connected = Boolean(status?.connected); const user = status?.user; const organization = status?.organization; const workspaces = status?.workspaces ?? []; const otherWorkspaces = workspaces.filter((workspace) => !workspace.current); const displayName = user?.displayName?.trim() || user?.name?.trim() || t('settings.integrations.linear.label.unknownUser'); const statusLabel = isWaiting ? t('settings.integrations.linear.status.waiting') : isLoading && !hasChecked ? t('common.loading') : connected ? (organization?.name?.trim() || t('settings.integrations.linear.status.connected')) : t('settings.integrations.linear.status.notConnected'); const statusClassName = isWaiting ? 'bg-[var(--status-warning)]/15 text-[var(--status-warning)]' : connected ? 'bg-[var(--status-success)]/15 text-[var(--status-success)]' : 'bg-[var(--surface-muted)] text-muted-foreground'; const expanded = isWaiting || open; return ( { if (isWaiting) { setOpen(true); return; } setOpen(nextOpen); }} >
{t('settings.integrations.linear.title')}

{t('settings.integrations.linear.description')}

{statusLabel}
{connected ? (
{user?.avatarUrl ? ( {t('settings.integrations.linear.avatarAlt.withName', ) : (
)}
{displayName}

{[organization?.name, user?.email].filter(Boolean).join(' ยท ')}

) : isWaiting ? (

{t('settings.integrations.linear.flow.description')}

) : null} {connected ? ( <> {otherWorkspaces.length > 0 ? (

{t('settings.integrations.linear.label.otherWorkspaces')}

{otherWorkspaces.map((workspace) => { const workspaceUser = workspace.user; const workspaceName = workspace.name?.trim() || t('settings.integrations.linear.status.connected'); return (
{workspaceName}
{workspaceUser?.email ? (

{workspaceUser.email}

) : null}
); })}
) : null}
) : isWaiting ? (
{t('settings.integrations.linear.flow.waiting')}
) : ( )}
); };