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
186 changes: 148 additions & 38 deletions wata-board-frontend/src/components/NetworkSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import type { NetworkType } from '../utils/network-config';
import { getCurrentNetworkConfig, getNetworkConfig } from '../utils/network-config';
import { getCurrentNetwork, getCurrentNetworkConfig, NETWORK_STORAGE_KEY, NETWORK_CHANGE_EVENT } from '../utils/network-config';

interface NetworkSwitcherProps {
onNetworkChange?: (network: NetworkType) => void;
Expand All @@ -13,26 +13,71 @@ export const NetworkSwitcher: React.FC<NetworkSwitcherProps> = ({
}) => {
const [currentNetwork, setCurrentNetwork] = useState<NetworkType>('testnet');
const [isDevelopment, setIsDevelopment] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const [pendingNetwork, setPendingNetwork] = useState<NetworkType | null>(null);
const confirmationRef = useRef<HTMLDivElement>(null);

useEffect(() => {
// Check if we're in development mode
setIsDevelopment(import.meta.env.DEV);
setIsDevelopment(import.meta.env?.DEV || false);

// Get current network from environment
const network = import.meta.env.VITE_NETWORK as NetworkType || 'testnet';
setCurrentNetwork(network);
// Check localStorage first, then fall back to environment
const savedNetwork = localStorage.getItem(NETWORK_STORAGE_KEY);
if (savedNetwork === 'mainnet' || savedNetwork === 'testnet') {
setCurrentNetwork(savedNetwork);
} else {
const network = (import.meta.env?.VITE_NETWORK as NetworkType) || 'testnet';
setCurrentNetwork(network);
}
}, []);

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && showConfirmation) {
setShowConfirmation(false);
setPendingNetwork(null);
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [showConfirmation]);

const handleNetworkChange = (newNetwork: NetworkType) => {
setCurrentNetwork(newNetwork);
onNetworkChange?.(newNetwork);
if (newNetwork === currentNetwork) return;

// Always show confirmation when switching
setPendingNetwork(newNetwork);
setShowConfirmation(true);
};

const confirmNetworkChange = () => {
if (!pendingNetwork) return;

// Save to localStorage
localStorage.setItem(NETWORK_STORAGE_KEY, pendingNetwork);
setCurrentNetwork(pendingNetwork);

// Dispatch custom event for services to listen to
window.dispatchEvent(new CustomEvent(NETWORK_CHANGE_EVENT, {
detail: { network: pendingNetwork }
}));

// In development, show a message about restarting
// Call callback
onNetworkChange?.(pendingNetwork);

// In development, show a message about restart
if (isDevelopment) {
const message = `Network changed to ${newNetwork}. Please restart the development server for changes to take effect.`;
const message = `Network changed to ${pendingNetwork}. Please restart the development server for some changes to take full effect.`;
console.warn(message);
alert(message);
}

setShowConfirmation(false);
setPendingNetwork(null);
};

const cancelNetworkChange = () => {
setShowConfirmation(false);
setPendingNetwork(null);
};

const currentConfig = getCurrentNetworkConfig();
Expand All @@ -55,35 +100,100 @@ export const NetworkSwitcher: React.FC<NetworkSwitcherProps> = ({
}

return (
<div className="flex items-center gap-3">
{showLabel && <span className="text-sm text-slate-400">Network:</span>}
<div className="flex rounded-lg bg-slate-800 p-1">
<button
onClick={() => handleNetworkChange('testnet')}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
!isMainnet
? 'bg-sky-500 text-white shadow-sm'
: 'text-slate-400 hover:text-slate-300'
}`}
>
Testnet
</button>
<button
onClick={() => handleNetworkChange('mainnet')}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
isMainnet
? 'bg-orange-500 text-white shadow-sm'
: 'text-slate-400 hover:text-slate-300'
}`}
>
Mainnet
</button>
<>
<div className="flex items-center gap-3">
{showLabel && <span className="text-sm text-slate-400">Network:</span>}
<div className="flex rounded-lg bg-slate-800 p-1">
<button
onClick={() => handleNetworkChange('testnet')}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
!isMainnet
? 'bg-sky-500 text-white shadow-sm'
: 'text-slate-400 hover:text-slate-300'
}`}
aria-pressed={!isMainnet}
>
Testnet
</button>
<button
onClick={() => handleNetworkChange('mainnet')}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
isMainnet
? 'bg-orange-500 text-white shadow-sm'
: 'text-slate-400 hover:text-slate-300'
}`}
aria-pressed={isMainnet}
>
Mainnet
</button>
</div>
{isMainnet && (
<div className="rounded-full bg-red-500/10 px-2 py-1 text-xs font-medium text-red-300 ring-1 ring-inset ring-red-500/20">
⚠️ MAINNET
</div>
)}
</div>
{isMainnet && (
<div className="rounded-full bg-red-500/10 px-2 py-1 text-xs font-medium text-red-300 ring-1 ring-inset ring-red-500/20">
⚠️ MAINNET

{/* Confirmation Dialog */}
{showConfirmation && pendingNetwork && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-labelledby="network-switch-title"
aria-modal="true"
>
<div
ref={confirmationRef}
className="bg-slate-900 border border-slate-800 rounded-2xl p-6 max-w-md w-full shadow-xl shadow-black/20"
>
<div className="flex items-center gap-3 mb-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
pendingNetwork === 'mainnet' ? 'bg-orange-500/20' : 'bg-sky-500/20'
}`}>
⚠️
</div>
<h3 id="network-switch-title" className="text-lg font-semibold text-slate-100">
Switch to {pendingNetwork.toUpperCase()}?
</h3>
</div>

<p className="text-sm text-slate-300 mb-4">
{pendingNetwork === 'mainnet'
? 'You are about to switch to the live Stellar Mainnet. Transactions will use real XLM.'
: 'You are about to switch to the Stellar Testnet. Transactions will use test XLM.'
}
</p>

{pendingNetwork === 'mainnet' && (
<div className="rounded-lg bg-red-500/10 border border-red-500/20 p-3 mb-4">
<p className="text-xs text-red-300 font-medium flex items-center gap-1">
<span>⚠️</span>
<span>Warning: This is a production network. Real funds will be used.</span>
</p>
</div>
)}

<div className="flex gap-3 justify-end">
<button
onClick={cancelNetworkChange}
className="px-4 py-2 rounded-lg text-sm font-medium text-slate-300 bg-slate-800 hover:bg-slate-700 transition-colors focus:outline-none focus:ring-2 focus:ring-slate-600"
>
Cancel
</button>
<button
onClick={confirmNetworkChange}
className={`px-4 py-2 rounded-lg text-sm font-medium text-white transition-colors focus:outline-none focus:ring-2 ${
pendingNetwork === 'mainnet'
? 'bg-orange-600 hover:bg-orange-500 focus:ring-orange-500/50'
: 'bg-sky-600 hover:bg-sky-500 focus:ring-sky-500/50'
}`}
>
Switch to {pendingNetwork.toUpperCase()}
</button>
</div>
</div>
</div>
)}
</div>
</>
);
};
57 changes: 57 additions & 0 deletions wata-board-frontend/src/hooks/useNetwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState, useEffect, useCallback } from 'react';
import type { NetworkType } from '../utils/network-config';
import { getCurrentNetwork, getStoredNetwork, NETWORK_CHANGE_EVENT } from '../utils/network-config';

export interface UseNetworkReturn {
network: NetworkType;
isMainnet: boolean;
isTestnet: boolean;
switchNetwork: (network: NetworkType) => void;
canSwitch: boolean;
}

export function useNetwork(autoRefresh: boolean = true): UseNetworkReturn {
const [network, setNetwork] = useState<NetworkType>(() => {
// Initialize from localStorage or environment
return getStoredNetwork() || getCurrentNetwork();
});

const listenToNetworkChanges = useCallback(() => {
const handler = (e: CustomEvent) => {
setNetwork(e.detail.network);
};
window.addEventListener(NETWORK_CHANGE_EVENT, handler as any);
return () => window.removeEventListener(NETWORK_CHANGE_EVENT, handler as any);
}, []);

useEffect(() => {
// Check for initial stored network
const stored = getStoredNetwork();
if (stored && stored !== network) {
setNetwork(stored);
}
}, []);

useEffect(() => {
if (autoRefresh) {
return listenToNetworkChanges();
}
}, [listenToNetworkChanges, autoRefresh]);

const switchNetwork = useCallback((newNetwork: NetworkType) => {
// This will be handled by the NetworkSwitcher component
// Services can listen to the NETWORK_CHANGE_EVENT
const event = new CustomEvent(NETWORK_CHANGE_EVENT, { detail: { network: newNetwork } });
window.dispatchEvent(event);
}, []);

return {
network,
isMainnet: network === 'mainnet',
isTestnet: network === 'testnet',
switchNetwork,
canSwitch: import.meta?.env?.DEV || false
};
}

export default useNetwork;
8 changes: 7 additions & 1 deletion wata-board-frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@
"mainnet": "MAINNET",
"testnet": "TESTNET",
"switchNetwork": "Switch Network",
"currentNetwork": "Current Network"
"currentNetwork": "Current Network",
"switchTo": "Switch to {{network}?",
"mainnetWarning": "Warning: This is a production network. Real funds will be used.",
"testnetInfo": "You are switching to the Stellar Testnet. Transactions will use test XLM.",
"mainnetConfirm": "You are about to switch to the live Stellar Mainnet. Transactions will use real XLM.",
"cancel": "Cancel",
"confirm": "Confirm"
},
"offline": {
"banner": {
Expand Down
18 changes: 16 additions & 2 deletions wata-board-frontend/src/services/feeEstimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Horizon, Networks, TransactionBuilder, Operation, Asset, BASE_FEE } from '@stellar/stellar-sdk';
import { requestAccess } from '../utils/wallet-bridge';
import { getCurrentNetworkConfig } from '../utils/network-config';
import { getCurrentNetworkConfig, NETWORK_CHANGE_EVENT } from '../utils/network-config';

export interface FeeEstimate {
baseFee: number; // Base fee in stroops
Expand All @@ -26,9 +26,23 @@ export interface TransactionDetails {
export class FeeEstimationService {
private server: Horizon.Server;
private networkConfig: any;
private networkChangeHandler: (() => void) | null = null;

constructor() {
this.networkConfig = getCurrentNetworkConfig();
this.updateServer();

// Listen for network changes
if (typeof window !== 'undefined') {
this.networkChangeHandler = () => {
this.networkConfig = getCurrentNetworkConfig();
this.updateServer();
};
window.addEventListener(NETWORK_CHANGE_EVENT, this.networkChangeHandler as any);
}
}

private updateServer(): void {
const horizonUrl = this.networkConfig.rpcUrl.replace('soroban', 'horizon');
this.server = new Horizon.Server(horizonUrl);
}
Expand Down Expand Up @@ -300,4 +314,4 @@ export const feeUtils = {
}
};

export default feeEstimationService;
export default feeEstimationService;
28 changes: 26 additions & 2 deletions wata-board-frontend/src/services/walletBalance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Horizon, Asset, Networks, StrKey } from '@stellar/stellar-sdk';
import { requestAccess, isConnected } from '../utils/wallet-bridge';
import { getCurrentNetworkConfig } from '../utils/network-config';
import { getCurrentNetworkConfig, NETWORK_CHANGE_EVENT } from '../utils/network-config';

export interface BalanceInfo {
assetCode: string;
Expand Down Expand Up @@ -61,12 +61,36 @@ export class WalletBalanceService {
private balanceCache: Map<string, { balance: WalletBalance; timestamp: number }> = new Map();
private updateCallbacks: Set<BalanceUpdateCallback> = new Set();
private refreshInterval: NodeJS.Timeout | null = null;
private readonly CACHE_DURATION = 30000; // 30 seconds cache
private readonly CACHE_DURATION = 30000;
private networkChangeHandler: (() => void) | null = null;

constructor() {
this.networkConfig = getCurrentNetworkConfig();
const horizonUrl = this.networkConfig.rpcUrl.replace('soroban', 'horizon');
this.server = new Horizon.Server(horizonUrl);

// Listen for network changes
if (typeof window !== 'undefined') {
this.networkChangeHandler = () => {
this.networkConfig = getCurrentNetworkConfig();
const horizonUrl = this.networkConfig.rpcUrl.replace('soroban', 'horizon');
this.server = new Horizon.Server(horizonUrl);
this.clearCache();
};
window.addEventListener(NETWORK_CHANGE_EVENT, this.networkChangeHandler as any);
}
}

clearCache(): void {
this.balanceCache.clear();
}

getCachedBalance(publicKey: string): WalletBalance | null {
const cached = this.balanceCache.get(publicKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
return cached.balance;
}
return null;
}

async refreshBalance(): Promise<WalletBalance | null> {
Expand Down
Loading
Loading