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
4 changes: 2 additions & 2 deletions contracts/vault/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
//! | `shipment_ids_by_status` | Public | Query shipments by status |
//! | `calculate_shares` | Public | Calculate shares for amount |
//! | `calculate_assets` | Public | Calculate assets for shares |
| `upgrade` | Admin | Upgrade contract WASM code (safety: must be paused) |
| `version` | Public | Query current contract version |
//! | `upgrade` | Admin | Upgrade contract WASM code (safety: must be paused) |
//! | `version` | Public | Query current contract version |

use soroban_sdk::Address;

Expand Down
37 changes: 8 additions & 29 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import ShortcutHelpModal from "./components/ShortcutHelpModal";
import { FeatureGate } from "./components/FeatureGate";
import { FeatureFlagProvider } from "./context/FeatureFlagContext";
import { AuthProvider, useAuth } from "./context/AuthContext";
import { useTranslation } from "./i18n";
import { useUsdcBalance } from "./hooks/useBalanceData";
import { queryClient } from "./lib/queryClient";
import { clearWalletSessionState } from "./lib/sessionCleanup";
import ErrorFallback from "./components/ErrorFallback";
import RouteLoadingFallback from "./components/RouteLoadingFallback";

const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);

Expand All @@ -23,32 +23,6 @@ const Analytics = lazy(() => import("./pages/Analytics"));
const UIPreview = lazy(() => import("./pages/UIPreview"));
const TransactionHistory = lazy(() => import("./pages/TransactionHistory"));
const Settings = lazy(() => import("./pages/Settings"));
const LoadingPage = () => {
const { t } = useTranslation();
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "60vh",
color: "var(--accent-cyan)",
fontSize: "1.2rem",
fontWeight: 500,
}}
>
<div style={{ textAlign: "center" }}>
<div
className="text-gradient"
style={{ fontSize: "2rem", marginBottom: "16px" }}
>
{t("app.loading.title")}
</div>
<div style={{ opacity: 0.6 }}>{t("app.loading.subtitle")}</div>
</div>
</div>
);
};

// Removed simple fallback in favor of components/ErrorFallback

Expand Down Expand Up @@ -94,7 +68,7 @@ function AppContent() {
onDisconnect={handleDisconnect}
/>
<main id="main-content" className="container app-main" style={{ marginTop: "100px", paddingBottom: "60px" }}>
<Suspense fallback={<LoadingPage />}>
<Suspense fallback={<RouteLoadingFallback />}>
<SentryRoutes>
<Route
path="/"
Expand Down Expand Up @@ -144,7 +118,12 @@ function AppContent() {
function App() {
return (
<Sentry.ErrorBoundary
fallback={(props) => <ErrorFallback {...props} />}
fallback={(props) => (
<ErrorFallback
error={(props.error instanceof Error ? props.error : new Error(String(props.error)))}
resetError={props.resetError}
/>
)}
showDialog
>
<AuthProvider>
Expand Down
25 changes: 9 additions & 16 deletions frontend/src/components/ErrorFallback.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import ErrorFallback from './ErrorFallback';
import * as ErrorNavigation from './errorNavigation';

describe('ErrorFallback', () => {
const mockError = new Error('Test error message');
Expand All @@ -14,34 +15,26 @@ describe('ErrorFallback', () => {
});

it('calls reload when reload button is clicked', () => {
const originalLocation = window.location;
// @ts-ignore
delete window.location;
window.location = { ...originalLocation, reload: vi.fn() };
const reloadSpy = vi.spyOn(ErrorNavigation, 'reloadPage').mockImplementation(() => undefined);

render(<ErrorFallback error={mockError} resetError={mockResetError} />);
render(<ErrorFallback error={mockError} resetError={mockResetError} onReload={reloadSpy} />);

const reloadButton = screen.getByText('Reload Page');
fireEvent.click(reloadButton);

expect(window.location.reload).toHaveBeenCalled();

window.location = originalLocation;
expect(reloadSpy).toHaveBeenCalled();
reloadSpy.mockRestore();
});

it('navigates to home when Go Home button is clicked', () => {
const originalLocation = window.location;
// @ts-ignore
delete window.location;
window.location = { ...originalLocation, href: '' };
const assignSpy = vi.spyOn(ErrorNavigation, 'goHome').mockImplementation(() => undefined);

render(<ErrorFallback error={mockError} resetError={mockResetError} />);
render(<ErrorFallback error={mockError} resetError={mockResetError} onGoHome={assignSpy} />);

const homeButton = screen.getByText('Go Home');
fireEvent.click(homeButton);

expect(window.location.href).toBe('/');

window.location = originalLocation;
expect(assignSpy).toHaveBeenCalled();
assignSpy.mockRestore();
});
});
13 changes: 10 additions & 3 deletions frontend/src/components/ErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React from "react";
import { RefreshCw, Home, AlertOctagon } from "lucide-react";
import { goHome, reloadPage } from "./errorNavigation";

interface ErrorFallbackProps {
error: Error;
resetError: () => void;
onReload?: () => void;
onGoHome?: () => void;
}

const ErrorFallback: React.FC<ErrorFallbackProps> = ({ error }) => {
const ErrorFallback: React.FC<ErrorFallbackProps> = ({
error,
onReload = reloadPage,
onGoHome = goHome,
}) => {
return (
<div
style={{
Expand Down Expand Up @@ -95,7 +102,7 @@ const ErrorFallback: React.FC<ErrorFallbackProps> = ({ error }) => {
>
<button
className="btn btn-primary"
onClick={() => window.location.reload()}
onClick={onReload}
style={{ width: "100%", padding: "14px" }}
>
<RefreshCw size={18} />
Expand All @@ -104,7 +111,7 @@ const ErrorFallback: React.FC<ErrorFallbackProps> = ({ error }) => {

<button
className="btn btn-outline"
onClick={() => (window.location.href = "/")}
onClick={onGoHome}
style={{ width: "100%", padding: "14px" }}
>
<Home size={18} />
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/RouteLoadingFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Skeleton from "./Skeleton";
import { useTranslation } from "../i18n";

export default function RouteLoadingFallback() {
const { t } = useTranslation();

return (
<section
aria-live="polite"
aria-busy="true"
style={{ display: "grid", gap: "20px", padding: "8px 0" }}
>
<header style={{ display: "grid", gap: "12px" }}>
<Skeleton height={32} width="260px" borderRadius={8} />
<Skeleton height={16} width="380px" borderRadius={8} />
</header>

<Skeleton height={120} borderRadius={12} />
<Skeleton height={260} borderRadius={12} />

<p style={{ color: "var(--text-muted)", margin: 0 }}>{t("app.loading.subtitle")}</p>
</section>
);
}
4 changes: 2 additions & 2 deletions frontend/src/components/VaultDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useEffect, useState } from "react";
import { Activity, AlertCircle, ShieldCheck, TrendingUp, Wallet as WalletIcon, Loader2, AlertTriangle, Info } from "./icons";
import { hasCustomRpcConfig, networkConfig } from "../config/network";
import { Activity, AlertCircle, ShieldCheck, TrendingUp, Wallet as WalletIcon, Loader2, Info } from "./icons";
import Skeleton from "./Skeleton";
import { useVault } from "../context/VaultContext";
import ApiStatusBanner from "./ApiStatusBanner";
import VaultPerformanceChart from "./VaultPerformanceChart";
import { useToast } from "../context/ToastContext";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs";
import { FormField } from "../forms";
import WithdrawalConfirmationModal from "./WithdrawalConfirmationModal";
import { FormField, SubmitButton } from "../forms";
import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/WalletConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ interface WalletConnectProps {
walletAddress: string | null;
usdcBalance?: number;
onConnect: (address: string) => void;
onDisconnect: () => void;
onDisconnect: (reason?: DisconnectReason) => void;
}

export type DisconnectReason = "manual" | "session-expired" | "connection-lost";

type ConnectionErrorType = "not-installed" | "not-allowed" | "no-address" | "generic" | null;

const WalletConnect: React.FC<WalletConnectProps> = ({
Expand Down Expand Up @@ -137,7 +139,7 @@ const WalletConnect: React.FC<WalletConnectProps> = ({
clearWalletManualDisconnect();
onConnect(discovered);
} else if (walletAddress) {
onDisconnect();
onDisconnect("connection-lost");
toast.info({
title: "Wallet disconnected",
description: "Freighter is no longer connected to this session.",
Expand Down Expand Up @@ -332,7 +334,7 @@ const WalletConnect: React.FC<WalletConnectProps> = ({
onClick={() => {
setConnectionError(null);
setWalletManualDisconnect();
onDisconnect();
onDisconnect("manual");
toast.info({
title: t("toast.walletDisconnected.title"),
description: t("toast.walletDisconnected.description"),
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/errorNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const reloadPage = () => window.location.reload();

export const goHome = () => window.location.assign("/");
3 changes: 0 additions & 3 deletions frontend/src/components/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export {
AlertTriangle,
ChevronRight,
AlertCircle,
AlertTriangle,
Check,
Copy,
Info,
Expand All @@ -18,8 +17,6 @@ export {
TrendingUp,
Wallet,
X,
AlertTriangle,
Info,
DollarSign,
Percent,
Briefcase,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/context/VaultContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ApiError } from "../lib/api";
import type { VaultSummary } from "../lib/vaultApi";
import { networkConfig } from "../config/network";
import { useVaultSummary, useVaultHistory } from "../hooks/useVaultData";
import { formatCurrency, formatPercent } from "../lib/formatters";
import { formatCurrency } from "../lib/formatters";

interface VaultContextType {
summary: VaultSummary;
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/lib/stellarAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ export async function discoverConnectedAddress(): Promise<string | null> {
}
}

export async function discoverConnectedAddressWithRetry(
retries = 5,
delayMs = 250,
): Promise<string | null> {
for (let index = 0; index < retries; index += 1) {
const address = await discoverConnectedAddress();
if (address) {
return address;
}

if (index < retries - 1) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
}

return null;
}

export async function fetchUsdcBalance(
walletAddress: string,
rpcUrl = import.meta.env.VITE_SOROBAN_RPC_URL || `https://${TESTNET_SOROBAN_RPC}`,
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/pages/Portfolio.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useMemo, useState, useEffect } from "react";
import { Activity, ShieldCheck, TrendingUp, DollarSign, Percent, Briefcase } from "../components/icons";
import { Activity, TrendingUp, DollarSign, Percent, Briefcase } from "../components/icons";
import ApiStatusBanner from "../components/ApiStatusBanner";
import Skeleton from "../components/Skeleton";
import {
DataTable,
type DataTableColumn,
Expand Down Expand Up @@ -133,7 +132,7 @@ const PortfolioSummaryCard: React.FC<{
onMouseLeave={(e) => e.currentTarget.style.transform = "translateY(0)"}
>
<div style={{ position: "absolute", top: "-10px", right: "-10px", opacity: 0.05 }}>
{React.cloneElement(icon as React.ReactElement, { size: 80 })}
{React.cloneElement(icon as React.ReactElement<Record<string, unknown>>, { size: 80 })}
</div>
<div className="flex items-center gap-sm" style={{ color: "var(--text-secondary)", marginBottom: "12px" }}>
{icon}
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/tests/routing.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
import App from '../App';

// Mock the modules that are lazy loaded to test the loading state
Expand All @@ -22,8 +23,8 @@ vi.mock('../components/ShortcutHelpModal', () => ({

// Mock Sentry
vi.mock('@sentry/react', () => ({
withSentryReactRouterV6Routing: (comp: any) => comp,
ErrorBoundary: ({ children }: any) => <>{children}</>,
withSentryReactRouterV6Routing: <T,>(comp: T) => comp,
ErrorBoundary: ({ children }: { children: ReactNode }) => <>{children}</>,
init: vi.fn(),
}));

Expand All @@ -47,9 +48,8 @@ describe('Routing and Lazy Loading', () => {
</MemoryRouter>
);

// Check if loading text appears
// The LoadingPage component uses {t("app.loading.title")}
expect(screen.getByText('app.loading.title')).toBeDefined();
// Shared route fallback renders while lazy chunks load
expect(screen.getByText('app.loading.subtitle')).toBeDefined();

// Wait for lazy component to load
await waitFor(() => {
Expand All @@ -64,7 +64,7 @@ describe('Routing and Lazy Loading', () => {
</MemoryRouter>
);

expect(screen.getByText('app.loading.title')).toBeDefined();
expect(screen.getByText('app.loading.subtitle')).toBeDefined();

await waitFor(() => {
expect(screen.getByTestId('portfolio-page')).toBeDefined();
Expand Down
Loading