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
25 changes: 14 additions & 11 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
import { connectWallet, getPublicKey, isFreighterInstalled } from "@/lib/wallet";
import { NetworkProvider } from "@/contexts/NetworkContext";

interface WalletContextType {
address: string | null;
Expand Down Expand Up @@ -45,16 +46,18 @@ export function Providers({ children }: { children: React.ReactNode }) {
}, []);

return (
<WalletContext.Provider
value={{
address,
isConnected: !!address,
isFreighterAvailable,
connect,
disconnect,
}}
>
{children}
</WalletContext.Provider>
<NetworkProvider>
<WalletContext.Provider
value={{
address,
isConnected: !!address,
isFreighterAvailable,
connect,
disconnect,
}}
>
{children}
</WalletContext.Provider>
</NetworkProvider>
);
}
6 changes: 5 additions & 1 deletion src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Link from "next/link";
import { ConnectWallet } from "./ConnectWallet";
import { NetworkSelector } from "./NetworkSelector";

export function Navbar() {
return (
Expand All @@ -27,7 +28,10 @@ export function Navbar() {
</Link>
</div>
</div>
<ConnectWallet />
<div className="flex items-center space-x-4">
<NetworkSelector />
<ConnectWallet />
</div>
</div>
</div>
</nav>
Expand Down
139 changes: 139 additions & 0 deletions src/components/NetworkSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import { useState } from "react";
import { useNetwork } from "@/contexts/NetworkContext";
import { getAllNetworks, NetworkId } from "@/lib/network";

/**
* Dropdown network selector displayed in the Navbar.
* Shows the current network with a colored indicator dot and allows
* switching between Testnet and Mainnet with a confirmation dialog.
*/
export function NetworkSelector() {
const { networkId, switchNetwork } = useNetwork();
const [pendingNetwork, setPendingNetwork] = useState<NetworkId | null>(null);
const [isOpen, setIsOpen] = useState(false);
const networks = getAllNetworks();

const indicatorColor =
networkId === "mainnet" ? "bg-green-500" : "bg-yellow-500";

function handleSelect(id: NetworkId) {
setIsOpen(false);
if (id === networkId) return;
setPendingNetwork(id);
}

function confirmSwitch() {
if (pendingNetwork) {
switchNetwork(pendingNetwork);
setPendingNetwork(null);
}
}

function cancelSwitch() {
setPendingNetwork(null);
}

return (
<>
{/* Dropdown trigger */}
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center space-x-2 bg-gray-100 hover:bg-gray-200 px-3 py-1.5 rounded-lg text-sm font-medium text-gray-700 transition-colors"
aria-label="Select network"
>
<span className={`w-2 h-2 rounded-full ${indicatorColor}`} />
<span>{networkId === "mainnet" ? "Mainnet" : "Testnet"}</span>
<svg
className={`w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>

{isOpen && (
<div className="absolute right-0 mt-2 w-44 bg-white rounded-lg shadow-lg border z-50">
{networks.map((n) => (
<button
key={n.id}
onClick={() => handleSelect(n.id)}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-50 first:rounded-t-lg last:rounded-b-lg flex items-center space-x-2 ${
n.id === networkId
? "text-primary-700 font-semibold"
: "text-gray-700"
}`}
>
<span
className={`w-2 h-2 rounded-full ${
n.id === "mainnet" ? "bg-green-500" : "bg-yellow-500"
}`}
/>
<span>{n.name}</span>
{n.id === networkId && (
<svg
className="w-4 h-4 ml-auto text-primary-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
))}
</div>
)}
</div>

{/* Confirmation dialog */}
{pendingNetwork && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100]">
<div className="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Switch Network
</h3>
<p className="text-sm text-gray-600 mb-6">
Are you sure you want to switch to{" "}
<span className="font-medium">
{pendingNetwork === "mainnet"
? "Stellar Mainnet"
: "Stellar Testnet"}
</span>
? Cached data will be cleared and the page will reload with the
new network configuration.
</p>
<div className="flex space-x-3 justify-end">
<button
onClick={cancelSwitch}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Cancel
</button>
<button
onClick={confirmSwitch}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 transition-colors"
>
Switch
</button>
</div>
</div>
</div>
)}
</>
);
}
65 changes: 65 additions & 0 deletions src/contexts/NetworkContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
} from "react";
import {
NetworkId,
NetworkConfig,
getNetworkConfig,
loadSelectedNetwork,
saveSelectedNetwork,
} from "@/lib/network";

interface NetworkContextType {
/** The currently active network id. */
networkId: NetworkId;
/** Full config object for the active network. */
network: NetworkConfig;
/** Switch to a different network. Persists the choice and clears cached data. */
switchNetwork: (id: NetworkId) => void;
}

const NetworkContext = createContext<NetworkContextType>({
networkId: "testnet",
network: getNetworkConfig("testnet"),
switchNetwork: () => {},
});

export function useNetwork() {
return useContext(NetworkContext);
}

export function NetworkProvider({ children }: { children: React.ReactNode }) {
const [networkId, setNetworkId] = useState<NetworkId>("testnet");

// Load persisted selection on mount (client-only)
useEffect(() => {
setNetworkId(loadSelectedNetwork());
}, []);

const switchNetwork = useCallback(
(id: NetworkId) => {
if (id === networkId) return;
saveSelectedNetwork(id);
setNetworkId(id);
// Clear any cached contract / group data so the UI re-fetches for the new network
if (typeof window !== "undefined") {
sessionStorage.clear();
}
},
[networkId],
);

const network = getNetworkConfig(networkId);

return (
<NetworkContext.Provider value={{ networkId, network, switchNetwork }}>
{children}
</NetworkContext.Provider>
);
}
77 changes: 77 additions & 0 deletions src/lib/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Network configuration for Stellar Testnet and Mainnet.
* Provides network definitions, persistence, and helpers for switching
* between networks in the SoroSave frontend.
*/

export type NetworkId = "testnet" | "mainnet";

export interface NetworkConfig {
id: NetworkId;
name: string;
rpcUrl: string;
networkPassphrase: string;
horizonUrl: string;
contractId: string;
}

const NETWORKS: Record<NetworkId, NetworkConfig> = {
testnet: {
id: "testnet",
name: "Testnet",
rpcUrl:
process.env.NEXT_PUBLIC_TESTNET_RPC_URL ||
"https://soroban-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
horizonUrl: "https://horizon-testnet.stellar.org",
contractId: process.env.NEXT_PUBLIC_TESTNET_CONTRACT_ID || "",
},
mainnet: {
id: "mainnet",
name: "Mainnet",
rpcUrl:
process.env.NEXT_PUBLIC_MAINNET_RPC_URL ||
"https://soroban.stellar.org",
networkPassphrase: "Public Global Stellar Network ; September 2015",
horizonUrl: "https://horizon.stellar.org",
contractId: process.env.NEXT_PUBLIC_MAINNET_CONTRACT_ID || "",
},
};

const STORAGE_KEY = "sorosave_selected_network";

/**
* Retrieve the full config for a given network ID.
*/
export function getNetworkConfig(id: NetworkId): NetworkConfig {
return NETWORKS[id];
}

/**
* Return all available networks.
*/
export function getAllNetworks(): NetworkConfig[] {
return Object.values(NETWORKS);
}

/**
* Persist the selected network in localStorage.
*/
export function saveSelectedNetwork(id: NetworkId): void {
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, id);
}
}

/**
* Load the previously selected network from localStorage, defaulting to testnet.
*/
export function loadSelectedNetwork(): NetworkId {
if (typeof window !== "undefined") {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "testnet" || stored === "mainnet") {
return stored;
}
}
return "testnet";
}
34 changes: 23 additions & 11 deletions src/lib/sorosave.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { SoroSaveClient } from "@sorosave/sdk";
import { getNetworkConfig, loadSelectedNetwork, type NetworkConfig } from "./network";

const TESTNET_RPC_URL =
process.env.NEXT_PUBLIC_RPC_URL || "https://soroban-testnet.stellar.org";
const NETWORK_PASSPHRASE =
process.env.NEXT_PUBLIC_NETWORK_PASSPHRASE || "Test SDF Network ; September 2015";
const CONTRACT_ID = process.env.NEXT_PUBLIC_CONTRACT_ID || "";
/**
* Create a SoroSaveClient configured for the given network.
* Falls back to env vars for backwards compatibility when no network is provided.
*/
export function createSoroSaveClient(network?: NetworkConfig): SoroSaveClient {
const config = network ?? getNetworkConfig(loadSelectedNetwork());

export const sorosaveClient = new SoroSaveClient({
contractId: CONTRACT_ID,
rpcUrl: TESTNET_RPC_URL,
networkPassphrase: NETWORK_PASSPHRASE,
});
return new SoroSaveClient({
contractId: config.contractId,
rpcUrl: config.rpcUrl,
networkPassphrase: config.networkPassphrase,
});
}

export { TESTNET_RPC_URL, NETWORK_PASSPHRASE, CONTRACT_ID };
// Default client — uses the persisted network selection.
// Components that are network-aware should prefer calling createSoroSaveClient()
// with the network from useNetwork() instead.
export const sorosaveClient = createSoroSaveClient();

// Re-export for backwards compatibility
const fallbackConfig = getNetworkConfig(loadSelectedNetwork());
export const TESTNET_RPC_URL = fallbackConfig.rpcUrl;
export const NETWORK_PASSPHRASE = fallbackConfig.networkPassphrase;
export const CONTRACT_ID = fallbackConfig.contractId;