Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions e2e/history.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { test, expect } from '@playwright/test';

test.describe('Activity History', () => {
test.beforeEach(async ({ page }) => {
// Navigate to history page
await page.goto('/history');

// Simulate connecting a wallet and populating localStorage with mock data
await page.evaluate(() => {
// Mock wallet connection state if necessary
window.localStorage.setItem('wraith-wallet', JSON.stringify({ address: 'GDTESTWALLET123' }));

// Mock history store
window.localStorage.setItem('wraith-activity-storage', JSON.stringify({
state: {
entries: [
{
id: 'tx1',
chain: 'stellar',
wallet: 'GDTESTWALLET123',
kind: 'stealth-send',
direction: 'out',
status: 'confirmed',
amount: '10',
timestamp: Date.now() - 1000,
},
{
id: 'tx2',
chain: 'stellar',
wallet: 'GDTESTWALLET123',
kind: 'withdrawal',
direction: 'out',
status: 'pending',
amount: '5',
timestamp: Date.now() - 2000,
},
{
id: 'tx3',
chain: 'stellar',
wallet: 'GDTESTWALLET123',
kind: 'stealth-receive',
direction: 'in',
status: 'confirmed',
timestamp: Date.now() - 3000,
}
]
},
version: 0
}));
});

// Reload to apply localStorage
await page.reload();
});

test('displays history entries and filters correctly', async ({ page }) => {
// Wait for the history page to load
await expect(page.getByText('Activity History')).toBeVisible();

// Check if all 3 items are shown initially
await expect(page.getByText('stealth send')).toBeVisible();
await expect(page.getByText('withdrawal')).toBeVisible();
await expect(page.getByText('stealth receive')).toBeVisible();

// Filter by type: withdrawal
await page.locator('select').first().selectOption('withdrawal');
await expect(page.getByText('withdrawal')).toBeVisible();
await expect(page.getByText('stealth send')).not.toBeVisible();

// Filter by status: pending
await page.locator('select').nth(1).selectOption('pending');
await expect(page.getByText('withdrawal')).toBeVisible();

// Clear history
await page.getByRole('button', { name: 'Clear History' }).click();
await expect(page.getByText('No activity recorded yet.')).toBeVisible();
});
});
183 changes: 183 additions & 0 deletions src/components/StellarHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { useState, useEffect, useMemo } from 'react';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { stellarTxUrl } from '@/lib/explorer';
import { useActivityStore, ActivityKind, ActivityStatus } from '@/stores/activityStore';

export function StellarHistory() {
const { address, isConnected } = useStellarWallet();
const { entries, clearHistory, pollPending } = useActivityStore();

const [filterKind, setFilterKind] = useState<ActivityKind | 'all'>('all');
const [filterStatus, setFilterStatus] = useState<ActivityStatus | 'all'>('all');

// Poll for pending transactions on mount and every 10 seconds
useEffect(() => {
if (!address) return;
pollPending();
const interval = setInterval(pollPending, 10000);
return () => clearInterval(interval);
}, [address, pollPending]);

const walletEntries = useMemo(() => {
if (!address) return [];
return entries.filter((e) => e.wallet === address && e.chain === 'stellar');
}, [entries, address]);

const filteredEntries = useMemo(() => {
return walletEntries.filter((e) => {
if (filterKind !== 'all' && e.kind !== filterKind) return false;
if (filterStatus !== 'all' && e.status !== filterStatus) return false;
return true;
}).sort((a, b) => b.timestamp - a.timestamp);
}, [walletEntries, filterKind, filterStatus]);

if (!isConnected) {
return (
<section className="flex flex-col gap-3">
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
History
</h1>
<p className="font-body text-sm leading-relaxed text-on-surface-variant">
Connect your Stellar wallet to view your transaction history.
</p>
</section>
);
}

return (
<section className="flex flex-col gap-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Stellar Testnet / XLM
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Activity History
</h1>
</div>
<button
onClick={() => clearHistory('stellar', address)}
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"
>
Clear History
</button>
</div>

<div className="flex flex-wrap gap-4">
<div className="flex flex-col gap-1">
<label className="font-mono text-[10px] uppercase tracking-widest text-outline">Type</label>
<select
value={filterKind}
onChange={(e) => setFilterKind(e.target.value as any)}
className="rounded-lg border border-outline bg-surface-container px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary"
>
<option value="all">All Types</option>
<option value="stealth-send">Stealth Send</option>
<option value="stealth-receive">Stealth Receive</option>
<option value="withdrawal">Withdrawal</option>
<option value="name-registration">Name Registration</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="font-mono text-[10px] uppercase tracking-widest text-outline">Status</label>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as any)}
className="rounded-lg border border-outline bg-surface-container px-3 py-2 font-mono text-sm text-on-surface outline-none focus:border-tertiary"
>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="failed">Failed</option>
</select>
</div>
</div>

{walletEntries.length === 0 && (
<p className="font-body text-sm text-on-surface-variant">No activity recorded yet.</p>
)}

{walletEntries.length > 0 && filteredEntries.length === 0 && (
<p className="font-body text-sm text-on-surface-variant">No activity matches the filters.</p>
)}

<div className="flex flex-col gap-4">
{filteredEntries.map((tx) => (
<div
key={tx.id}
className="group flex flex-col gap-3 rounded-xl border border-outline-variant bg-surface-container p-4 transition-colors hover:border-outline"
>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<span
className={`h-2 w-2 rounded-full ${
tx.status === 'confirmed'
? 'bg-secondary'
: tx.status === 'pending'
? 'bg-tertiary animate-pulse'
: 'bg-error'
}`}
/>
<span className="font-mono text-[10px] uppercase tracking-widest text-on-surface">
{tx.status} • {tx.kind.replace('-', ' ')}
</span>
</div>
<span className="font-mono text-[10px] text-outline">
{new Date(tx.timestamp).toLocaleString()}
</span>
</div>

<div className="flex flex-col gap-1">
<div className="flex items-center justify-between gap-4">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Direction
</span>
<span className="font-mono text-[10px] text-on-surface">
{tx.direction === 'in' ? 'Incoming (Received)' : 'Outgoing (Sent)'}
</span>
</div>

{tx.amount && (
<div className="flex items-center justify-between gap-4">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Amount
</span>
<span className="font-mono text-[10px] text-on-surface">
{tx.amount} XLM
</span>
</div>
)}

{tx.recipient && (
<div className="flex items-center justify-between gap-4">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Recipient / Address
</span>
<span className="max-w-[200px] truncate font-mono text-[10px] text-on-surface" title={tx.recipient}>
{tx.recipient.length > 30 ? `${tx.recipient.slice(0, 12)}...${tx.recipient.slice(-12)}` : tx.recipient}
</span>
</div>
)}

{tx.kind !== 'stealth-receive' && (
<div className="flex items-center justify-between gap-4">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Hash
</span>
<a
href={stellarTxUrl(tx.id)}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-[10px] text-tertiary hover:underline"
>
{tx.id.slice(0, 12)}...{tx.id.slice(-12)}
</a>
</div>
)}
</div>
</div>
))}
</div>
</section>
);
}
Loading