diff --git a/wata-board-frontend/src/components/NetworkSwitcher.tsx b/wata-board-frontend/src/components/NetworkSwitcher.tsx index b619a8d7..b3a72dc2 100644 --- a/wata-board-frontend/src/components/NetworkSwitcher.tsx +++ b/wata-board-frontend/src/components/NetworkSwitcher.tsx @@ -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; @@ -13,26 +13,71 @@ export const NetworkSwitcher: React.FC = ({ }) => { const [currentNetwork, setCurrentNetwork] = useState('testnet'); const [isDevelopment, setIsDevelopment] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); + const [pendingNetwork, setPendingNetwork] = useState(null); + const confirmationRef = useRef(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(); @@ -55,35 +100,100 @@ export const NetworkSwitcher: React.FC = ({ } return ( -
- {showLabel && Network:} -
- - + <> +
+ {showLabel && Network:} +
+ + +
+ {isMainnet && ( +
+ ⚠️ MAINNET +
+ )}
- {isMainnet && ( -
- ⚠️ MAINNET + + {/* Confirmation Dialog */} + {showConfirmation && pendingNetwork && ( +
+
+
+
+ ⚠️ +
+

+ Switch to {pendingNetwork.toUpperCase()}? +

+
+ +

+ {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.' + } +

+ + {pendingNetwork === 'mainnet' && ( +
+

+ ⚠️ + Warning: This is a production network. Real funds will be used. +

+
+ )} + +
+ + +
+
)} -
+ ); }; diff --git a/wata-board-frontend/src/hooks/useNetwork.ts b/wata-board-frontend/src/hooks/useNetwork.ts new file mode 100644 index 00000000..d6047865 --- /dev/null +++ b/wata-board-frontend/src/hooks/useNetwork.ts @@ -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(() => { + // 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; \ No newline at end of file diff --git a/wata-board-frontend/src/i18n/locales/en.json b/wata-board-frontend/src/i18n/locales/en.json index 781820ae..cc67115c 100644 --- a/wata-board-frontend/src/i18n/locales/en.json +++ b/wata-board-frontend/src/i18n/locales/en.json @@ -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": { diff --git a/wata-board-frontend/src/services/feeEstimation.ts b/wata-board-frontend/src/services/feeEstimation.ts index 57bc8f36..f5838969 100644 --- a/wata-board-frontend/src/services/feeEstimation.ts +++ b/wata-board-frontend/src/services/feeEstimation.ts @@ -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 @@ -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); } @@ -300,4 +314,4 @@ export const feeUtils = { } }; -export default feeEstimationService; +export default feeEstimationService; \ No newline at end of file diff --git a/wata-board-frontend/src/services/walletBalance.ts b/wata-board-frontend/src/services/walletBalance.ts index 0c107924..2c2da0e9 100644 --- a/wata-board-frontend/src/services/walletBalance.ts +++ b/wata-board-frontend/src/services/walletBalance.ts @@ -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; @@ -61,12 +61,36 @@ export class WalletBalanceService { private balanceCache: Map = new Map(); private updateCallbacks: Set = 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 { diff --git a/wata-board-frontend/src/utils/network-config.ts b/wata-board-frontend/src/utils/network-config.ts index 45152a10..f52a89a3 100644 --- a/wata-board-frontend/src/utils/network-config.ts +++ b/wata-board-frontend/src/utils/network-config.ts @@ -14,23 +14,39 @@ export const NETWORKS: Record = { }, mainnet: { networkPassphrase: "Public Global Stellar Network ; September 2015", - contractId: "MAINNET_CONTRACT_ID_HERE", // Replace with actual mainnet contract ID + contractId: "MAINNET_CONTRACT_ID_HERE", rpcUrl: "https://soroban.stellar.org", }, }; +export const NETWORK_STORAGE_KEY = 'wata-board-network'; +export const NETWORK_CHANGE_EVENT = 'wata-network-change'; + export function getNetworkConfig(network: NetworkType): NetworkConfig { return NETWORKS[network]; } +export function getStoredNetwork(): NetworkType | null { + if (typeof window === 'undefined') return null; + const stored = localStorage.getItem(NETWORK_STORAGE_KEY); + return stored === 'testnet' || stored === 'mainnet' ? stored : null; +} + +export function getCurrentNetwork(): NetworkType { + // Check localStorage first (user preference) + const stored = getStoredNetwork(); + if (stored) return stored; + // Fall back to environment variable + return getNetworkFromEnv(); +} + export function getNetworkFromEnv(): NetworkType { - // For frontend (Vite): import.meta.env.VITE_NETWORK - const network = import.meta.env.VITE_NETWORK; + const network = import.meta?.env?.VITE_NETWORK; return network === 'mainnet' ? 'mainnet' : 'testnet'; } export function getCurrentNetworkConfig(): NetworkConfig { - const network = getNetworkFromEnv(); + const network = getCurrentNetwork(); return getNetworkConfig(network); }