import React, { useState } from 'react';
import { ActivityIndicator, Image, Pressable, Text, View } from 'react-native';
import { AlertCircle, Info, MoreHorizontal, Plug, Unplug } from 'lucide-react-native';
import { ConnectorBrandIcon, hasConnectorBrandIcon } from './connectorBrandIcons';
import { connectorDescription, connectorSubtitle, getConnectorScopesForMode } from './connectorPanelUtils';
import { connectorPanelStyles as cStyles } from './connectorPanelStyles';
import { editorShellStyles } from '../editor/editorShellStyles';
import { useAgentBi } from '../../analytics/mixpanelContext';
import { styles as sharedStyles } from '../../styles';
import { themedColor } from '../../theme';
import type {
SuperagentConnector,
SuperagentConnectorAccessMode,
SuperagentConnectorActionInput,
} from '../../types';
export function ConnectorCard({
agentId,
connector,
isConnecting,
onCancel,
onSelect,
}: {
agentId: string;
connector: SuperagentConnector;
isConnecting?: boolean;
onCancel?: (input: { agentId: string; connectorId: string }) => void;
onSelect: () => void;
}) {
// While this connector is connecting, the card stays a spinner + Cancel (not a
// pressable "Connect" chip) so backing out of the detail view during OAuth
// still leaves the flow visible and cancelable.
if (isConnecting) {
return (
{connector.name}
{connectorSubtitle(connector)}
onCancel?.({ agentId, connectorId: connector.id })}
style={({ pressed }) => [editorShellStyles.cancelConnectButton, pressed && sharedStyles.pressed]}
>
Cancel
);
}
return (
[editorShellStyles.connectorCard, cStyles.cardSurface, pressed && sharedStyles.pressed]}
>
{connector.name}
{connectorSubtitle(connector)}
Connect
);
}
export function ConnectedConnectorCard({
agentId,
connector,
isConnecting,
onCancel,
onConnect,
onDisconnect,
onRemove,
}: {
agentId: string;
connector: SuperagentConnector;
isConnecting: boolean;
onCancel?: (input: { agentId: string; connectorId: string }) => void;
onConnect?: (input: SuperagentConnectorActionInput) => Promise | boolean | string | void;
onDisconnect?: (input: SuperagentConnectorActionInput) => Promise | void;
onRemove?: (input: SuperagentConnectorActionInput) => Promise | void;
}) {
const status = connector.status ?? 'active';
const [menuOpen, setMenuOpen] = useState(false);
const bi = useAgentBi();
const hasAccessModes = !!connector.accessModes;
const currentMode = connector.accessMode ?? null;
const oppositeMode: SuperagentConnectorAccessMode = currentMode === 'read_only' ? 'full_access' : 'read_only';
return (
{connector.name}
{connector.accountIdentifier || connectorDescription(connector)}
{isConnecting ? (
onCancel?.({ agentId, connectorId: connector.id })}
style={({ pressed }) => [editorShellStyles.cancelConnectButton, pressed && sharedStyles.pressed]}
>
Cancel
) : (
setMenuOpen((open) => !open)}
style={({ pressed }) => [editorShellStyles.moreButton, pressed && sharedStyles.pressed]}
>
)}
{menuOpen ? (
{hasAccessModes && onConnect && !connector.requiresConnectionConfig ? (
{
setMenuOpen(false);
const result = await onConnect({
accessMode: oppositeMode,
agentId,
connectorId: connector.id,
forceReconnect: true,
scopes: getConnectorScopesForMode(connector, oppositeMode),
});
if (result) {
void bi.trackEditor('Plugin Access Mode Switch', { connector_type: connector.id, from_mode: currentMode ?? 'full_access', to_mode: oppositeMode });
}
}}
/>
) : null}
{connector.requiresConnectionConfig ? (
) : (
{
setMenuOpen(false);
const result = await onConnect?.({
accessMode: connector.accessMode ?? undefined,
agentId,
connectorId: connector.id,
forceReconnect: true,
scopes: connector.scopes?.length ? connector.scopes : getConnectorScopesForMode(connector, connector.accessMode ?? 'full_access'),
});
// A successful reconnect / switch-account counts as a connect too.
if (result) {
void bi.trackEditor('Plugin Connect', { connector_type: connector.id, access_mode: connector.accessMode ?? 'full_access' });
}
}}
/>
)}
{status !== 'disconnected' ? (
{
setMenuOpen(false);
onDisconnect?.({ agentId, connectorId: connector.id });
}}
/>
) : null}
{
setMenuOpen(false);
onRemove?.({ agentId, connectorId: connector.id });
}}
/>
) : null}
);
}
export function ConnectorIcon({ connector }: { connector: SuperagentConnector }) {
const iconBackgroundColor = connector.iconBackgroundColor ?? '#FFFFFF';
const hasBrandIcon = hasConnectorBrandIcon(connector.id);
const [imageFailed, setImageFailed] = useState(false);
const iconUrl = connector.iconUrl?.trim();
return (
{hasBrandIcon ? (
) : iconUrl && !imageFailed ? (
setImageFailed(true)}
resizeMode="contain"
source={{ uri: iconUrl }}
style={editorShellStyles.connectorIconImage}
/>
) : (
)}
);
}
// Matches the web ConnectorStatusLabel: a small colored dot + label, no filled
// pill. Active = green dot + green text; expired = alert + danger; disconnected
// = unplug + muted.
export function StatusBadge({ status }: { status: SuperagentConnector['status'] }) {
const isActive = status === 'active' || !status;
const isDisconnected = status === 'disconnected';
if (isActive) {
return (
Active
);
}
if (isDisconnected) {
return (
Disconnected
);
}
return (
{status}
);
}
function MenuAction({ destructive, label, onPress }: { destructive?: boolean; label: string; onPress: () => void }) {
return (
[
editorShellStyles.menuAction,
pressed && sharedStyles.pressed,
]}>
{label}
);
}
function MenuNote({ label }: { label: string }) {
return (
{label}
);
}