Skip to content

Commit 848dfca

Browse files
author
Senior Engineer
committed
feat(frontend): Freighter auto-reconnect and wallet session UX (#250)
- Auto-discover Freighter on load with bounded retries; respect manual disconnect via sessionStorage - Shorter poll interval in automated tests; skip blocking reconnect UI in test env - Stabilize App wallet handlers with useCallback for effect dependencies - Add i18n strings for wallet checking state (en/es) - Repair VaultDashboard deposit/withdraw form structure; restore cap context wiring - TypeScript and Zod v4 schema fixes for production build; misc test/lint cleanups Closes #250 Made-with: Cursor
1 parent 7eb4c5a commit 848dfca

23 files changed

Lines changed: 515 additions & 424 deletions

frontend/src/App.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { lazy, Suspense, useState } from "react";
1+
import { lazy, Suspense, useCallback, useState } from "react";
22
import { Navigate, Route, Routes } from "react-router-dom";
33
import * as Sentry from "@sentry/react";
44
import Navbar from "./components/Navbar";
@@ -53,13 +53,13 @@ function AppContent() {
5353
const [walletAddress, setWalletAddress] = useState<string | null>(null);
5454
const { data: usdcBalance = 0 } = useUsdcBalance(walletAddress);
5555

56-
const handleConnect = (address: string) => {
56+
const handleConnect = useCallback((address: string) => {
5757
setWalletAddress(address);
58-
};
58+
}, []);
5959

60-
const handleDisconnect = () => {
60+
const handleDisconnect = useCallback(() => {
6161
setWalletAddress(null);
62-
};
62+
}, []);
6363

6464

6565
return (
@@ -89,10 +89,7 @@ function AppContent() {
8989
<Route
9090
path="/portfolio"
9191
element={
92-
<Portfolio
93-
walletAddress={walletAddress}
94-
usdcBalance={usdcBalance}
95-
/>
92+
<Portfolio walletAddress={walletAddress} />
9693
}
9794
/>
9895
<Route

frontend/src/components/Navbar.test.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,12 @@ import { MemoryRouter } from 'react-router-dom';
88
describe('Navbar', () => {
99
const mockOnConnect = vi.fn();
1010
const mockOnDisconnect = vi.fn();
11-
const mockOnNavigate = vi.fn();
12-
1311
it('renders the navbar with navigation links', () => {
1412
render(
1513
<MemoryRouter>
1614
<ToastProvider>
1715
<ThemeProvider>
1816
<Navbar
19-
currentPath="/"
20-
onNavigate={mockOnNavigate}
2117
walletAddress={null}
2218
onConnect={mockOnConnect}
2319
onDisconnect={mockOnDisconnect}
@@ -34,14 +30,12 @@ describe('Navbar', () => {
3430
expect(screen.getByText('Portfolio')).toBeInTheDocument();
3531
});
3632

37-
it('renders the wallet connect button', () => {
33+
it('renders the wallet connect button', async () => {
3834
render(
3935
<MemoryRouter>
4036
<ToastProvider>
4137
<ThemeProvider>
4238
<Navbar
43-
currentPath="/"
44-
onNavigate={mockOnNavigate}
4539
walletAddress={null}
4640
onConnect={mockOnConnect}
4741
onDisconnect={mockOnDisconnect}
@@ -51,7 +45,7 @@ describe('Navbar', () => {
5145
</MemoryRouter>
5246
);
5347

54-
expect(screen.getByText(/Connect Freighter/i)).toBeInTheDocument();
48+
expect(await screen.findByText(/Connect Freighter/i)).toBeInTheDocument();
5549
});
5650

5751
it('shows the truncated wallet address when connected', () => {
@@ -62,8 +56,6 @@ describe('Navbar', () => {
6256
<ToastProvider>
6357
<ThemeProvider>
6458
<Navbar
65-
currentPath="/portfolio"
66-
onNavigate={mockOnNavigate}
6759
walletAddress={fullAddress}
6860
onConnect={mockOnConnect}
6961
onDisconnect={mockOnDisconnect}

frontend/src/components/Navbar.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { Layers } from './icons';
55
import { useTranslation } from '../i18n';
66

77
interface NavbarProps {
8-
currentPath: '/' | '/analytics' | '/portfolio';
9-
onNavigate: (path: '/' | '/analytics' | '/portfolio') => void;
108
walletAddress: string | null;
119
usdcBalance?: number;
1210
onConnect: (address: string) => void;

frontend/src/components/PageHeader.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import React from "react";
22
import { Link } from "react-router-dom";
33
import { ChevronRight } from "./icons";
4-
import Badge, { BadgeColor } from "./Badge";
5-
import { usePageHeadingFocus } from "../hooks/usePageHeadingFocus";
64
import Badge from "./Badge";
75
import type { BadgeColor } from "./Badge";
6+
import { usePageHeadingFocus } from "../hooks/usePageHeadingFocus";
87

98
export interface Breadcrumb {
109
label: string;

frontend/src/components/VaultDashboard.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ describe("VaultDashboard", () => {
182182
await waitFor(() => {
183183
expect(screen.getByRole("alert")).toHaveTextContent("Data unavailable");
184184
}, { timeout: 3000 });
185-
expect(screen.getByRole("alert")).toHaveTextContent("Failed to load vault data");
185+
expect(screen.getByRole("alert")).toHaveTextContent(
186+
"We could not reach the server. Check your connection and try again.",
187+
);
186188
});
187189
});

frontend/src/components/VaultDashboard.tsx

Lines changed: 37 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import React, { useEffect, useState } from "react";
22
import { Activity, ShieldCheck, TrendingUp, Wallet as WalletIcon, AlertTriangle, Info } from "./icons";
3-
import { useState, useEffect } from "react";
4-
import { Activity, ShieldCheck, TrendingUp, Wallet as WalletIcon, Loader2 } from "./icons";
5-
import { hasCustomRpcConfig, networkConfig } from "../config/network";
63
import { useVault } from "../context/VaultContext";
74
import ApiStatusBanner from "./ApiStatusBanner";
85
import VaultPerformanceChart from "./VaultPerformanceChart";
@@ -11,9 +8,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs";
118
import { FormField, SubmitButton } from "../forms";
129
import CopyButton from "./CopyButton";
1310
import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
14-
import TransactionStatus, { type ActionStatus } from "./TransactionStatus";
15-
import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
16-
import CopyButton from "./CopyButton";
1711

1812
interface VaultDashboardProps {
1913
walletAddress: string | null;
@@ -51,23 +45,20 @@ const VaultCapWarning: React.FC<{ utilization: number; isReached: boolean }> = (
5145
);
5246
};
5347

54-
function buildFakeTxHash(walletAddress: string, action: "deposit" | "withdraw", amount: number): string {
55-
const seed = `${walletAddress}-${action}-${amount.toFixed(2)}-${Date.now()}`;
56-
let hash = "";
57-
for (let i = 0; i < 64; i += 1) {
58-
const code = seed.charCodeAt(i % seed.length);
59-
hash += ((code + i * 13) % 16).toString(16);
60-
}
61-
return hash;
62-
}
63-
64-
const STATUS_VISIBLE_MS = 12000;
65-
6648
const VaultDashboard: React.FC<VaultDashboardProps> = ({
6749
walletAddress,
6850
usdcBalance = 0,
6951
}) => {
70-
const { formattedTvl, formattedApy, summary, error, isLoading } = useVault();
52+
const {
53+
formattedTvl,
54+
formattedApy,
55+
summary,
56+
error,
57+
isLoading,
58+
utilization,
59+
isCapWarning,
60+
isCapReached,
61+
} = useVault();
7162
const toast = useToast();
7263
const [activeTab, setActiveTab] = useState<"deposit" | "withdraw">("deposit");
7364
const [amount, setAmount] = useState("");
@@ -87,11 +78,7 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
8778
return () => window.removeEventListener('TRIGGER_DEPOSIT', handleTrigger);
8879
}, []);
8980

90-
const isProcessing = depositMutation.isPending
91-
? "deposit"
92-
: withdrawMutation.isPending
93-
? "withdraw"
94-
: null;
81+
const isBusy = depositMutation.isPending || withdrawMutation.isPending;
9582

9683
const availableBalance = walletAddress ? usdcBalance : 0;
9784
const strategy = summary.strategy;
@@ -302,67 +289,38 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
302289
onClick={() => setAmount(availableBalance.toFixed(2))}
303290
disabled={!walletAddress || availableBalance <= 0 || isBusy || (tab === "deposit" && isCapReached)}
304291
>
305-
<div style={{ marginBottom: "24px" }}>
306-
<div className="flex justify-between items-center" style={{ marginBottom: "16px" }}>
307-
<div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>
308-
{tab === "deposit" ? "Amount to deposit" : "Amount to withdraw"}
309-
</div>
310-
<div style={{ color: "var(--text-secondary)", fontSize: "0.85rem" }}>
311-
Balance: <span style={{ color: "var(--text-primary)", fontWeight: 600 }}>{availableBalance.toFixed(2)}</span>
312-
</div>
313-
</div>
314-
315-
<div className="input-group">
316-
<div className="input-wrapper">
317-
<span style={{ color: "var(--text-secondary)", paddingRight: "12px", borderRight: "1px solid var(--border-glass)", marginRight: "16px" }}>USDC</span>
318-
<input className="input-field" type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} disabled={isProcessing !== null} />
319-
<button className="btn-max" onClick={() => setAmount(availableBalance.toFixed(2))} disabled={!walletAddress || availableBalance <= 0 || isProcessing !== null}>
320292
MAX
321293
</button>
322294
</div>
323-
</div>
324295

325-
<SubmitButton
326-
loading={isBusy && activeTab === tab}
327-
disabled={!walletAddress || isBusy || !amount || Number(amount) <= 0 || (tab === "deposit" && isCapReached)}
328-
label={tab === "deposit" ? (isCapReached ? "Vault is full" : "Approve & Deposit") : "Withdraw Funds"}
329-
loadingLabel="Waiting for confirmation..."
330-
/>
331-
</form>
332-
</div>
296+
<div className="glass-panel" style={{ padding: "14px 16px", background: "rgba(0, 0, 0, 0.15)", marginBottom: "16px" }}>
297+
<div className="flex justify-between items-center" style={{ marginBottom: "6px" }}>
298+
<span style={{ color: "var(--text-secondary)", fontSize: "0.86rem" }}>Estimated protocol fee</span>
299+
<span style={{ fontSize: "0.9rem", fontWeight: 600 }}>
300+
{isValidAmount ? `${estimatedFee.toFixed(4)} USDC` : "0.0000 USDC"}
301+
</span>
302+
</div>
303+
<div className="flex justify-between items-center">
304+
<span style={{ color: "var(--text-secondary)", fontSize: "0.82rem" }}>
305+
{tab === "deposit" ? "Estimated net deposit" : "Estimated net withdrawal"}
306+
</span>
307+
<span style={{ fontSize: "0.9rem", fontWeight: 600 }}>
308+
{isValidAmount ? `${estimatedNetAmount.toFixed(4)} USDC` : "0.0000 USDC"}
309+
</span>
310+
</div>
311+
<div style={{ marginTop: "6px", color: "var(--text-secondary)", fontSize: "0.75rem" }}>
312+
Network fee: {summary.networkFeeEstimate}
313+
</div>
314+
</div>
333315

334-
<div className="glass-panel" style={{ padding: "14px 16px", background: "rgba(0, 0, 0, 0.15)", marginBottom: "16px" }}>
335-
<div className="flex justify-between items-center" style={{ marginBottom: "6px" }}>
336-
<span style={{ color: "var(--text-secondary)", fontSize: "0.86rem" }}>Estimated protocol fee</span>
337-
<span style={{ fontSize: "0.9rem", fontWeight: 600 }}>
338-
{isValidAmount ? `${estimatedFee.toFixed(4)} USDC` : "0.0000 USDC"}
339-
</span>
340-
</div>
341-
<div className="flex justify-between items-center">
342-
<span style={{ color: "var(--text-secondary)", fontSize: "0.82rem" }}>
343-
{tab === "deposit" ? "Estimated net deposit" : "Estimated net withdrawal"}
344-
</span>
345-
<span style={{ fontSize: "0.9rem", fontWeight: 600 }}>
346-
{isValidAmount ? `${estimatedNetAmount.toFixed(4)} USDC` : "0.0000 USDC"}
347-
</span>
348-
</div>
349-
<div style={{ marginTop: "6px", color: "var(--text-secondary)", fontSize: "0.75rem" }}>
350-
Network fee: {summary.networkFeeEstimate}
316+
<SubmitButton
317+
loading={isBusy && activeTab === tab}
318+
disabled={!walletAddress || isBusy || !amount || Number(amount) <= 0 || (tab === "deposit" && isCapReached)}
319+
label={tab === "deposit" ? (isCapReached ? "Vault is full" : "Approve & Deposit") : "Withdraw Funds"}
320+
loadingLabel="Processing Transaction..."
321+
/>
351322
</div>
352-
</div>
353-
354-
<button className="btn btn-primary" style={{ width: "100%", padding: "16px" }} onClick={() => handleTransaction(tab)} disabled={isProcessing !== null || !amount || Number(amount) <= 0}>
355-
{isProcessing === tab ? "Processing Transaction..." : tab === "deposit" ? "Approve & Deposit" : "Withdraw Funds"}
356-
<button className="btn btn-primary" style={{ width: "100%", padding: "16px", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px" }} onClick={() => handleTransaction(tab)} disabled={isProcessing !== null || !amount || Number(amount) <= 0}>
357-
{isProcessing === tab ? (
358-
<>
359-
<Loader2 size={16} className="spin" style={{ animation: "spin 0.9s linear infinite" }} />
360-
Processing Transaction...
361-
</>
362-
) : (
363-
tab === "deposit" ? "Approve & Deposit" : "Withdraw Funds"
364-
)}
365-
</button>
323+
</form>
366324
</TabsContent>
367325
))}
368326
</Tabs>

frontend/src/components/VaultPerformanceChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { TrendingUp } from "./icons";
33
import { useVaultHistory } from "../hooks/useVaultData";
44

55
const VaultPerformanceChart: React.FC = () => {
6-
const { data = [] } = useVaultHistory();
6+
useVaultHistory();
77

88
return (
99
<div style={{ width: "100%" }}>

0 commit comments

Comments
 (0)