diff --git a/src/App.tsx b/src/App.tsx
index ddef64a..0de907c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useState, type ComponentType, type CSSProperties } from 'react'
+import { Routes, Route, useNavigate, useLocation } from 'react-router-dom'
import { I18nProvider } from './components/I18nProvider'
import './i18n/index.js'
import './styles/responsive.css'
@@ -175,6 +176,7 @@ function NotificationBell({ onClick }: { onClick: () => void }) {
}
function DashboardLayout() {
+ const navigate = useNavigate()
const {
connectedAddress,
activeTab,
@@ -292,18 +294,14 @@ function DashboardLayout() {
const handleSearchResult = (result: SearchResult | null | undefined): void => {
if (!result) return
if (result.type === 'transaction' || result.type === 'operation') {
- setActiveTab('transactions')
+ navigate('/transactions')
return
}
if (result.type === 'account') {
- setActiveTab('account')
+ navigate('/account')
return
}
- if (result.type === 'contract') {
- setActiveTab('contracts')
- return
- }
- setActiveTab('overview')
+ navigate('/overview')
}
return (
@@ -383,10 +381,39 @@ function DashboardLayout() {
)
}
+function RouterSync() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const { connectedAddress, activeTab, setActiveTab } = useStore()
+
+ const pathTab = location.pathname === '/' ? 'overview' : location.pathname.slice(1)
+
+ useEffect(() => {
+ if (pathTab === 'connect') return
+ if (TABS[pathTab] && pathTab !== activeTab) {
+ setActiveTab(pathTab)
+ }
+ }, [location.pathname])
+
+ useEffect(() => {
+ if (!connectedAddress && pathTab !== 'connect') {
+ navigate('/connect', { replace: true })
+ } else if (connectedAddress && pathTab === 'connect') {
+ navigate(`/${activeTab}`, { replace: true })
+ }
+ }, [connectedAddress, location.pathname])
+
+ return null
+}
+
export default function App() {
return (
-
+
+
+ } />
+ } />
+
)
}
diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx
index 5212b9b..a321751 100644
--- a/src/components/layout/Sidebar.jsx
+++ b/src/components/layout/Sidebar.jsx
@@ -1,4 +1,5 @@
-import React from 'react'
+import React, { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
import { useStore } from '../../lib/store'
import CopyableValue from '../dashboard/CopyableValue'
import { NETWORKS, updateCustomNetworkConfig, switchToCustomProfile, loadCustomNetworkProfiles } from '../../lib/stellar'
@@ -43,13 +44,9 @@ const NAV_ITEMS = [
]
export default function Sidebar({ isMobile = false }) {
- const initialCustomHeaders = getCustomNetworkAuthHeaders()
- const initialHeaderName = Object.keys(initialCustomHeaders)[0] || 'Authorization'
- const [customHeaderName, setCustomHeaderName] = useState(initialHeaderName)
- const [customHeaderValue, setCustomHeaderValue] = useState(initialCustomHeaders[initialHeaderName] || '')
+ const navigate = useNavigate()
const {
activeTab,
- setActiveTab,
network,
setNetwork,
connectedAddress,
@@ -84,26 +81,8 @@ export default function Sidebar({ isMobile = false }) {
}, [network])
const handleNavClick = (tabId) => {
- setActiveTab(tabId)
- setMobileMenuOpen(false)
- }
-
- const handleSwitchProfile = async (profileId) => {
- try {
- await switchToCustomProfile(profileId)
- setActiveProfileId(profileId)
- // Force store update to refresh clients
- const profile = customProfiles.find(p => p.id === profileId)
- if (profile) {
- updateCustomNetworkConfig({
- horizonUrl: profile.horizonUrl,
- sorobanUrl: profile.sorobanUrl,
- passphrase: profile.passphrase,
- })
- }
- } catch (err) {
- console.error('Failed to switch profile:', err)
- }
+ navigate(`/${tabId}`)
+ setMobileMenuOpen(false) // Close mobile menu after navigation
}
// Restore custom API key from sessionStorage on mount
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 3bfacd2..943360c 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -399,6 +399,11 @@ export const useStore = create((set, get) => ({
setStreamError: (e) => set({ streamError: e }),
}))
+// ─── Expose store for e2e testing ────────────────────────────────────────────
+if (typeof window !== 'undefined') {
+ (window as any).__store = useStore
+}
+
// ─── Persistence middleware ───────────────────────────────────────────────────
if (typeof window !== 'undefined') {
diff --git a/src/main.jsx b/src/main.jsx
index 53055c3..a331ca5 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./styles/globals.css";
import { initPerformanceMonitoring } from "./lib/performanceMonitoring";
@@ -9,6 +10,8 @@ initPerformanceMonitoring();
ReactDOM.createRoot(document.getElementById("root")).render(
-
+
+
+
,
);
diff --git a/tests/e2e/navigation.spec.js b/tests/e2e/navigation.spec.js
index 54634d9..50d5ab1 100644
--- a/tests/e2e/navigation.spec.js
+++ b/tests/e2e/navigation.spec.js
@@ -13,16 +13,86 @@ test.describe('Sidebar navigation', () => {
test('network toggle switches between testnet and mainnet', async ({ page }) => {
const mainBtn = page.locator('button', { hasText: 'Main' });
await mainBtn.click();
- await expect(mainBtn).toHaveCSS('color', /0, 229, 255/); // cyan active color
+ await expect(mainBtn).toHaveCSS('color', /0, 229, 255/);
});
- test('clicking Multisig nav item shows multisig page', async ({ page }) => {
- await page.locator('button', { hasText: 'Multisig' }).click();
- await expect(page.locator('text=Multi-Signature')).toBeVisible();
+ test('unauthenticated user visiting /transactions is redirected to /connect', async ({ page }) => {
+ await page.goto('/transactions');
+ await expect(page).toHaveURL(/\/connect/);
});
- test('clicking Overview nav item shows overview page', async ({ page }) => {
- await page.locator('button', { hasText: 'Overview' }).click();
- await expect(page.locator('text=Overview')).toBeVisible();
+ test('unauthenticated user visiting /overview is redirected to /connect', async ({ page }) => {
+ await page.goto('/overview');
+ await expect(page).toHaveURL(/\/connect/);
+ });
+
+ test('unauthenticated user visiting /contracts is redirected to /connect', async ({ page }) => {
+ await page.goto('/contracts');
+ await expect(page).toHaveURL(/\/connect/);
+ });
+});
+
+test.describe('Authenticated navigation', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await page.evaluate(() => {
+ const store = window.__store;
+ if (store) {
+ store.getState().setConnectedAddress('GA2C5W4Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5');
+ }
+ });
+ });
+
+ test('sidebar click on Transactions updates URL to /transactions', async ({ page }) => {
+ await page.locator('aside button', { hasText: 'Transactions' }).click();
+ await expect(page).toHaveURL(/\/transactions/);
+ });
+
+ test('sidebar click on Overview updates URL to /overview', async ({ page }) => {
+ await page.locator('aside button', { hasText: 'Overview' }).click();
+ await expect(page).toHaveURL(/\/overview/);
+ });
+
+ test('sidebar click on Contracts updates URL to /contracts', async ({ page }) => {
+ await page.locator('aside button', { hasText: 'Contracts' }).click();
+ await expect(page).toHaveURL(/\/contracts/);
+ });
+
+ test('sidebar click on Assets updates URL to /assets', async ({ page }) => {
+ await page.locator('aside button', { hasText: 'Assets' }).click();
+ await expect(page).toHaveURL(/\/assets/);
+ });
+
+ test('sidebar click on Charts updates URL to /charts', async ({ page }) => {
+ await page.locator('aside button', { hasText: 'Charts' }).click();
+ await expect(page).toHaveURL(/\/charts/);
+ });
+
+ test('browser back/forward updates active tab', async ({ page }) => {
+ await page.locator('aside button', { hasText: 'Contracts' }).click();
+ await expect(page).toHaveURL(/\/contracts/);
+
+ await page.locator('aside button', { hasText: 'Assets' }).click();
+ await expect(page).toHaveURL(/\/assets/);
+
+ await page.goBack();
+ await expect(page).toHaveURL(/\/contracts/);
+
+ await page.goBack();
+ await expect(page).toHaveURL(/\/overview/);
+
+ await page.goForward();
+ await expect(page).toHaveURL(/\/contracts/);
+ });
+
+ test('direct navigation to /contracts renders contracts panel when connected', async ({ page }) => {
+ await page.goto('/contracts');
+ await expect(page).toHaveURL(/\/contracts/);
+ await expect(page.locator('text=Contracts')).toBeVisible();
+ });
+
+ test('direct navigation to /search renders search panel when connected', async ({ page }) => {
+ await page.goto('/search');
+ await expect(page).toHaveURL(/\/search/);
});
});