'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 && ( )} ); };