import React, { useState } from 'react'; import { createThemedStyles, themedColor } from '../../theme'; import { ActivityIndicator, Pressable, Text, View } from 'react-native'; import { Copy, ExternalLink, MessageCircle, RefreshCw, Unplug, } from 'lucide-react-native'; import { ChannelBrandIcon } from './channelBrandIcons'; import { editorShellStyles } from '../editor/editorShellStyles'; import { slackWorkspaceEntries } from './slackChannelUtils'; import { useSuperagentChannels } from '../../runtime/runtimeContext'; import { styles } from '../../styles'; import type { SuperagentAgent, SuperagentChannelStatus, SuperagentHomeScreenViewProps, SuperagentSlackChannelStatus, } from '../../types'; import { useCanCreateManagedTelegramBot, useCanManageSuperagent, } from '../../user/userContext'; export function ChannelsPanel({ agent }: { agent: SuperagentAgent }) { const { channelStatus, connectingChannelId, disconnectingChannelId, onConnectSlack, onDisconnectSlack, onDisconnectTelegram, onDisconnectIMessage, onDisconnectWhatsApp, onGenerateIMessageCode, onOpenIMessage, onOpenTelegram, onOpenWhatsApp, onSetupTelegram, onShareIMessageCode, } = useSuperagentChannels(); const canManage = useCanManageSuperagent(agent); const canSetupTelegram = useCanCreateManagedTelegramBot(agent); const imessage = channelStatus?.imessage; const telegram = channelStatus?.telegram; const whatsapp = channelStatus?.whatsapp; const slack = channelStatus?.slack; // LINE is hidden for now — its connection status isn't wired on native yet // (no runtime status route), so it would always show as disconnected. return ( {/* LINE hidden for now — real connection status isn't wired on native yet (no runtime status route). Re-add once it is. */} ); } function WhatsAppChannel({ agentId, canManage, connected, isConnecting, isDisconnecting, onDisconnectWhatsApp, onOpenWhatsApp, userHandle, }: { agentId: string; canManage: boolean; connected: boolean; isConnecting: boolean; isDisconnecting: boolean; onDisconnectWhatsApp?: SuperagentHomeScreenViewProps['onDisconnectWhatsApp']; onOpenWhatsApp?: SuperagentHomeScreenViewProps['onOpenWhatsApp']; userHandle?: string | null; }) { // Any WhatsApp action in flight disables both CTAs, but each button only spins // for its own action so a disconnect doesn't also spin "Open WhatsApp". const isBusy = isConnecting || isDisconnecting; return ( } title="WhatsApp" > {/* The handler builds the connect URL on press, so don't gate on a preloaded connectUrl — it's missing before status loads even though the press works. */} : undefined} isBusy={isConnecting} label={connected ? 'Open WhatsApp' : 'Connect'} onPress={() => onOpenWhatsApp?.({ agentId })} /> {connected ? ( } isBusy={isDisconnecting} label="Disconnect" onPress={() => onDisconnectWhatsApp?.({ agentId })} /> ) : null} ); } function IMessageChannel({ agentId, imessage, isBusy, onDisconnectIMessage, onGenerateIMessageCode, onOpenIMessage, onShareIMessageCode, }: { agentId: string; imessage?: SuperagentChannelStatus['imessage']; isBusy: boolean; onDisconnectIMessage?: SuperagentHomeScreenViewProps['onDisconnectIMessage']; onGenerateIMessageCode?: SuperagentHomeScreenViewProps['onGenerateIMessageCode']; onOpenIMessage?: SuperagentHomeScreenViewProps['onOpenIMessage']; onShareIMessageCode?: SuperagentHomeScreenViewProps['onShareIMessageCode']; }) { const activation = imessage?.activation; const phoneNumber = imessage?.phoneNumber ?? activation?.phoneNumber ?? null; const [localError, setLocalError] = useState(null); const connectIMessage = async () => { if (!onGenerateIMessageCode) { return; } setLocalError(null); try { const generated = await onGenerateIMessageCode({ agentId }); if (generated && onOpenIMessage) { await onOpenIMessage({ agentId, ...generated }); } } catch (error) { setLocalError(error instanceof Error ? error.message : 'iMessage setup failed.'); } }; return ( } title="iMessage" > {imessage?.connected ? ( } isBusy={isBusy} label="Disconnect" onPress={() => onDisconnectIMessage?.({ agentId })} /> ) : ( {localError ? {localError} : null} {activation ? ( Text this activation code {activation.code} To: {activation.phoneNumber} Code expires in 30 minutes. ) : null} {activation ? ( <> } label="Open Messages" onPress={() => onOpenIMessage?.({ agentId, ...activation })} /> } label="Share code" onPress={() => onShareIMessageCode?.({ agentId, ...activation })} /> ) : null} )} ); } function TelegramChannel({ agentDisplayName, agentId, agentProfilePhotoUrl, canManage, canSetup, isBusy, onDisconnectTelegram, onOpenTelegram, onSetupTelegram, telegram, }: { agentDisplayName: string; agentId: string; agentProfilePhotoUrl?: string; canManage: boolean; canSetup: boolean; isBusy: boolean; onDisconnectTelegram?: SuperagentHomeScreenViewProps['onDisconnectTelegram']; onOpenTelegram?: SuperagentHomeScreenViewProps['onOpenTelegram']; onSetupTelegram?: SuperagentHomeScreenViewProps['onSetupTelegram']; telegram?: SuperagentChannelStatus['telegram']; }) { const botTitle = telegram?.botName || telegram?.botUsername || 'Telegram bot'; return ( } title="Telegram" > {telegram?.connected ? ( {telegram.botLink ? ( } label="Open bot" onPress={() => onOpenTelegram?.({ agentId, url: telegram.botLink || '' })} /> ) : null} } isBusy={isBusy} label="Disconnect" onPress={() => onDisconnectTelegram?.({ agentId })} /> ) : canSetup ? ( onSetupTelegram?.({ agentDisplayName, agentId, agentProfilePhotoUrl })} /> ) : null} ); } function SlackChannel({ agentId, isConnecting, isDisconnecting, onConnectSlack, onDisconnectSlack, slack, }: { agentId: string; isConnecting: boolean; isDisconnecting: boolean; onConnectSlack?: SuperagentHomeScreenViewProps['onConnectSlack']; onDisconnectSlack?: SuperagentHomeScreenViewProps['onDisconnectSlack']; slack?: SuperagentSlackChannelStatus; }) { // Any Slack action in flight disables both CTAs, but each button only spins // for its own action so a disconnect doesn't also spin "Connect". const isBusy = isConnecting || isDisconnecting; const connected = !!slack?.connected; const supported = slack?.supported !== false; const pendingCleanup = !!slack?.pendingCleanup; const workspaces = slackWorkspaceEntries(slack); const teamName = workspaces[0]?.teamName ?? slack?.teamName ?? null; const description = supported ? 'Add your agent to a Slack workspace, then mention it to start chatting.' : 'Slack is not available on this platform yet.'; // Tearing down a live connection is destructive, so this stays the red danger // button. (The pending-cleanup retry below is a neutral recovery action.) const disconnectButton = ( } isBusy={isDisconnecting} label="Disconnect" onPress={() => onDisconnectSlack?.({ agentId })} /> ); const connectedCleanupNotice = pendingCleanup ? ( Some workspaces are still being disconnected on Slack’s side. ) : null; // Not-connected states: notice + an explicit "Retry disconnect" affordance, // the only path left to retire a still-active external install/@handle. const pendingCleanupNotice = pendingCleanup ? ( Some workspaces are still being disconnected on Slack’s side. } isBusy={isDisconnecting} label="Retry disconnect" onPress={() => onDisconnectSlack?.({ agentId })} /> ) : null; return ( } title="Slack" > {connected ? ( <> {workspaces.length > 0 ? ( {workspaces.map((workspace, index) => ( 0 ? channelStyles.slackWorkspaceStacked : undefined}> {/* Name each workspace only when there's more than one — otherwise the single team's name already shows in the card status line. */} {workspaces.length > 1 && workspace.teamName ? ( {workspace.teamName} ) : null} {workspace.usergroupHandle ? ( <> @{workspace.usergroupHandle} Mention this handle in Slack to reach the agent. ) : ( Mention the “Superagents by Base44” bot in your channel. )} ))} ) : null} {connectedCleanupNotice} {disconnectButton} ) : supported || pendingCleanup ? ( {pendingCleanupNotice} {supported ? ( onConnectSlack?.({ agentId })} /> ) : null} ) : null} ); } function LineChannel({ agentId, isBusy, line, onGenerateLineCode, onOpenLine, onShareLineCode, }: { agentId: string; isBusy: boolean; line?: SuperagentChannelStatus['line']; onGenerateLineCode?: SuperagentHomeScreenViewProps['onGenerateLineCode']; onOpenLine?: SuperagentHomeScreenViewProps['onOpenLine']; onShareLineCode?: SuperagentHomeScreenViewProps['onShareLineCode']; }) { const activation = line?.activation; return ( } title="LINE" > {activation ? ( Activation code {activation.code} } label="Open LINE" onPress={() => onOpenLine?.({ agentId, url: activation.addFriendUrl })} /> } label="Share code" onPress={() => onShareLineCode?.({ agentId, ...activation })} /> ) : ( onGenerateLineCode?.({ agentId })} /> )} ); } function ChannelCard({ children, connected, description, detail, icon, title, }: { children: React.ReactNode; connected?: boolean; description?: string; detail?: string | null; icon: React.ReactNode; title: string; }) { return ( {icon} {title} {connected ? ( Connected {detail ? · {detail} : null} ) : description ? ( {description} ) : null} {children} ); } function ChannelButton({ accessibilityLabel, danger, disabled, icon, isBusy, label, onPress, secondary, }: { accessibilityLabel?: string; danger?: boolean; disabled?: boolean; icon?: React.ReactNode; isBusy?: boolean; label: string; onPress?: () => void; secondary?: boolean; }) { return ( [ channelStyles.button, secondary && channelStyles.buttonSecondary, danger && channelStyles.buttonDanger, disabled && channelStyles.disabled, pressed && styles.pressed, ]} > {isBusy ? : icon} {label} ); } const channelStyles = createThemedStyles({ actionRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end', marginTop: 12, }, activationCode: { color: '#F4F4F5', fontFamily: 'Courier', fontSize: 20, fontWeight: '900', letterSpacing: 0, marginTop: 5, }, button: { alignItems: 'center', backgroundColor: '#F4F4F5', borderRadius: 8, flexDirection: 'row', gap: 6, minHeight: 36, paddingHorizontal: 13, }, buttonDanger: { backgroundColor: 'transparent', borderColor: '#552022', borderWidth: 1, }, buttonSecondary: { backgroundColor: 'transparent', borderColor: '#353539', borderWidth: 1, }, buttonText: { color: '#111111', fontSize: 12, fontWeight: '900', }, buttonTextDanger: { color: '#FCA5A5', }, buttonTextSecondary: { color: '#F4F4F5', }, card: { backgroundColor: '#0B0B0C', borderColor: '#242427', borderRadius: 8, borderWidth: 1, marginBottom: 12, padding: 16, }, cardHeader: { alignItems: 'flex-start', flexDirection: 'row', }, channelBody: { color: '#D4D4D8', fontSize: 14, fontWeight: '600', lineHeight: 19, marginTop: 3, }, channelIcon: { marginRight: 12, marginTop: 1, }, channelText: { flex: 1, minWidth: 0, }, channelTitle: { color: '#F4F4F5', fontSize: 16, fontWeight: '500', }, cleanupBox: { backgroundColor: '#101012', borderColor: '#2A2A2A', borderRadius: 8, borderWidth: 1, marginTop: 12, padding: 11, }, cleanupText: { color: '#A1A1AA', fontSize: 12, fontWeight: '600', lineHeight: 17, }, codeLabel: { color: '#8E8E93', fontSize: 11, fontWeight: '900', textTransform: 'uppercase', }, connectedBody: { color: '#A1A1AA', fontSize: 12, fontWeight: '700', marginTop: 4, }, connectedBox: { backgroundColor: '#151515', borderColor: '#2A2A2A', borderRadius: 8, borderWidth: 1, marginTop: 12, padding: 12, }, disabled: { opacity: 0.55, }, errorText: { color: '#FCA5A5', fontSize: 12, fontWeight: '700', marginTop: 7, }, expiryText: { color: '#71717A', fontSize: 11, fontWeight: '700', marginTop: 7, }, helpText: { color: '#A1A1AA', fontSize: 12, fontWeight: '600', lineHeight: 17, }, noticeSpacing: { marginTop: 12, }, setupStack: { marginTop: 12, }, slackHandle: { color: '#F4F4F5', fontFamily: 'Courier', fontSize: 15, fontWeight: '900', marginTop: 3, }, slackHint: { color: '#71717A', fontSize: 11, fontWeight: '700', marginTop: 3, }, slackWorkspaceStacked: { marginTop: 14, }, statusConnected: { color: '#D4D4D8', fontSize: 14, fontWeight: '600', }, statusDetail: { color: '#A1A1AA', fontSize: 14, fontWeight: '600', }, statusDot: { backgroundColor: '#34D399', borderRadius: 4, height: 7, marginRight: 7, width: 7, }, statusLine: { alignItems: 'center', flexDirection: 'row', marginTop: 4, }, });