Skip to content
Open
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
12 changes: 10 additions & 2 deletions frontend/src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Outlet, NavLink, useLocation } from 'react-router-dom';
import ConnectAccount from '../components/ConnectAccount';
import AppNav from './AppNav';
import ThemeToggle from './ThemeToggle';
import TestnetBanner from './TestnetBanner';
import { useNetwork } from '../hooks/useNetwork';

// ── Page Wrapper ───────────────────────
const PageWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
Expand All @@ -12,6 +14,7 @@ const PageWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
// ── Layout ────────────────────────────
const AppLayout: React.FC = () => {
const location = useLocation();
const { config } = useNetwork();

return (
<div
Expand Down Expand Up @@ -49,8 +52,13 @@ const AppLayout: React.FC = () => {
</div>
</header>

{/* Testnet Banner — rendered in normal flow below the fixed header */}
<div className="pt-(--header-h)">
<TestnetBanner />
</div>

{/* Main */}
<main className="flex flex-col flex-1 pt-(--header-h)">
<main className="flex flex-col flex-1">
<PageWrapper>
<div key={location.pathname} className="flex flex-col flex-1 px-6 py-8">
<Outlet />
Expand All @@ -76,7 +84,7 @@ const AppLayout: React.FC = () => {
</span>
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-(--accent) shadow-[0_0_6px_var(--accent)]" />
STELLAR NETWORK · MAINNET
STELLAR NETWORK · {config.displayName.toUpperCase()}
</div>
</footer>
</div>
Expand Down
96 changes: 96 additions & 0 deletions frontend/src/components/NetworkSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { useNetwork } from '../hooks/useNetwork';
import type { NetworkName } from '../hooks/useNetwork';

/**
* Pill-style toggle between Testnet and Mainnet.
* Shows a confirmation modal before switching to explain side-effects.
*/
const NetworkSwitcher: React.FC = () => {
const { network, switchNetwork } = useNetwork();
const [pending, setPending] = useState<NetworkName | null>(null);

const handleRequest = (target: NetworkName) => {
if (target === network) return;
setPending(target);
};

const handleConfirm = () => {
if (pending) switchNetwork(pending);
setPending(null);
};

const handleCancel = () => setPending(null);

return (
<>
{/* Toggle pill */}
<div className="flex items-center gap-1 bg-black/20 border border-hi rounded-xl p-1">
<button
onClick={() => handleRequest('testnet')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-widest transition-all ${
network === 'testnet'
? 'bg-amber-500/20 text-amber-400 border border-amber-500/40'
: 'text-muted hover:text-text'
}`}
>
Testnet
</button>
<button
onClick={() => handleRequest('mainnet')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-widest transition-all ${
network === 'mainnet'
? 'bg-accent/20 text-accent border border-accent/40'
: 'text-muted hover:text-text'
}`}
>
Mainnet
</button>
</div>

{/* Confirmation modal */}
{pending && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-black/90 border border-hi rounded-2xl p-8 max-w-md w-full mx-4 shadow-2xl">
<h2 className="text-xl font-black mb-3">
Switch to{' '}
<span className={pending === 'testnet' ? 'text-amber-400' : 'text-accent'}>
{pending === 'testnet' ? 'Testnet' : 'Mainnet'}
</span>
?
</h2>
<div className="text-sm text-muted space-y-2 mb-6">
<p>Switching networks will:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Disconnect your wallet</li>
<li>Clear all cached balances and queries</li>
<li>Reset active socket subscriptions</li>
<li>Re-fetch the contract registry for {pending}</li>
</ul>
</div>
<div className="flex gap-3">
<button
onClick={handleConfirm}
className={`flex-1 py-3 font-black rounded-xl text-sm uppercase tracking-widest transition-all ${
pending === 'testnet'
? 'bg-amber-500/20 text-amber-400 border border-amber-500/40 hover:bg-amber-500 hover:text-black'
: 'bg-accent/20 text-accent border border-accent/40 hover:bg-accent hover:text-black'
}`}
>
Switch to {pending === 'testnet' ? 'Testnet' : 'Mainnet'}
</button>
<button
onClick={handleCancel}
className="flex-1 py-3 bg-black/20 border border-hi font-black rounded-xl text-sm uppercase tracking-widest hover:bg-black/40 transition-all"
>
Cancel
</button>
</div>
</div>
</div>
)}
</>
);
};

export default NetworkSwitcher;
46 changes: 46 additions & 0 deletions frontend/src/components/TestnetBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useState } from 'react';
import { useNetwork } from '../hooks/useNetwork';

const SESSION_KEY = 'payd-testnet-banner-dismissed';

const TestnetBanner: React.FC = () => {
const { isTestnet, switchNetwork } = useNetwork();
const [dismissed, setDismissed] = useState(() => {
return sessionStorage.getItem(SESSION_KEY) === 'true';
});

if (!isTestnet || dismissed) return null;

const handleDismiss = () => {
sessionStorage.setItem(SESSION_KEY, 'true');
setDismissed(true);
};

return (
<div className="w-full bg-amber-500/15 border-b border-amber-500/30 px-6 py-2.5 flex items-center justify-between gap-4 text-sm">
<div className="flex items-center gap-2 text-amber-400">
<span className="font-bold text-base leading-none">⚠</span>
<span>
<strong>Testnet mode</strong> — transactions have no real-world value.
</span>
</div>
<div className="flex items-center gap-3 shrink-0">
<button
onClick={() => switchNetwork('mainnet')}
className="px-3 py-1 bg-amber-500/20 border border-amber-500/40 text-amber-300 rounded-lg text-xs font-bold uppercase tracking-widest hover:bg-amber-500 hover:text-black transition-all"
>
Switch to Mainnet
</button>
<button
onClick={handleDismiss}
className="text-amber-500/60 hover:text-amber-400 transition-colors text-lg leading-none"
aria-label="Dismiss banner"
>
×
</button>
</div>
</div>
);
};

export default TestnetBanner;
2 changes: 1 addition & 1 deletion frontend/src/hooks/useFeeEstimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function useFeeEstimation() {
refetch,
} = useQuery<FeeRecommendation, Error>({
queryKey: FEE_ESTIMATION_QUERY_KEY,
queryFn: getFeeRecommendation,
queryFn: () => getFeeRecommendation(),
refetchInterval: POLL_INTERVAL_MS,
staleTime: POLL_INTERVAL_MS,
});
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/hooks/useNetwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createContext, use } from 'react';
import { WalletNetwork } from '@creit.tech/stellar-wallets-kit';

export type NetworkName = 'testnet' | 'mainnet';

export interface StellarNetworkConfig {
name: NetworkName;
displayName: string;
networkPassphrase: string;
horizonUrl: string;
rpcUrl: string;
walletNetwork: WalletNetwork;
}

export interface ContractRegistry {
bulkPayment: string;
crossAssetPayment: string;
vestingEscrow: string;
revenueSplit: string;
}

export interface NetworkContextType {
network: NetworkName;
config: StellarNetworkConfig;
contracts: ContractRegistry | null;
isTestnet: boolean;
switchNetwork: (network: NetworkName) => void;
}

export const NetworkContext = createContext<NetworkContextType | undefined>(undefined);

export const useNetwork = () => {
const context = use(NetworkContext);
if (!context) throw new Error('useNetwork must be used within NetworkProvider');
return context;
};
29 changes: 16 additions & 13 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { WalletProvider } from './providers/WalletProvider.tsx';
import { NotificationProvider } from './providers/NotificationProvider.tsx';
import { SocketProvider } from './providers/SocketProvider.tsx';
import { ThemeProvider } from './providers/ThemeProvider.tsx';
import { NetworkProvider } from './providers/NetworkProvider.tsx';
import * as Sentry from '@sentry/react';
import ErrorBoundary from './components/ErrorBoundary';
import ErrorFallback from './components/ErrorFallback';
Expand All @@ -30,19 +31,21 @@ const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<NotificationProvider>
<SocketProvider>
<WalletProvider>
<BrowserRouter>
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<App />
</ErrorBoundary>
</BrowserRouter>
</WalletProvider>
</SocketProvider>
</NotificationProvider>
</ThemeProvider>
<NetworkProvider>
<ThemeProvider>
<NotificationProvider>
<SocketProvider>
<WalletProvider>
<BrowserRouter>
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<App />
</ErrorBoundary>
</BrowserRouter>
</WalletProvider>
</SocketProvider>
</NotificationProvider>
</ThemeProvider>
</NetworkProvider>
</QueryClientProvider>
</React.StrictMode>
);
15 changes: 14 additions & 1 deletion frontend/src/pages/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Code2,
} from 'lucide-react';
import { useNotification } from '../hooks/useNotification';
import NetworkSwitcher from '../components/NetworkSwitcher';
import { useNetwork } from '../hooks/useNetwork';
import { useWallet } from '../hooks/useWallet';
import ContractUpgradeTab from '../components/ContractUpgradeTab';

Expand Down Expand Up @@ -75,6 +77,7 @@ const TAB_LABELS: Record<ActiveTab, string> = {

export default function AdminPanel() {
const { notifySuccess, notifyError } = useNotification();
const { config, isTestnet } = useNetwork();
const { address: adminAddress } = useWallet();

const [activeTab, setActiveTab] = useState<ActiveTab>('account');
Expand Down Expand Up @@ -242,13 +245,23 @@ export default function AdminPanel() {
{/* Header */}
<div className="w-full mb-8 flex items-end justify-between border-b border-hi pb-8">
<div>
<h1 className="text-4xl font-black mb-2 tracking-tight">
<h1 className="text-4xl font-black mb-2 tracking-tight flex items-center gap-3">
Security <span className="text-red-500">Center</span>
<span
className={`text-xs font-bold uppercase tracking-widest px-2 py-1 rounded border ${
isTestnet
? 'bg-amber-500/20 text-amber-400 border-amber-500/30'
: 'bg-accent/20 text-accent border-accent/30'
}`}
>
{config.displayName}
</span>
</h1>
<p className="text-muted font-mono text-sm tracking-wider uppercase">
Asset Freeze & Administrative Controls
</p>
</div>
<NetworkSwitcher />
</div>

{/* Tab bar */}
Expand Down
34 changes: 27 additions & 7 deletions frontend/src/pages/Debugger.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useNetwork } from '../hooks/useNetwork';
import NetworkSwitcher from '../components/NetworkSwitcher';

export default function Debugger() {
const { contractName } = useParams<{ contractName?: string }>();
const { t } = useTranslation();
const { config } = useNetwork();

return (
<div className="flex-1 flex flex-col items-center justify-start p-12 max-w-6xl mx-auto w-full">
Expand All @@ -17,11 +20,14 @@ export default function Debugger() {
{t('debugger.subtitle')}
</p>
</div>
{contractName && (
<div className="px-4 py-2 glass border-hi text-accent2 font-mono text-xs rounded-lg">
{t('debugger.target', { contractName })}
</div>
)}
<div className="flex items-center gap-3">
{contractName && (
<div className="px-4 py-2 glass border-hi text-accent2 font-mono text-xs rounded-lg">
{t('debugger.target', { contractName })}
</div>
)}
<NetworkSwitcher />
</div>
</div>

<div className="w-full grid grid-cols-1 lg:grid-cols-3 gap-8">
Expand All @@ -35,12 +41,26 @@ export default function Debugger() {
</span>
<span className="text-text font-mono text-sm">v21</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted text-xs uppercase font-bold tracking-widest">
Network
</span>
<span className="font-mono text-sm text-accent">{config.displayName}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted text-xs uppercase font-bold tracking-widest">
{t('debugger.horizon')}
</span>
<span className="text-success font-mono text-sm">
{t('debugger.horizonOnline')}
<span className="text-success font-mono text-xs truncate max-w-[140px]" title={config.horizonUrl}>
{config.horizonUrl.replace('https://', '')}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-muted text-xs uppercase font-bold tracking-widest">
RPC
</span>
<span className="text-text font-mono text-xs truncate max-w-[140px]" title={config.rpcUrl}>
{config.rpcUrl.replace('https://', '')}
</span>
</div>
<div className="flex justify-between items-center">
Expand Down
Loading
Loading