diff --git a/src/App.tsx b/src/App.tsx index 20fc307..f1f60d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import Names from '@/pages/Names'; import NameProfile from '@/pages/NameProfile'; import NamesAuctions from '@/pages/NamesAuctions'; import Activity from '@/pages/Activity'; +import History from '@/pages/History'; import Portfolio from '@/pages/Portfolio'; import Debug from '@/pages/Debug'; import { useChain } from '@/context/ChainContext'; @@ -223,7 +224,7 @@ export function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/pages/History.tsx b/src/pages/History.tsx index a68bf55..729f23a 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -1,45 +1,197 @@ +import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useChain } from '@/context/ChainContext'; -import { StellarHistory } from '@/components/StellarHistory'; +import { ActivityRow } from '@/components/ActivityRow'; import { EmptyState } from '@/components/EmptyState'; +import { + useActivityStore, + type ActivityEntry, + type ActivityKind, + type ActivityStatus, +} from '@/stores/activityStore'; +import { downloadActivityCsv, downloadActivityJson } from '@/utils/activityExport'; + +type HistoryChain = 'all' | 'horizen' | 'stellar' | 'solana' | 'ckb'; + +const CHAINS: HistoryChain[] = ['all', 'horizen', 'stellar', 'solana', 'ckb']; export default function History() { const navigate = useNavigate(); - const { chain } = useChain(); + const { entries, clearAll } = useActivityStore(); + const [filterKind, setFilterKind] = useState('all'); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterChain, setFilterChain] = useState('all'); + + const filteredEntries = useMemo(() => { + return entries + .filter((entry: ActivityEntry) => { + if (filterChain !== 'all' && entry.chain !== filterChain) return false; + if (filterKind !== 'all' && entry.kind !== filterKind) return false; + if (filterStatus !== 'all' && entry.status !== filterStatus) return false; + return true; + }) + .sort((a: ActivityEntry, b: ActivityEntry) => b.timestamp - a.timestamp); + }, [entries, filterChain, filterKind, filterStatus]); - if (chain === 'stellar') return ; + // Exports deliberately omit `metadata` so private key and memo material + // is never leaked into CSV or JSON downloads. + const exportEntries = useMemo(() => entries.map(({ metadata, ...rest }) => rest), [entries]); return ( - - - - History - - - + + + + Multi-Chain Activity + + + Activity History + + + clearAll()} + disabled={entries.length === 0} + className="rounded-lg bg-surface-container px-4 py-2 font-mono text-xs uppercase tracking-widest text-on-surface transition-colors hover:bg-error/20 hover:text-error disabled:cursor-not-allowed disabled:opacity-40" + > + Clear History + + + + + + + + Type + + setFilterKind(event.target.value as ActivityKind | 'all')} + className="rounded-lg border border-outline bg-surface px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary" + > + All Types + Stealth Send + Stealth Receive + Withdrawal + Name Registration + Vault Claim + + + + + + Status + + setFilterStatus(event.target.value as ActivityStatus | 'all')} + className="rounded-lg border border-outline bg-surface px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary" + > + All Statuses + Pending + Confirmed + Failed + + + + + + Chain + + setFilterChain(event.target.value as HistoryChain)} + className="rounded-lg border border-outline bg-surface px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary" + > + {CHAINS.map((chain) => ( + + {chain === 'all' + ? 'All Chains' + : `${chain.charAt(0).toUpperCase()}${chain.slice(1)}`} + + ))} + + + + + {filteredEntries.length} {filteredEntries.length === 1 ? 'entry' : 'entries'} + + + + + downloadActivityCsv(exportEntries)} + disabled={filteredEntries.length === 0} + className="border border-outline px-4 py-2 font-mono text-xs uppercase tracking-widest text-on-surface transition-colors hover:border-tertiary hover:text-tertiary disabled:cursor-not-allowed disabled:opacity-40" + > + Export CSV + + downloadActivityJson(exportEntries)} + disabled={filteredEntries.length === 0} + className="border border-outline px-4 py-2 font-mono text-xs uppercase tracking-widest text-on-surface transition-colors hover:border-tertiary hover:text-tertiary disabled:cursor-not-allowed disabled:opacity-40" > - - - - - - - - } - title="History coming soon" - description={`Transaction history for ${chain.charAt(0).toUpperCase() + chain.slice(1)} is not yet available. Switch to Stellar to view activity.`} - primaryCTA={{ label: 'Send your first payment', onClick: () => navigate('/send') }} - /> - + Export JSON + + + Exports include chain, status, amount, timestamp, and explorer links only. Private key + and memo material is never included. + + + + + {entries.length === 0 && ( + + + + + + + + + } + title="No activity recorded yet." + description="Send or receive a stealth payment to see your history here." + primaryCTA={{ label: 'Send your first payment', onClick: () => navigate('/send') }} + /> + )} + + {entries.length > 0 && filteredEntries.length === 0 && ( + + )} + + + {filteredEntries.map((entry: ActivityEntry) => ( + + ))} + + ); } diff --git a/src/stores/activityStore.ts b/src/stores/activityStore.ts index 5d972dc..936745c 100644 --- a/src/stores/activityStore.ts +++ b/src/stores/activityStore.ts @@ -30,6 +30,7 @@ interface ActivityState { addEntry: (entry: ActivityEntry) => void; updateStatus: (id: string, status: ActivityStatus) => void; clearHistory: (chain: string, wallet: string) => void; + clearAll: () => void; pollPending: () => Promise; } @@ -52,6 +53,7 @@ export const useActivityStore = create()( set((state) => ({ entries: state.entries.filter((e) => !(e.chain === chain && e.wallet === wallet)), })), + clearAll: () => set({ entries: [] }), pollPending: async () => { const { entries, updateStatus } = get(); const pendingTxs = entries.filter((e) => e.status === 'pending' && e.chain === 'stellar');
+ Exports include chain, status, amount, timestamp, and explorer links only. Private key + and memo material is never included. +