Skip to content

Commit 951087d

Browse files
committed
feat: implement multi-wallet provider and network auto-detection
1 parent 72dcaa2 commit 951087d

2 files changed

Lines changed: 498 additions & 137 deletions

File tree

frontend/src/components/layout/WalletGate.tsx

Lines changed: 142 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
'use client';
22

3-
import { useWallet } from '@/contexts/WalletContext';
4-
import { AlertCircle, Wallet } from 'lucide-react';
3+
import { useWallet, StellarNetwork } from '@/contexts/WalletContext';
4+
import { AlertCircle, CheckCircle2, Download, ExternalLink, Globe, RefreshCw, ShieldAlert, Wallet } from 'lucide-react';
55
import { motion } from 'framer-motion';
66
import { useEffect, useState } from 'react';
77
import { usePathname } from 'next/navigation';
88

99
export default function WalletGate({ children }: { children: React.ReactNode }) {
10-
const { isConnected, connect, isConnecting, availableWallets, activeWallet } = useWallet();
10+
const {
11+
isConnected,
12+
connect,
13+
isConnecting,
14+
availableWallets,
15+
activeWallet,
16+
activeNetwork,
17+
walletNetwork,
18+
isNetworkDivergent,
19+
blockHeight,
20+
switchNetwork,
21+
setAppNetwork,
22+
} = useWallet();
23+
1124
const pathname = usePathname();
1225
const [isMounted, setIsMounted] = useState(false);
1326

1427
useEffect(() => {
1528
setIsMounted(true);
1629
}, []);
1730

18-
// Public routes (landing, offline recovery, auth) must always render — on the
19-
// server AND the client — so they never depend on mount state or localStorage.
20-
// Gated routes stay blocked until mounted to avoid a hydration flicker / a
21-
// leaked localStorage wallet bypassing the gate before React hydrates.
2231
const isPublicRoute =
2332
pathname === '/' || pathname === '/offline' || pathname?.startsWith('/auth/');
2433

@@ -33,57 +42,145 @@ export default function WalletGate({ children }: { children: React.ReactNode })
3342
const hasLocalWallet = typeof window !== 'undefined' && !!localStorage.getItem('stellar_wallet');
3443

3544
if (isConnected || hasLocalWallet) {
36-
return <>{children}</>;
45+
return (
46+
<div className="relative min-h-screen flex flex-col">
47+
{isNetworkDivergent && (
48+
<div className="bg-amber-500/15 border-b border-amber-500/30 p-3 px-4 text-amber-200 flex flex-wrap items-center justify-between gap-3 text-sm z-50">
49+
<div className="flex items-center gap-2">
50+
<ShieldAlert className="h-5 w-5 text-amber-400 shrink-0" />
51+
<span>
52+
<strong>Network Mismatch Detected:</strong> Application target is{' '}
53+
<span className="font-semibold underline">{activeNetwork}</span> but connected wallet is on{' '}
54+
<span className="font-semibold underline">{walletNetwork}</span>.
55+
</span>
56+
</div>
57+
<div className="flex items-center gap-2">
58+
<button
59+
onClick={() => switchNetwork(activeNetwork)}
60+
className="px-3 py-1 bg-amber-500 text-black font-semibold rounded-md text-xs hover:bg-amber-400 transition-colors"
61+
>
62+
Switch Wallet to {activeNetwork}
63+
</button>
64+
<button
65+
onClick={() => setAppNetwork((walletNetwork as StellarNetwork) || 'TESTNET')}
66+
className="px-3 py-1 bg-neutral-800 text-white font-medium rounded-md text-xs border border-amber-500/30 hover:bg-neutral-700 transition-colors"
67+
>
68+
Align App to {walletNetwork}
69+
</button>
70+
</div>
71+
</div>
72+
)}
73+
<main className="flex-1">{children}</main>
74+
</div>
75+
);
3776
}
3877

3978
return (
4079
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
41-
<motion.div
80+
<motion.div
4281
initial={{ opacity: 0, y: 20 }}
4382
animate={{ opacity: 1, y: 0 }}
44-
className="max-w-md w-full bg-bg-secondary border border-border-theme rounded-2xl p-8 shadow-2xl"
83+
className="max-w-xl w-full bg-bg-secondary border border-border-theme rounded-2xl p-8 shadow-2xl space-y-6"
4584
>
46-
<div className="flex justify-center mb-6">
47-
<div className="bg-red-500/10 p-4 rounded-full border border-red-500/20">
48-
<Wallet className="h-12 w-12 text-red-500" />
85+
<div className="flex justify-between items-start">
86+
<div className="flex items-center gap-3">
87+
<div className="bg-red-500/10 p-3 rounded-full border border-red-500/20">
88+
<Wallet className="h-8 w-8 text-red-500" />
89+
</div>
90+
<div>
91+
<h1 className="text-xl font-black uppercase tracking-tight">
92+
Connect Web3 Wallet
93+
</h1>
94+
<p className="text-xs text-text-secondary">
95+
Select your preferred Stellar ecosystem wallet to log in.
96+
</p>
97+
</div>
98+
</div>
99+
100+
<div className="flex flex-col items-end text-xs">
101+
<div className="flex items-center gap-1.5 bg-neutral-800 border border-neutral-700 rounded-full px-3 py-1">
102+
<Globe className="h-3.5 w-3.5 text-emerald-400" />
103+
<span className="font-semibold">{activeNetwork}</span>
104+
</div>
105+
{blockHeight && (
106+
<span className="text-[10px] text-text-secondary mt-1">
107+
Block #{blockHeight.toLocaleString()}
108+
</span>
109+
)}
49110
</div>
50111
</div>
51-
52-
<h1 className="text-2xl font-black text-center uppercase tracking-tight mb-2">
53-
Authentication Required
54-
</h1>
55-
<p className="text-text-secondary text-center mb-8">
56-
You must connect your Web3 wallet to access the Web3 Student Lab platform.
57-
</p>
58112

59-
<div className="space-y-4">
60-
{availableWallets.map((wallet) => (
61-
<button
62-
key={wallet.name}
63-
onClick={() => connect(wallet.name)}
64-
disabled={isConnecting}
65-
className="w-full flex items-center justify-between p-4 rounded-xl border border-border-theme hover:border-red-500 hover:bg-red-500/5 transition-all group disabled:opacity-50"
66-
>
67-
<div className="flex items-center gap-3">
68-
<span className="text-2xl">{wallet.icon}</span>
69-
<span className="font-bold">{wallet.name}</span>
113+
<div className="space-y-3">
114+
<h2 className="text-xs font-bold uppercase tracking-wider text-text-secondary">
115+
Available & Detected Wallets
116+
</h2>
117+
{availableWallets.map((wallet) => {
118+
const installed = wallet.isInstalled();
119+
return (
120+
<div
121+
key={wallet.id}
122+
className={`flex items-center justify-between p-4 rounded-xl border transition-all ${
123+
installed
124+
? 'border-border-theme hover:border-red-500 hover:bg-red-500/5 cursor-pointer'
125+
: 'border-neutral-800 bg-neutral-950/40 opacity-75'
126+
}`}
127+
>
128+
<div className="flex items-center gap-3">
129+
<span className="text-2xl">{wallet.icon}</span>
130+
<div>
131+
<div className="flex items-center gap-2">
132+
<span className="font-bold text-sm">{wallet.name}</span>
133+
{installed ? (
134+
<span className="text-[10px] bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full flex items-center gap-1">
135+
<CheckCircle2 className="h-3 w-3" /> Detected
136+
</span>
137+
) : (
138+
<span className="text-[10px] bg-neutral-800 text-neutral-400 px-2 py-0.5 rounded-full">
139+
Not Installed
140+
</span>
141+
)}
142+
</div>
143+
<p className="text-xs text-text-secondary mt-0.5">{wallet.description}</p>
144+
</div>
145+
</div>
146+
147+
{installed ? (
148+
<button
149+
onClick={() => connect(wallet.name)}
150+
disabled={isConnecting}
151+
className="px-4 py-2 bg-red-600 hover:bg-red-500 text-white font-bold text-xs rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5"
152+
>
153+
{isConnecting && activeWallet === wallet.name ? (
154+
<>
155+
<RefreshCw className="h-3.5 w-3.5 animate-spin" />
156+
Connecting...
157+
</>
158+
) : (
159+
'Connect →'
160+
)}
161+
</button>
162+
) : (
163+
<a
164+
href={wallet.downloadUrl}
165+
target="_blank"
166+
rel="noopener noreferrer"
167+
className="px-3 py-1.5 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 text-xs font-medium rounded-lg text-neutral-300 transition-colors flex items-center gap-1 shrink-0"
168+
>
169+
<Download className="h-3.5 w-3.5" />
170+
Install
171+
<ExternalLink className="h-3 w-3 text-neutral-500" />
172+
</a>
173+
)}
70174
</div>
71-
{isConnecting && activeWallet === wallet.name ? (
72-
<span className="text-sm text-text-secondary animate-pulse">Connecting...</span>
73-
) : (
74-
<span className="text-sm text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity">
75-
Connect →
76-
</span>
77-
)}
78-
</button>
79-
))}
175+
);
176+
})}
80177
</div>
81178

82-
<div className="mt-8 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-xl flex items-start gap-3">
83-
<AlertCircle className="h-5 w-5 text-yellow-500 shrink-0 mt-0.5" />
84-
<p className="text-sm text-yellow-200/80">
85-
If you don't have a wallet installed, we recommend installing the Freighter extension for the Stellar network.
86-
</p>
179+
<div className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-xl flex items-start gap-3 text-xs text-yellow-200/80">
180+
<AlertCircle className="h-4 w-4 text-yellow-500 shrink-0 mt-0.5" />
181+
<span>
182+
Targeting <strong>{activeNetwork}</strong>. If your wallet is on a different network, you will be prompted to switch seamlessly.
183+
</span>
87184
</div>
88185
</motion.div>
89186
</div>

0 commit comments

Comments
 (0)