import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ArrowLeft, ClipboardCopy, Loader2, ShieldAlert, Link2, Sparkles, UserPlus, Users, Trash2, } from 'lucide-react'; import AppShell from '@/components/layout/AppShell'; import EmailGeneratorTab from '@/components/campaigns/EmailGeneratorTab'; import LinkedInConnectSettings from '@/components/settings/LinkedInConnectSettings'; import ConnectMailboxSettings from '@/components/settings/ConnectMailboxSettings'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { apiFetch } from '@/lib/api'; import { cn } from '@/lib/utils'; export default function Settings() { /** 'main' | 'email_generator' — full-page generator inside Settings */ const [settingsPanel, setSettingsPanel] = useState('main'); const [me, setMe] = useState(null); const [meLoading, setMeLoading] = useState(true); const [members, setMembers] = useState([]); const [membersLoading, setMembersLoading] = useState(true); const [membersError, setMembersError] = useState(''); const [inviteEmail, setInviteEmail] = useState(''); const [inviteBusy, setInviteBusy] = useState(false); const [inviteResult, setInviteResult] = useState(null); /** e.g. `member:12` or `invite:3` while a remove/revoke request is in flight */ const [removeBusyKey, setRemoveBusyKey] = useState(null); const loadMe = useCallback(async () => { setMeLoading(true); try { const r = await apiFetch('/api/auth/me'); setMe(r.ok ? await r.json() : null); } catch { setMe(null); } finally { setMeLoading(false); } }, []); const loadMembers = useCallback(async () => { setMembersLoading(true); setMembersError(''); try { const r = await apiFetch('/api/tenants/members'); if (!r.ok) { const data = await r.json().catch(() => ({})); setMembersError( typeof data.detail === 'string' ? data.detail : 'Could not load members' ); setMembers([]); return; } const data = await r.json(); setMembers(data.members || []); } catch (e) { setMembersError(String(e)); setMembers([]); } finally { setMembersLoading(false); } }, []); useEffect(() => { loadMe(); }, [loadMe]); const isAdmin = me?.current_role === 'admin'; useEffect(() => { if (!meLoading && isAdmin) { loadMembers(); } }, [meLoading, isAdmin, loadMembers]); const webhookUrl = useMemo(() => { if (typeof window === 'undefined') return ''; const base = `${window.location.origin}/api/webhooks/smartlead`; const tid = me?.current_tenant_id; if (tid != null && !Number.isNaN(Number(tid))) { return `${base}?tenant_id=${tid}`; } return base; }, [me]); const tenants = me?.tenants || []; const multiWorkspace = tenants.length > 1; const switchTenant = async (tid) => { const id = parseInt(tid, 10); if (!id || id === me?.current_tenant_id) return; try { const res = await apiFetch('/api/auth/switch-tenant', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenant_id: id }), }); if (!res.ok) return; window.location.reload(); } catch (e) { console.error(e); } }; const sendInvite = async () => { const email = inviteEmail.trim().toLowerCase(); if (!email || !email.includes('@')) return; setInviteBusy(true); setInviteResult(null); try { const res = await apiFetch('/api/tenants/invite', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, role: 'member' }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { setInviteResult({ error: typeof data.detail === 'string' ? data.detail : 'Invite failed', }); return; } setInviteResult({ url: data.invite_url, emailSent: !!data.email_sent, emailError: data.email_error || null, inviteeEmail: data.email, }); setInviteEmail(''); loadMe(); await loadMembers(); } catch (e) { setInviteResult({ error: String(e) }); } finally { setInviteBusy(false); } }; const removeMember = async (userId) => { if (!window.confirm('Remove this user from the workspace?')) return; setRemoveBusyKey(`member:${userId}`); try { const res = await apiFetch(`/api/tenants/members/${userId}`, { method: 'DELETE' }); const data = await res.json().catch(() => ({})); if (!res.ok) { alert(typeof data.detail === 'string' ? data.detail : 'Could not remove member'); return; } await loadMembers(); } catch (e) { alert(e.message || 'Could not remove member'); } finally { setRemoveBusyKey(null); } }; const revokeInvitation = async (invitationId) => { if (!window.confirm('Cancel this invitation? They will not be able to join with the current link.')) return; setRemoveBusyKey(`invite:${invitationId}`); try { const res = await apiFetch(`/api/tenants/invitations/${invitationId}`, { method: 'DELETE' }); const data = await res.json().catch(() => ({})); if (!res.ok) { alert(typeof data.detail === 'string' ? data.detail : 'Could not cancel invitation'); return; } await loadMembers(); } catch (e) { alert(e.message || 'Could not cancel invitation'); } finally { setRemoveBusyKey(null); } }; if (meLoading) { return (
); } if (!me) { return (

Sign in to manage workspace settings.

); } if (settingsPanel === 'email_generator') { return (
); } return (
{!isAdmin ? (

Inviting users and removing members is limited to workspace admins. Ask an admin if you need help.

) : null} {isAdmin ? (
Invite people

We email an invitation from your Google account. The invitee must sign in with the same email address.

{me?.gmail_invites_ready === false ? (

Sign in with Google again (you will see a consent screen) so invites can send from your address, or use{' '} Reconnect Google for invites .

) : null}
setInviteEmail(e.target.value)} className="h-10" />
{inviteResult?.error ? (

{inviteResult.error}

) : null} {inviteResult?.emailSent ? (

Invitation email sent to {inviteResult.inviteeEmail}.

) : null} {inviteResult?.emailError ? (

{inviteResult.emailError}

) : null} {inviteResult?.url ? (

Invite link (copy if email did not arrive; expires in 7 days):

) : null}
) : null} {isAdmin ? (
Members
{membersLoading ? (
) : membersError ? (

{membersError}

) : members.length === 0 ? (

No members or pending invitations.

) : (
{members.map((m) => { const rowKey = m.user_id != null ? `m-${m.user_id}` : `i-${m.invitation_id}`; const isSelf = m.user_id === me.user_id; const isInvited = m.status === 'invited'; const busyMember = removeBusyKey === `member:${m.user_id}`; const busyInvite = m.invitation_id != null && removeBusyKey === `invite:${m.invitation_id}`; return ( ); })}
Email Name Role Status
{m.email || '—'} {m.name || '—'} {m.role} {isInvited ? 'Invited' : 'Active'} {isSelf ? ( You ) : isInvited ? ( ) : ( )}
)}
) : null}
Email / AI generator

Import an Apollo CSV, choose products, edit prompts, and generate email sequences in a guided flow. Opens full width here so you can focus on the tool.

Smartlead webhook

Paste this URL in Smartlead so reply events are stored for this workspace. The link includes tenant_id for the active workspace.

{webhookUrl}
{multiWorkspace ? (

Switch workspace

The webhook URL updates when you change workspace (different{' '} tenant_id).

) : null}
); }