Skip to content

Commit 57b6500

Browse files
authored
Merge pull request #120 from dijangh904/solved
Add mainnet support with network switcher
2 parents 0e22a94 + 6fef1b1 commit 57b6500

8 files changed

Lines changed: 339 additions & 131 deletions

File tree

‎.env.local.example‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
# ─── Stellar contract ─────────────────────────────────────────────────────────
88

9-
# Deployed streaming contract address on Stellar.
9+
# Deployed streaming contract addresses on Stellar.
1010
#
11-
# When this is set, FlowStar talks to the live Soroban contract on testnet.
12-
# When it is empty (or this file is not copied), the app falls back to an
13-
# in-memory MOCK mode — streams are stored in memory only and reset on reload.
11+
# When a contract ID is set for the active network, FlowStar talks to the live
12+
# Soroban contract on that network. When no contract ID is configured, the app
13+
# falls back to an in-memory MOCK mode — streams are stored in memory only and
14+
# reset on reload.
1415
#
1516
# Deploy your own with `./scripts/deploy.sh --update-env` (see DEPLOYMENT.md),
1617
# or use the public testnet deployment below to get started immediately.
17-
NEXT_PUBLIC_STREAM_CONTRACT_ID=CBNDCZTRFNTDAPQLPK2ESOKO4XFMSC4PX37QE75BBYFOYIEWIPMHAKFV
18+
NEXT_PUBLIC_STREAM_CONTRACT_ID_TESTNET=CBNDCZTRFNTDAPQLPK2ESOKO4XFMSC4PX37QE75BBYFOYIEWIPMHAKFV
19+
NEXT_PUBLIC_STREAM_CONTRACT_ID_MAINNET=

‎app/layout.tsx‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Analytics } from '@vercel/analytics/next'
22
import type { Metadata, Viewport } from 'next'
33
import { Geist, Geist_Mono } from 'next/font/google'
4+
import { ThemeProvider } from 'next-themes'
5+
import { NetworkProvider } from '@/components/providers/network-provider'
46
import { WalletProvider } from '@/components/providers/wallet-provider'
57
import { Toaster } from '@/components/ui/sonner'
68
import './globals.css'
@@ -44,7 +46,6 @@ export const metadata: Metadata = {
4446
}
4547

4648
export const viewport: Viewport = {
47-
colorScheme: 'dark',
4849
themeColor: '#0c1014',
4950
}
5051

@@ -57,13 +58,23 @@ export default function RootLayout({
5758
<html
5859
lang="en"
5960
className={`${geistSans.variable} ${geistMono.variable} bg-background`}
61+
suppressHydrationWarning
6062
>
6163
<body className="font-sans antialiased">
62-
<WalletProvider>
63-
{children}
64-
<Toaster position="top-right" />
65-
</WalletProvider>
66-
{process.env.NODE_ENV === 'production' && <Analytics />}
64+
<ThemeProvider
65+
attribute="class"
66+
defaultTheme="system"
67+
enableSystem
68+
disableTransitionOnChange
69+
>
70+
<NetworkProvider>
71+
<WalletProvider>
72+
{children}
73+
<Toaster position="top-right" />
74+
</WalletProvider>
75+
</NetworkProvider>
76+
{process.env.NODE_ENV === 'production' && <Analytics />}
77+
</ThemeProvider>
6778
</body>
6879
</html>
6980
)

‎components/layout/mock-mode-banner.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
'use client'
22

33
import { FlaskConical } from 'lucide-react'
4-
import { IS_MOCK_MODE } from '@/lib/stellar'
4+
import { useNetwork } from '@/components/providers/network-provider'
55

66
/**
77
* Dev-only indicator shown when the app runs against the in-memory mock store
8-
* (i.e. NEXT_PUBLIC_STREAM_CONTRACT_ID is unset). Hidden in production builds so
8+
* (i.e. NEXT_PUBLIC_STREAM_CONTRACT_ID_{NETWORK} is unset). Hidden in production builds so
99
* it never leaks into a real deployment.
1010
*/
1111
export function MockModeBanner() {
12-
if (process.env.NODE_ENV === 'production' || !IS_MOCK_MODE) return null
12+
const { isMockMode } = useNetwork()
13+
if (process.env.NODE_ENV === 'production' || !isMockMode) return null
1314

1415
return (
1516
<div
1617
role="status"
17-
title="No contract configured — set NEXT_PUBLIC_STREAM_CONTRACT_ID in .env.local to use the live contract."
18+
title="No contract configured — set NEXT_PUBLIC_STREAM_CONTRACT_ID_{NETWORK} in .env.local to use the live contract."
1819
className="fixed bottom-4 left-4 z-50 flex items-center gap-1.5 rounded-full border border-amber-500/40 bg-amber-500/10 px-3 py-1.5 text-xs font-medium text-amber-400 backdrop-blur-md"
1920
>
2021
<FlaskConical className="size-3.5" />

‎components/layout/navbar.tsx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
import Link from 'next/link'
44
import { usePathname } from 'next/navigation'
5-
import { Plus } from 'lucide-react'
5+
import { Plus, Moon, Sun, Monitor } from 'lucide-react'
6+
import { useTheme } from 'next-themes'
67
import { Brand } from '@/components/brand'
78
import { Button } from '@/components/ui/button'
9+
import {
10+
DropdownMenu,
11+
DropdownMenuContent,
12+
DropdownMenuItem,
13+
DropdownMenuTrigger,
14+
} from '@/components/ui/dropdown-menu'
815
import { ConnectWalletButton } from '@/components/layout/connect-wallet-button'
916
import { NotificationBell } from '@/components/layout/notification-bell'
1017
import { cn } from '@/lib/utils'
@@ -16,6 +23,7 @@ const NAV_LINKS = [
1623

1724
export function Navbar() {
1825
const pathname = usePathname()
26+
const { setTheme, theme } = useTheme()
1927

2028
return (
2129
<header className="sticky top-0 z-40 border-b border-border bg-background/80 backdrop-blur-xl">
@@ -52,6 +60,31 @@ export function Navbar() {
5260
<span className="hidden sm:inline">New stream</span>
5361
</Link>
5462
</Button>
63+
64+
<DropdownMenu>
65+
<DropdownMenuTrigger asChild>
66+
<Button variant="ghost" size="icon">
67+
<Sun className="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
68+
<Moon className="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
69+
<span className="sr-only">Toggle theme</span>
70+
</Button>
71+
</DropdownMenuTrigger>
72+
<DropdownMenuContent align="end">
73+
<DropdownMenuItem onClick={() => setTheme('light')}>
74+
<Sun className="mr-2 size-4" />
75+
Light
76+
</DropdownMenuItem>
77+
<DropdownMenuItem onClick={() => setTheme('dark')}>
78+
<Moon className="mr-2 size-4" />
79+
Dark
80+
</DropdownMenuItem>
81+
<DropdownMenuItem onClick={() => setTheme('system')}>
82+
<Monitor className="mr-2 size-4" />
83+
System
84+
</DropdownMenuItem>
85+
</DropdownMenuContent>
86+
</DropdownMenu>
87+
5588
<NotificationBell />
5689
<ConnectWalletButton />
5790
</div>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use client'
2+
3+
import {
4+
createContext,
5+
useCallback,
6+
useContext,
7+
useEffect,
8+
useMemo,
9+
useState,
10+
type ReactNode,
11+
} from 'react'
12+
import { type NetworkName, getNetworkConfig } from '@/lib/stellar'
13+
14+
const NETWORK_KEY = 'flowstar:network'
15+
16+
interface NetworkContextValue {
17+
network: NetworkName
18+
setNetwork: (network: NetworkName) => void
19+
config: ReturnType<typeof getNetworkConfig>
20+
isMockMode: boolean
21+
}
22+
23+
const NetworkContext = createContext<NetworkContextValue | null>(null)
24+
25+
export function NetworkProvider({ children }: { children: ReactNode }) {
26+
const [network, setNetworkState] = useState<NetworkName>('testnet')
27+
const [mounted, setMounted] = useState(false)
28+
29+
useEffect(() => {
30+
setMounted(true)
31+
const saved = localStorage.getItem(NETWORK_KEY) as NetworkName | null
32+
if (saved && ['testnet', 'mainnet'].includes(saved)) {
33+
setNetworkState(saved)
34+
}
35+
}, [])
36+
37+
const setNetwork = useCallback((newNetwork: NetworkName) => {
38+
setNetworkState(newNetwork)
39+
localStorage.setItem(NETWORK_KEY, newNetwork)
40+
}, [])
41+
42+
const config = useMemo(() => getNetworkConfig(network), [network])
43+
const isMockMode = !config.streamContractId
44+
45+
// Startup check: warn (in non-production) when the contract ID is missing
46+
useEffect(() => {
47+
if (mounted && isMockMode && process.env.NODE_ENV !== 'production') {
48+
console.warn(
49+
`[FlowStar] No contract ID configured for ${network} — running in MOCK mode. ` +
50+
'Streams are kept in memory only and reset on reload. ' +
51+
'Copy .env.local.example to .env.local and set the appropriate contract ID.',
52+
)
53+
}
54+
}, [network, isMockMode, mounted])
55+
56+
const value = useMemo<NetworkContextValue>(
57+
() => ({
58+
network,
59+
setNetwork,
60+
config,
61+
isMockMode,
62+
}),
63+
[network, setNetwork, config, isMockMode],
64+
)
65+
66+
return <NetworkContext.Provider value={value}>{children}</NetworkContext.Provider>
67+
}
68+
69+
export function useNetwork() {
70+
const ctx = useContext(NetworkContext)
71+
if (!ctx) throw new Error('useNetwork must be used within a NetworkProvider')
72+
return ctx
73+
}

‎components/providers/wallet-provider.tsx‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
type ReactNode,
1111
} from 'react'
1212
import { setSignTransaction } from '@/lib/contract'
13-
import { NETWORK } from '@/lib/stellar'
13+
import { type NetworkName, getNetworkConfig } from '@/lib/stellar'
14+
import { useNetwork } from './network-provider'
1415

1516
// ─── Wallet options ───────────────────────────────────────────────────────────
1617

@@ -36,7 +37,7 @@ interface WalletContextValue {
3637
isConnected: boolean
3738
connect: (walletId: string) => Promise<void>
3839
disconnect: () => void
39-
signTransaction: (xdr: string) => Promise<string>
40+
signTransaction: (xdr: string, network?: NetworkName) => Promise<string>
4041
}
4142

4243
const WalletContext = createContext<WalletContextValue | null>(null)
@@ -56,10 +57,10 @@ async function connectFreighter(): Promise<string> {
5657
return result.address
5758
}
5859

59-
async function signWithFreighter(xdr: string): Promise<string> {
60+
async function signWithFreighter(xdr: string, networkPassphrase: string): Promise<string> {
6061
const { signTransaction } = await import('@stellar/freighter-api')
6162
const result = await signTransaction(xdr, {
62-
networkPassphrase: NETWORK.passphrase,
63+
networkPassphrase,
6364
})
6465
if (result.error) throw new Error(result.error)
6566
return result.signedTxXdr
@@ -82,19 +83,13 @@ async function connectWallet(id: string): Promise<string> {
8283
}
8384
}
8485

85-
async function signWithWallet(id: string, xdr: string): Promise<string> {
86-
switch (id) {
87-
case 'freighter': return signWithFreighter(xdr)
88-
default: throw new Error(`Signing not implemented for ${id}`)
89-
}
90-
}
91-
9286
// ─── Provider ─────────────────────────────────────────────────────────────────
9387

9488
export function WalletProvider({ children }: { children: ReactNode }) {
9589
const [address, setAddress] = useState<string | null>(null)
9690
const [walletId, setWalletId] = useState<string | null>(null)
9791
const [connecting, setConnecting] = useState(false)
92+
const { network } = useNetwork()
9893

9994
const connect = useCallback(async (id: string) => {
10095
setConnecting(true)
@@ -113,16 +108,20 @@ export function WalletProvider({ children }: { children: ReactNode }) {
113108
}, [])
114109

115110
const signTransaction = useCallback(
116-
async (xdr: string): Promise<string> => {
111+
async (xdr: string, customNetwork?: NetworkName): Promise<string> => {
117112
if (!walletId) throw new Error('No wallet connected')
118-
return signWithWallet(walletId, xdr)
113+
const config = getNetworkConfig(customNetwork || network)
114+
switch (walletId) {
115+
case 'freighter': return signWithFreighter(xdr, config.passphrase)
116+
default: throw new Error(`Signing not implemented for ${walletId}`)
117+
}
119118
},
120-
[walletId],
119+
[walletId, network],
121120
)
122121

123122
// Keep contract layer in sync
124123
useEffect(() => {
125-
setSignTransaction(signTransaction)
124+
setSignTransaction((xdr: string) => signTransaction(xdr))
126125
}, [signTransaction])
127126

128127
const value = useMemo<WalletContextValue>(

0 commit comments

Comments
 (0)