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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]

### Features
- Add empty-state deposit and withdraw intent actions across dashboard pages (#734)
- CORS configuration for cross-origin API access
- Add canonical `VaultError` namespace module and replace contract panics with stable error codes (#754)
- Structured logging, graceful shutdown, caching, and API key authentication
- Network badge showing testnet vs mainnet status in the frontend
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/VaultDashboard.emptystate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,12 @@ describe("VaultDashboard — empty state", () => {
renderDashboard("GABC123", 0);

await waitFor(() => {
expect(screen.getByText("No deposits yet.")).toBeInTheDocument();
expect(screen.getByText(/No deposits yet/i)).toBeInTheDocument();
});

expect(
screen.getByText(
/Start earning yield by depositing USDC into our high-efficiency vaults\./i,
/Start earning yield by depositing USDC into our high-efficiency vaults/i,
),
).toBeInTheDocument();
});
Expand All @@ -156,9 +156,11 @@ describe("VaultDashboard — empty state", () => {
const cta = await screen.findByRole("button", { name: "Deposit Now" });
fireEvent.click(cta);

expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_DEPOSIT" }),
);
await waitFor(() => {
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_DEPOSIT" }),
);
});
});

it("does NOT show the empty state when balance is non-zero", async () => {
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/components/VaultDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import { useFeeEstimate } from "../hooks/useFeeEstimate";
import { useSlippage } from "../hooks/useSlippage";
import HelpIcon from "./ui/HelpIcon";
import EmptyState from "./ui/EmptyState";
import { useTranslation } from "../i18n";
import { useNavigate } from "react-router-dom";
import { triggerDepositIntent } from "../lib/vaultIntentActions";
import { networkConfig } from "../config/network";
import { useDashboardUrlState, type TransactionTab, type TransactionStep } from "../hooks/useDashboardUrlState";
import RefreshControl from "./RefreshControl";
Expand Down Expand Up @@ -170,6 +173,8 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
lastUpdate,
refresh,
} = useVault();
const { t } = useTranslation();
const navigate = useNavigate();
const toast = useToast();
const delayedLoading = useDelayedLoading(isLoading);

Expand Down Expand Up @@ -843,13 +848,11 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
{!isLoading && walletAddress && usdcBalance === 0 && (
<EmptyState
kind="no-data"
title="No deposits yet."
description="Start earning yield by depositing USDC into our high-efficiency vaults."
title={t("dashboard.emptyState.noDeposits.title")}
description={t("dashboard.emptyState.noDeposits.desc")}
icon={<TrendingUp />}
actionLabel="Deposit Now"
onAction={() => {
window.dispatchEvent(new Event("TRIGGER_DEPOSIT"));
}}
actionLabel={t("emptyState.depositNow")}
onAction={() => triggerDepositIntent(navigate, walletAddress)}
/>
)}
</div>
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,18 @@ export const en = {
failed: "Failed",
},
},
dashboard: {
emptyState: {
noDeposits: {
title: "No deposits yet.",
desc: "Start earning yield by depositing USDC into our high-efficiency vaults.",
},
},
},
emptyState: {
depositNow: "Deposit Now",
withdrawNow: "Withdraw Now",
},
txHistory: {
pageTitle: "Transaction History",
pageDesc: "View all your past deposits and withdrawals.",
Expand All @@ -437,6 +449,15 @@ export const en = {
},
resetFilters: "Reset filters",
depositNow: "Deposit Now",
noWithdrawals: {
title: "No withdrawals yet",
desc: "Your deposits are active — withdraw anytime from the vault.",
},
connectWallet: {
title: "Connect your wallet",
desc: "Connect your wallet to view your transaction history.",
action: "Connect wallet",
},
loadingLabel: "Loading...",
upToDateLabel: "Up to date",
paginatedView: "Paginated view",
Expand Down Expand Up @@ -524,5 +545,6 @@ export const en = {
homeLabel: "Home",
emptyTitle: "No analytics data yet",
emptyDesc: "Vault analytics will appear once the first deposit is made.",
depositNow: "Deposit Now",
},
} as const;
22 changes: 22 additions & 0 deletions frontend/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,18 @@ export const es = {
failed: "Fallido",
},
},
dashboard: {
emptyState: {
noDeposits: {
title: "Aún no hay depósitos.",
desc: "Comienza a generar rendimiento depositando USDC en nuestros vaults de alta eficiencia.",
},
},
},
emptyState: {
depositNow: "Depositar ahora",
withdrawNow: "Retirar ahora",
},
txHistory: {
pageTitle: "Historial de transacciones",
pageDesc: "Consulta todos tus depósitos y retiros pasados.",
Expand All @@ -423,6 +435,15 @@ export const es = {
},
resetFilters: "Restablecer filtros",
depositNow: "Depositar ahora",
noWithdrawals: {
title: "Aún no hay retiros",
desc: "Tus depósitos están activos — retira en cualquier momento desde el vault.",
},
connectWallet: {
title: "Conecta tu billetera",
desc: "Conecta tu billetera para ver tu historial de transacciones.",
action: "Conectar billetera",
},
loadingLabel: "Cargando...",
upToDateLabel: "Al día",
paginatedView: "Vista paginada",
Expand Down Expand Up @@ -510,5 +531,6 @@ export const es = {
homeLabel: "Inicio",
emptyTitle: "Aún no hay datos de analítica",
emptyDesc: "La analítica de la bóveda aparecerá una vez que se realice el primer depósito.",
depositNow: "Depositar ahora",
},
} as const;
55 changes: 55 additions & 0 deletions frontend/src/lib/vaultIntentActions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
triggerDepositIntent,
triggerWithdrawIntent,
triggerWalletConnectIntent,
} from "./vaultIntentActions";

describe("vaultIntentActions", () => {
const navigate = vi.fn();

beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(window, "dispatchEvent");
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

it("dispatches wallet connect when deposit intent has no wallet", () => {
triggerDepositIntent(navigate, null);
expect(navigate).not.toHaveBeenCalled();
expect(window.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_WALLET_CONNECT" }),
);
});

it("navigates home and dispatches deposit intent when wallet is connected", () => {
triggerDepositIntent(navigate, "GABC123");
expect(navigate).toHaveBeenCalledWith("/");
expect(window.dispatchEvent).not.toHaveBeenCalled();

vi.advanceTimersByTime(100);
expect(window.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_DEPOSIT" }),
);
});

it("navigates home and dispatches withdraw intent when wallet is connected", () => {
triggerWithdrawIntent(navigate, "GABC123");
expect(navigate).toHaveBeenCalledWith("/");
vi.advanceTimersByTime(100);
expect(window.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_WITHDRAW" }),
);
});

it("dispatches wallet connect for withdraw intent without wallet", () => {
triggerWalletConnectIntent();
expect(window.dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_WALLET_CONNECT" }),
);
});
});
37 changes: 37 additions & 0 deletions frontend/src/lib/vaultIntentActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { NavigateFunction } from "react-router-dom";

const INTENT_DELAY_MS = 100;

export function triggerWalletConnectIntent(): void {
window.dispatchEvent(new CustomEvent("TRIGGER_WALLET_CONNECT"));
}

export function triggerDepositIntent(
navigate: NavigateFunction,
walletAddress: string | null,
): void {
if (!walletAddress) {
triggerWalletConnectIntent();
return;
}
navigate("/");
window.setTimeout(
() => window.dispatchEvent(new CustomEvent("TRIGGER_DEPOSIT")),
INTENT_DELAY_MS,
);
}

export function triggerWithdrawIntent(
navigate: NavigateFunction,
walletAddress: string | null,
): void {
if (!walletAddress) {
triggerWalletConnectIntent();
return;
}
navigate("/");
window.setTimeout(
() => window.dispatchEvent(new CustomEvent("TRIGGER_WITHDRAW")),
INTENT_DELAY_MS,
);
}
5 changes: 3 additions & 2 deletions frontend/src/pages/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Skeleton from "../components/Skeleton";
import EmptyState from "../components/ui/EmptyState";
import APYTrendChart from "../components/APYTrendChart";
import { useNavigate } from "react-router-dom";
import { triggerDepositIntent } from "../lib/vaultIntentActions";
import RefreshControl from "../components/RefreshControl";
import { usePolling } from "../hooks/usePolling";
import { useStaleIndicator } from "../hooks/useStaleIndicator";
Expand Down Expand Up @@ -118,8 +119,8 @@ const Analytics: React.FC = () => {
title={t("analytics.emptyTitle")}
description={t("analytics.emptyDesc")}
icon={<LineChart />}
actionLabel={t("txHistory.depositNow")}
onAction={() => navigate("/")}
actionLabel={t("analytics.depositNow")}
onAction={() => triggerDepositIntent(navigate, null)}
/>
)}
</div>
Expand Down
18 changes: 17 additions & 1 deletion frontend/src/pages/Portfolio.emptystate.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
Expand Down Expand Up @@ -95,6 +95,22 @@ describe("Portfolio — empty state", () => {
});
});

it("Deposit Now CTA dispatches TRIGGER_DEPOSIT and navigates home", async () => {
const dispatchSpy = vi.spyOn(window, "dispatchEvent");
vi.mocked(portfolioApi.getPortfolioHoldings).mockResolvedValue([]);

renderPortfolio("GABC123");

const cta = await screen.findByRole("button", { name: "Deposit Now" });
fireEvent.click(cta);

await waitFor(() => {
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({ type: "TRIGGER_DEPOSIT" }),
);
});
});

it("does NOT show the empty state when holdings have value", async () => {
vi.mocked(portfolioApi.getPortfolioHoldings).mockResolvedValue([mockHolding]);

Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import EmptyState from "../components/ui/EmptyState";
import FirstTimePortfolioPanel from "../components/FirstTimePortfolioPanel";
import { useNavigate } from "react-router-dom";
import { triggerDepositIntent } from "../lib/vaultIntentActions";
import { formatCurrency, formatNumber, formatPercent } from "../lib/formatters";
import { displayBalance } from "../lib/maskSensitiveValues";

Expand Down Expand Up @@ -314,7 +315,7 @@
</span>
),
},
], [formatSensitiveCurrency, t]);

Check warning on line 318 in frontend/src/pages/Portfolio.tsx

View workflow job for this annotation

GitHub Actions / Frontend lint + test

React Hook useMemo has a missing dependency: 'locale'. Either include it or remove the dependency array

// Compute trend values
const totalNetValueTrend = useMemo(() => {
Expand Down Expand Up @@ -485,7 +486,7 @@
description={t("portfolio.noPositions.desc")}
icon={<Briefcase />}
actionLabel={t("portfolio.depositNow")}
onAction={() => navigate("/")}
onAction={() => triggerDepositIntent(navigate, walletAddress)}
/>
) : (
<section
Expand Down
Loading
Loading