'use client';
import { useCallback, useEffect, useState } from 'react';
import {
Box,
Button,
Field,
Flex,
HStack,
IconButton,
Input,
Stack,
Text,
} from '@chakra-ui/react';
import { FiEdit2 } from 'react-icons/fi';
import type { MarketUniverse } from '@tradejs/types';
import { toaster } from '#ui';
type PublicTradingAccount = {
id: string;
label: string;
provider: string;
enabled: boolean;
isDefault?: boolean;
universes: MarketUniverse[];
environment: 'mainnet' | 'testnet';
hasApiKey: boolean;
hasApiSecret: boolean;
maskedApiKey: string;
maskedApiSecret: string;
};
const FieldInput = ({
label,
value,
onChange,
type,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
}) => (
{label}
onChange(event.target.value)}
/>
);
type CredentialFieldName = 'apiKey' | 'apiSecret';
const AccountCredentialField = ({
label,
savedValue,
value,
editing,
onEdit,
onChange,
}: {
label: string;
savedValue: string;
value: string;
editing: boolean;
onEdit: () => void;
onChange: (value: string) => void;
}) => (
{label}
{editing ? (
onChange(event.target.value)}
autoFocus
fontFamily="mono"
/>
) : (
{savedValue || 'Not set'}
)}
);
export const TradingAccountsPanel = () => {
const [accounts, setAccounts] = useState([]);
const [saving, setSaving] = useState<'account' | 'default' | null>(null);
const [settingDefaultId, setSettingDefaultId] = useState('');
const [accountDraft, setAccountDraft] = useState({
id: '',
label: '',
apiKey: '',
apiSecret: '',
});
const [addingAccount, setAddingAccount] = useState(false);
const [editingCredentials, setEditingCredentials] = useState<
Record
>({ apiKey: false, apiSecret: false });
const load = useCallback(async () => {
const accountsResponse = await fetch('/api/user/trading-accounts');
if (accountsResponse.ok) {
const payload = (await accountsResponse.json()) as {
accounts?: PublicTradingAccount[];
};
const nextAccounts = payload.accounts ?? [];
setAccounts(nextAccounts);
}
}, []);
useEffect(() => {
void load();
}, [load]);
const saveAccount = async () => {
setSaving('account');
try {
const normalizedLabel = accountDraft.label.trim();
const baseId =
normalizedLabel
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '') || 'account';
const generatedId = `bybit-${baseId}`;
const id =
accountDraft.id ||
(accounts.some((account) => account.id === generatedId)
? `${generatedId}-${Date.now().toString(36)}`
: generatedId);
const response = await fetch('/api/user/trading-accounts', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
...accountDraft,
id,
provider: 'bybit',
enabled: true,
universes: ['crypto', 'tradfi'],
environment: 'mainnet',
isDefault:
accounts.length === 0 ||
accounts.find((account) => account.id === id)?.isDefault === true,
}),
});
const payload = (await response.json()) as { error?: string };
if (!response.ok) throw new Error(payload.error || 'Save failed');
setAccountDraft({
id: '',
label: '',
apiKey: '',
apiSecret: '',
});
setAddingAccount(false);
setEditingCredentials({ apiKey: false, apiSecret: false });
await load();
toaster.create({ title: 'Trading account saved', type: 'success' });
} catch (error) {
toaster.create({
title: error instanceof Error ? error.message : 'Save failed',
type: 'error',
});
} finally {
setSaving(null);
}
};
const editCredential = (
account: PublicTradingAccount,
field: CredentialFieldName,
) => {
if (accountDraft.id !== account.id) {
setAccountDraft({
id: account.id,
label: account.label,
apiKey: '',
apiSecret: '',
});
setEditingCredentials({
apiKey: field === 'apiKey',
apiSecret: field === 'apiSecret',
});
return;
}
setEditingCredentials((current) => ({ ...current, [field]: true }));
};
const makeDefault = async (account: PublicTradingAccount) => {
setSaving('default');
setSettingDefaultId(account.id);
try {
const response = await fetch('/api/user/trading-accounts', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
id: account.id,
label: account.label,
provider: account.provider,
enabled: account.enabled,
universes: account.universes,
environment: account.environment,
isDefault: true,
}),
});
const payload = (await response.json()) as { error?: string };
if (!response.ok) throw new Error(payload.error || 'Save failed');
await load();
toaster.create({
title: 'Default trading account updated',
type: 'success',
});
} catch (error) {
toaster.create({
title: error instanceof Error ? error.message : 'Save failed',
type: 'error',
});
} finally {
setSaving(null);
setSettingDefaultId('');
}
};
return (
Bybit
Trading account credentials used by active strategies.
{accounts.map((account) => {
const isEditingAccount = accountDraft.id === account.id;
const hasChanges = Boolean(
isEditingAccount &&
(accountDraft.apiKey.trim() || accountDraft.apiSecret.trim()),
);
return (
1 ? '1px' : '0'}
borderColor="gray.700"
borderRadius="md"
p={accounts.length > 1 ? 3 : 0}
gap={3}
>
{account.label}
Bybit · {account.universes.join(' · ')} ·{' '}
{account.environment}
{account.isDefault
? 'default'
: account.enabled
? 'enabled'
: 'disabled'}
{!account.isDefault && account.enabled && (
)}
editCredential(account, 'apiKey')}
onChange={(apiKey) =>
setAccountDraft((draft) => ({ ...draft, apiKey }))
}
/>
editCredential(account, 'apiSecret')}
onChange={(apiSecret) =>
setAccountDraft((draft) => ({ ...draft, apiSecret }))
}
/>
);
})}
{(accounts.length === 0 || addingAccount) && (
Connect account
setAccountDraft((draft) => ({ ...draft, label }))
}
/>
setAccountDraft((draft) => ({ ...draft, apiKey }))
}
/>
setAccountDraft((draft) => ({ ...draft, apiSecret }))
}
/>
{accounts.length > 0 && (
)}
)}
{accounts.length > 0 && !addingAccount && (
)}
);
};