import React, { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; import { Building2, Eye, EyeOff, ExternalLink, Link2Off, Loader2, RefreshCw } from 'lucide-react'; import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { SectionCard } from './dataShared'; function TokenInput({ value, onChange, disabled }) { const [show, setShow] = useState(false); const tail = value.slice(-4); return (
{value && ( )}
{value && !show && (

···{tail}

)}
); } export default function BankSyncSection({ onConnectionChange }) { const [enabled, setEnabled] = useState(null); const [connections, setConnections] = useState([]); const [loadError, setLoadError] = useState(''); const [setupToken, setSetupToken] = useState(''); const [connecting, setConnecting] = useState(false); const [syncing, setSyncing] = useState(null); const [disconnectTarget, setDisconnectTarget] = useState(null); const [disconnecting, setDisconnecting] = useState(false); const load = useCallback(async () => { setLoadError(''); try { const [status, sources] = await Promise.all([ api.simplefinStatus(), api.dataSources({ type: 'provider_sync' }), ]); setEnabled(status.enabled); const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []; setConnections(conns); onConnectionChange?.(conns[0] || null); } catch (err) { setEnabled(false); setLoadError(err.message || 'Failed to load bank sync status'); } }, [onConnectionChange]); useEffect(() => { load(); }, [load]); const handleConnect = async () => { const token = setupToken.trim(); if (!token) { toast.error('Paste your SimpleFIN setup token first.'); return; } setConnecting(true); try { const result = await api.connectSimplefin(token); toast.success(`Connected — ${result.accountsUpserted} account(s), ${result.transactionsNew} new transaction(s).`); setSetupToken(''); await load(); } catch (err) { toast.error(err.message || 'Failed to connect SimpleFIN'); } finally { setConnecting(false); } }; const handleSync = async (id) => { setSyncing(id); try { const result = await api.syncDataSource(id); toast.success(`Synced — ${result.transactionsNew} new transaction(s).`); await load(); } catch (err) { toast.error(err.message || 'Sync failed'); await load(); } finally { setSyncing(null); } }; const handleDisconnect = async () => { if (!disconnectTarget) return; setDisconnecting(true); try { await api.deleteDataSource(disconnectTarget.id); toast.success('SimpleFIN disconnected.'); setDisconnectTarget(null); await load(); } catch (err) { toast.error(err.message || 'Failed to disconnect'); } finally { setDisconnecting(false); } }; function fmtDate(iso) { if (!iso) return '—'; return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } if (enabled === null) { return (
Loading…
); } return ( <> {!enabled ? (
Bank sync is not enabled on this server. Ask your administrator to enable it in the Admin panel.
) : ( <> {loadError && (
{loadError}
)} {connections.length > 0 && connections.map(conn => (

{conn.name}

{conn.account_count} account{conn.account_count !== 1 ? 's' : ''} ·{' '} {conn.transaction_count} transaction{conn.transaction_count !== 1 ? 's' : ''}

Last sync

{fmtDate(conn.last_sync_at)}

Status

{conn.last_error ? conn.last_error : (conn.status === 'active' ? 'Active' : conn.status)}

))} {connections.length === 0 && (

Connect a SimpleFIN Bridge account

Paste your SimpleFIN setup token below. BillTracker only stores an encrypted access URL — no bank credentials are saved.

Need a token?{' '} Open SimpleFIN Bridge

setSetupToken(e.target.value)} disabled={connecting} />
)} {connections.length > 0 && (

Add another connection

Get a SimpleFIN token
setSetupToken(e.target.value)} disabled={connecting} />
)} )}
{ if (!open) setDisconnectTarget(null); }}> Disconnect SimpleFIN? This removes the connection and deletes synced accounts. Previously synced transactions are kept but will no longer be associated with a data source. Cancel {disconnecting ? 'Disconnecting…' : 'Disconnect'} ); }