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,
},
});