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

import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
import { connectWallet, getPublicKey, isFreighterInstalled } from "@/lib/wallet";
import { getCurrentNetwork, type NetworkType } from "@/lib/sorosave";

interface WalletContextType {
address: string | null;
isConnected: boolean;
isFreighterAvailable: boolean;
network: NetworkType;
connect: () => Promise<void>;
disconnect: () => void;
}
Expand All @@ -15,6 +17,7 @@ const WalletContext = createContext<WalletContextType>({
address: null,
isConnected: false,
isFreighterAvailable: false,
network: "testnet",
connect: async () => {},
disconnect: () => {},
});
Expand All @@ -26,9 +29,12 @@ export function useWallet() {
export function Providers({ children }: { children: React.ReactNode }) {
const [address, setAddress] = useState<string | null>(null);
const [isFreighterAvailable, setIsFreighterAvailable] = useState(false);
const [network, setNetwork] = useState<NetworkType>("testnet");

useEffect(() => {
isFreighterInstalled().then(setIsFreighterAvailable);
setNetwork(getCurrentNetwork());

// Try to reconnect on load
getPublicKey().then((key) => {
if (key) setAddress(key);
Expand All @@ -50,6 +56,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
address,
isConnected: !!address,
isFreighterAvailable,
network,
connect,
disconnect,
}}
Expand Down
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 { NetworkSwitcher } from "./NetworkSwitcher";

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">
<NetworkSwitcher />
<ConnectWallet />
</div>
</div>
</div>
</nav>
Expand Down
92 changes: 92 additions & 0 deletions src/components/NetworkSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { useState, useEffect } from "react";
import { getCurrentNetwork, switchNetwork, type NetworkType } from "@/lib/sorosave";

export function NetworkSwitcher() {
const [network, setNetwork] = useState<NetworkType>("testnet");
const [showConfirm, setShowConfirm] = useState(false);
const [pendingNetwork, setPendingNetwork] = useState<NetworkType | null>(null);

useEffect(() => {
setNetwork(getCurrentNetwork());
}, []);

const handleNetworkChange = (newNetwork: NetworkType) => {
if (newNetwork === network) return;
setPendingNetwork(newNetwork);
setShowConfirm(true);
};

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

// Clear cache data
if (typeof window !== "undefined") {
const keysToRemove = Object.keys(localStorage).filter(
(key) => key.startsWith("sorosave_") && key !== "sorosave_network"
);
keysToRemove.forEach((key) => localStorage.removeItem(key));
sessionStorage.clear();
}

switchNetwork(pendingNetwork);
setNetwork(pendingNetwork);
setShowConfirm(false);
setPendingNetwork(null);

// Reload page to reinitialize with new network
window.location.reload();
};

const cancelSwitch = () => {
setShowConfirm(false);
setPendingNetwork(null);
};

return (
<>
<div className="relative">
<select
value={network}
onChange={(e) => handleNetworkChange(e.target.value as NetworkType)}
className="appearance-none bg-gray-100 border border-gray-300 rounded-md px-4 py-2 pr-8 text-sm font-medium text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="testnet">Testnet</option>
<option value="mainnet">Mainnet</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<svg className="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
</svg>
</div>
</div>

{showConfirm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md mx-4 shadow-xl">
<h3 className="text-lg font-semibold mb-2">Switch Network?</h3>
<p className="text-gray-600 mb-4">
Switching to <strong>{pendingNetwork}</strong> will clear all cached data and reload the page.
Are you sure you want to continue?
</p>
<div className="flex justify-end space-x-3">
<button
onClick={cancelSwitch}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={confirmSwitch}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700"
>
Switch Network
</button>
</div>
</div>
</div>
)}
</>
);
}
77 changes: 66 additions & 11 deletions src/lib/sorosave.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,70 @@
import { SoroSaveClient } from "@sorosave/sdk";

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 || "";

export const sorosaveClient = new SoroSaveClient({
contractId: CONTRACT_ID,
rpcUrl: TESTNET_RPC_URL,
networkPassphrase: NETWORK_PASSPHRASE,
export type NetworkType = "testnet" | "mainnet";

export interface NetworkConfig {
rpcUrl: string;
networkPassphrase: string;
contractId: string;
}

const NETWORK_CONFIGS: Record<NetworkType, NetworkConfig> = {
testnet: {
rpcUrl: process.env.NEXT_PUBLIC_TESTNET_RPC_URL || "https://soroban-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
contractId: process.env.NEXT_PUBLIC_TESTNET_CONTRACT_ID || "",
},
mainnet: {
rpcUrl: process.env.NEXT_PUBLIC_MAINNET_RPC_URL || "https://soroban-mainnet.stellar.org",
networkPassphrase: "Public Global Stellar Network ; September 2015",
contractId: process.env.NEXT_PUBLIC_MAINNET_CONTRACT_ID || "",
},
};

let currentClient: SoroSaveClient;
let currentNetwork: NetworkType = "testnet";

// Initialize with testnet by default
if (typeof window !== "undefined") {
const savedNetwork = localStorage.getItem("sorosave_network") as NetworkType | null;
currentNetwork = savedNetwork || "testnet";
}

const config = NETWORK_CONFIGS[currentNetwork];
currentClient = new SoroSaveClient({
contractId: config.contractId,
rpcUrl: config.rpcUrl,
networkPassphrase: config.networkPassphrase,
});

export { TESTNET_RPC_URL, NETWORK_PASSPHRASE, CONTRACT_ID };
export function getCurrentNetwork(): NetworkType {
return currentNetwork;
}

export function switchNetwork(network: NetworkType): void {
currentNetwork = network;
if (typeof window !== "undefined") {
localStorage.setItem("sorosave_network", network);
}

const config = NETWORK_CONFIGS[network];
currentClient = new SoroSaveClient({
contractId: config.contractId,
rpcUrl: config.rpcUrl,
networkPassphrase: config.networkPassphrase,
});
}

export function getSoroSaveClient(): SoroSaveClient {
return currentClient;
}

export function getNetworkConfig(network: NetworkType): NetworkConfig {
return NETWORK_CONFIGS[network];
}

// Legacy exports for backward compatibility
export const sorosaveClient = currentClient;
export const TESTNET_RPC_URL = NETWORK_CONFIGS.testnet.rpcUrl;
export const NETWORK_PASSPHRASE = NETWORK_CONFIGS.testnet.networkPassphrase;
export const CONTRACT_ID = NETWORK_CONFIGS.testnet.contractId;