From 30a248077394af1467f3e4476f7918f6d593d830 Mon Sep 17 00:00:00 2001 From: realmpastai-web Date: Mon, 2 Mar 2026 17:51:43 +0530 Subject: [PATCH] feat: add network switcher components for multi-chain support - Add NetworkContext for managing network state - Add NetworkSelector UI component - Add NetworkSwitchDialog for network switching - Add network utilities (network.ts, sorosave.ts) Closes sorosave-protocol/frontend#80 --- src/components/network/NetworkContext.tsx | 86 +++++++++++++++++++ src/components/network/NetworkSelector.tsx | 44 ++++++++++ .../network/NetworkSwitchDialog.tsx | 51 +++++++++++ src/network.ts | 42 +++++++++ src/sorosave.ts | 16 ++++ 5 files changed, 239 insertions(+) create mode 100644 src/components/network/NetworkContext.tsx create mode 100644 src/components/network/NetworkSelector.tsx create mode 100644 src/components/network/NetworkSwitchDialog.tsx create mode 100644 src/network.ts create mode 100644 src/sorosave.ts diff --git a/src/components/network/NetworkContext.tsx b/src/components/network/NetworkContext.tsx new file mode 100644 index 0000000..504ce45 --- /dev/null +++ b/src/components/network/NetworkContext.tsx @@ -0,0 +1,86 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { + NetworkType, + NetworkConfig, + NETWORKS, + getStoredNetwork, + setStoredNetwork, + clearNetworkCache +} from '@/lib/network'; + +interface NetworkContextType { + network: NetworkType; + config: NetworkConfig; + switchNetwork: (network: NetworkType) => void; + showConfirmDialog: boolean; + pendingNetwork: NetworkType | null; + confirmSwitch: () => void; + cancelSwitch: () => void; +} + +const NetworkContext = createContext(undefined); + +export function NetworkProvider({ children }: { children: React.ReactNode }) { + const [network, setNetwork] = useState('testnet'); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [pendingNetwork, setPendingNetwork] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + const stored = getStoredNetwork(); + setNetwork(stored); + setIsInitialized(true); + }, []); + + const switchNetwork = useCallback((newNetwork: NetworkType) => { + if (newNetwork === network) return; + setPendingNetwork(newNetwork); + setShowConfirmDialog(true); + }, [network]); + + const confirmSwitch = useCallback(() => { + if (pendingNetwork) { + clearNetworkCache(); + setStoredNetwork(pendingNetwork); + setNetwork(pendingNetwork); + setShowConfirmDialog(false); + setPendingNetwork(null); + window.location.reload(); + } + }, [pendingNetwork]); + + const cancelSwitch = useCallback(() => { + setShowConfirmDialog(false); + setPendingNetwork(null); + }, []); + + const value: NetworkContextType = { + network, + config: NETWORKS[network], + switchNetwork, + showConfirmDialog, + pendingNetwork, + confirmSwitch, + cancelSwitch, + }; + + if (!isInitialized) { + return null; + } + + return ( + + {children} + + ); +} + +export function useNetwork() { + const context = useContext(NetworkContext); + if (context === undefined) { + throw new Error('useNetwork must be used within a NetworkProvider'); + } + return context; +} diff --git a/src/components/network/NetworkSelector.tsx b/src/components/network/NetworkSelector.tsx new file mode 100644 index 0000000..081b0a5 --- /dev/null +++ b/src/components/network/NetworkSelector.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useNetwork } from '@/contexts/NetworkContext'; +import { NETWORKS, NetworkType } from '@/lib/network'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { ChevronDown, Globe } from 'lucide-react'; + +export function NetworkSelector() { + const { network, config, switchNetwork } = useNetwork(); + + return ( + + + + + + {(Object.keys(NETWORKS) as NetworkType[]).map((net) => ( + switchNetwork(net)} + className={network === net ? 'bg-accent' : ''} + > + + {NETWORKS[net].name} + + {network === net && (current)} + + ))} + + + ); +} diff --git a/src/components/network/NetworkSwitchDialog.tsx b/src/components/network/NetworkSwitchDialog.tsx new file mode 100644 index 0000000..82e56d0 --- /dev/null +++ b/src/components/network/NetworkSwitchDialog.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useNetwork } from '@/contexts/NetworkContext'; +import { NETWORKS } from '@/lib/network'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle } from 'lucide-react'; + +export function NetworkSwitchDialog() { + const { showConfirmDialog, pendingNetwork, confirmSwitch, cancelSwitch } = useNetwork(); + + if (!pendingNetwork) return null; + + return ( + + + + + + Switch Network? + + + You are about to switch to {NETWORKS[pendingNetwork].name}. +

+ This will: +
    +
  • Clear all cached data
  • +
  • Disconnect current wallet session
  • +
  • Reload the application
  • +
+
+
+ + + + +
+
+ ); +} diff --git a/src/network.ts b/src/network.ts new file mode 100644 index 0000000..d95c002 --- /dev/null +++ b/src/network.ts @@ -0,0 +1,42 @@ +export type NetworkType = 'mainnet' | 'testnet'; + +export interface NetworkConfig { + name: string; + rpcUrl: string; + contractId: string; + passphrase: string; +} + +export const NETWORKS: Record = { + mainnet: { + name: 'Mainnet', + rpcUrl: 'https://horizon.stellar.org', + contractId: process.env.NEXT_PUBLIC_MAINNET_CONTRACT_ID || '', + passphrase: 'Public Global Stellar Network ; September 2015', + }, + testnet: { + name: 'Testnet', + rpcUrl: 'https://horizon-testnet.stellar.org', + contractId: process.env.NEXT_PUBLIC_TESTNET_CONTRACT_ID || '', + passphrase: 'Test SDF Network ; September 2015', + }, +}; + +const STORAGE_KEY = 'sorosave-network'; + +export function getStoredNetwork(): NetworkType { + if (typeof window === 'undefined') return 'testnet'; + const stored = localStorage.getItem(STORAGE_KEY); + return (stored as NetworkType) || 'testnet'; +} + +export function setStoredNetwork(network: NetworkType): void { + localStorage.setItem(STORAGE_KEY, network); +} + +export function clearNetworkCache(): void { + const keysToRemove = Object.keys(localStorage).filter(key => + key.startsWith('sorosave-') && key !== STORAGE_KEY + ); + keysToRemove.forEach(key => localStorage.removeItem(key)); +} diff --git a/src/sorosave.ts b/src/sorosave.ts new file mode 100644 index 0000000..dc1e5c6 --- /dev/null +++ b/src/sorosave.ts @@ -0,0 +1,16 @@ +import { getStoredNetwork, NETWORKS } from './network'; + +export function getRpcUrl(): string { + const network = getStoredNetwork(); + return NETWORKS[network].rpcUrl; +} + +export function getContractId(): string { + const network = getStoredNetwork(); + return NETWORKS[network].contractId; +} + +export function getNetworkPassphrase(): string { + const network = getStoredNetwork(); + return NETWORKS[network].passphrase; +}