|
1 | | -// eslint-disable-next-line @typescript-eslint/ban-ts-comment |
2 | | -// @ts-nocheck |
3 | | -'use client'; |
4 | | - |
5 | | -import React, { useCallback, useState } from 'react'; |
6 | | -import { Wallet, LogOut, AlertCircle, Loader2, Copy, Check, ChevronDown } from 'lucide-react'; |
7 | | -import { useWeb3Wallet, type WalletProvider } from '@/hooks/useWeb3Wallet'; |
| 1 | +import React, { useState, useEffect } from 'react'; |
| 2 | +import { scanWalletDependencies, WalletDetectionResult } from '../../utils/web3/walletDetection'; |
8 | 3 |
|
9 | 4 | interface WalletConnectorProps { |
10 | | - className?: string; |
11 | | - showBalance?: boolean; |
12 | | - onConnect?: (address: string, provider: WalletProvider) => void; |
13 | | - onDisconnect?: () => void; |
14 | | - /** Optional flag to expose Service Account provider */ |
15 | | - showServiceAccount?: boolean; |
| 5 | + onConnect?: (walletType: string) => void; |
16 | 6 | } |
17 | 7 |
|
18 | | -/** |
19 | | - * WalletConnector Component |
20 | | - * |
21 | | - * Provides seamless multi-wallet connection experience with support for: |
22 | | - * - MetaMask |
23 | | - * - Starknet (ArgentX, Braavos) |
24 | | - * - WalletConnect |
25 | | - * - Coinbase Wallet |
26 | | - * |
27 | | - * Features: |
28 | | - * - Easy switching between providers |
29 | | - * - Address copy-to-clipboard |
30 | | - * - Connection status display |
31 | | - * - Error handling and recovery |
32 | | - * - Responsive design |
33 | | - */ |
34 | | -export const WalletConnector: React.FC<WalletConnectorProps> = ({ |
35 | | - className = '', |
36 | | - showBalance = false, |
37 | | - onConnect, |
38 | | - onDisconnect, |
39 | | - showServiceAccount, |
40 | | -}) => { |
41 | | - const wallet = useWeb3Wallet(); |
42 | | - const [isDropdownOpen, setIsDropdownOpen] = useState(false); |
43 | | - const [copiedAddress, setCopiedAddress] = useState(false); |
44 | | - |
45 | | - const walletProviders: { id: WalletProvider; name: string; description: string }[] = [ |
46 | | - { id: 'metamask', name: 'MetaMask', description: 'Connect using MetaMask extension' }, |
47 | | - { id: 'starknet', name: 'Starknet', description: 'Connect using ArgentX or Braavos' }, |
48 | | - ...(showServiceAccount |
49 | | - ? [ |
50 | | - { |
51 | | - id: 'service', |
52 | | - name: 'Service Account', |
53 | | - description: 'Connect using backend service account', |
54 | | - }, |
55 | | - ] |
56 | | - : []), |
57 | | - ]; |
58 | | - |
59 | | - /** |
60 | | - * Handle wallet connection |
61 | | - */ |
62 | | - const handleConnect = useCallback( |
63 | | - async (provider: WalletProvider) => { |
64 | | - try { |
65 | | - const result = await wallet.connect(provider); |
66 | | - setIsDropdownOpen(false); |
67 | | - onConnect?.(result.address, result.provider); |
68 | | - } catch (error) { |
69 | | - console.error('[WalletConnector] Connection failed:', error); |
70 | | - } |
71 | | - }, |
72 | | - [wallet, onConnect], |
73 | | - ); |
74 | | - |
75 | | - /** |
76 | | - * Handle wallet disconnection |
77 | | - */ |
78 | | - const handleDisconnect = useCallback(async () => { |
79 | | - await wallet.disconnect(); |
80 | | - setIsDropdownOpen(false); |
81 | | - onDisconnect?.(); |
82 | | - }, [wallet, onDisconnect]); |
83 | | - |
84 | | - /** |
85 | | - * Copy address to clipboard |
86 | | - */ |
87 | | - const handleCopyAddress = useCallback(async () => { |
88 | | - if (!wallet.address) return; |
89 | | - |
90 | | - try { |
91 | | - await navigator.clipboard.writeText(wallet.address); |
92 | | - setCopiedAddress(true); |
93 | | - setTimeout(() => setCopiedAddress(false), 2000); |
94 | | - } catch (error) { |
95 | | - console.error('[WalletConnector] Failed to copy address:', error); |
| 8 | +export const WalletConnector: React.FC<WalletConnectorProps> = ({ onConnect }) => { |
| 9 | + const [scanResult, setScanResult] = useState<WalletDetectionResult | null>(null); |
| 10 | + const [isScanning, setIsScanning] = useState(true); |
| 11 | + const [isConnected, setIsConnected] = useState(false); |
| 12 | + const [selectedWallet, setSelectedWallet] = useState<string | null>(null); |
| 13 | + |
| 14 | + useEffect(() => { |
| 15 | + const scan = async () => { |
| 16 | + setIsScanning(true); |
| 17 | + // Simulate async scan |
| 18 | + await new Promise(resolve => setTimeout(resolve, 500)); |
| 19 | + const result = scanWalletDependencies(); |
| 20 | + setScanResult(result); |
| 21 | + setIsScanning(false); |
| 22 | + }; |
| 23 | + scan(); |
| 24 | + }, []); |
| 25 | + |
| 26 | + const handleConnect = async (walletType: string) => { |
| 27 | + setSelectedWallet(walletType); |
| 28 | + setIsConnected(true); |
| 29 | + if (onConnect) { |
| 30 | + onConnect(walletType); |
96 | 31 | } |
97 | | - }, [wallet.address]); |
98 | | - |
99 | | - /** |
100 | | - * Format address for display |
101 | | - */ |
102 | | - const formatAddress = (address: string, chars = 4): string => { |
103 | | - return `${address.slice(0, chars)}...${address.slice(-chars)}`; |
104 | 32 | }; |
105 | 33 |
|
106 | | - // Not connected state |
107 | | - if (!wallet.isConnected) { |
| 34 | + if (isScanning) { |
108 | 35 | return ( |
109 | | - <div className={`relative ${className}`}> |
110 | | - <div className="relative"> |
111 | | - <button |
112 | | - onClick={() => setIsDropdownOpen(!isDropdownOpen)} |
113 | | - disabled={wallet.isConnecting} |
114 | | - className="px-4 py-2.5 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:from-gray-400 disabled:to-gray-500 text-white font-medium rounded-lg transition-all flex items-center gap-2 shadow-lg hover:shadow-xl disabled:shadow-md" |
115 | | - aria-label="Connect wallet" |
116 | | - aria-expanded={isDropdownOpen} |
117 | | - > |
118 | | - {wallet.isConnecting ? ( |
119 | | - <> |
120 | | - <Loader2 className="w-4 h-4 animate-spin" /> |
121 | | - <span className="hidden sm:inline">Connecting...</span> |
122 | | - </> |
123 | | - ) : ( |
124 | | - <> |
125 | | - <Wallet className="w-4 h-4" /> |
126 | | - <span className="hidden sm:inline">Connect Wallet</span> |
127 | | - <span className="sm:hidden">Connect</span> |
128 | | - </> |
129 | | - )} |
130 | | - <ChevronDown |
131 | | - className={`w-4 h-4 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} |
132 | | - /> |
133 | | - </button> |
| 36 | + <div className="wallet-connector" role="status" aria-label="Scanning for wallets"> |
| 37 | + <div className="flex items-center justify-center p-4"> |
| 38 | + <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div> |
| 39 | + <span className="ml-2 text-gray-600">Scanning for wallets...</span> |
134 | 40 | </div> |
| 41 | + </div> |
| 42 | + ); |
| 43 | + } |
135 | 44 |
|
136 | | - {/* Dropdown menu */} |
137 | | - {isDropdownOpen && ( |
138 | | - <div className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden z-50"> |
139 | | - <div className="p-3 border-b border-gray-200 dark:border-gray-700"> |
140 | | - <h3 className="font-semibold text-gray-900 dark:text-white text-sm">Select Wallet</h3> |
141 | | - </div> |
142 | | - |
143 | | - <div className="divide-y divide-gray-200 dark:divide-gray-700"> |
144 | | - {walletProviders.map((provider) => ( |
145 | | - <button |
146 | | - key={provider.id} |
147 | | - onClick={() => handleConnect(provider.id)} |
148 | | - className="w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group" |
149 | | - aria-label={`Connect ${provider.name}`} |
150 | | - > |
151 | | - <div className="font-medium text-gray-900 dark:text-white text-sm"> |
152 | | - {provider.name} |
153 | | - </div> |
154 | | - <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
155 | | - {provider.description} |
156 | | - </div> |
157 | | - </button> |
158 | | - ))} |
159 | | - </div> |
160 | | - </div> |
161 | | - )} |
162 | | - |
163 | | - {/* Error message */} |
164 | | - {wallet.error && ( |
165 | | - <div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex gap-2"> |
166 | | - <AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" /> |
167 | | - <div className="flex-1"> |
168 | | - <p className="text-sm text-red-700 dark:text-red-300">{wallet.error}</p> |
169 | | - <button |
170 | | - onClick={() => wallet.clearError()} |
171 | | - className="text-xs text-red-600 dark:text-red-400 hover:underline mt-1 block" |
172 | | - > |
173 | | - Dismiss |
174 | | - </button> |
175 | | - </div> |
176 | | - </div> |
177 | | - )} |
| 45 | + if (!scanResult || scanResult.detectedWallets.length === 0) { |
| 46 | + return ( |
| 47 | + <div className="wallet-connector" role="alert" aria-label="No wallets detected"> |
| 48 | + <div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg"> |
| 49 | + <p className="text-yellow-800 font-medium">No wallets detected</p> |
| 50 | + <p className="text-yellow-600 text-sm mt-1">{scanResult?.message}</p> |
| 51 | + <a |
| 52 | + href="https://www.freighter.app/" |
| 53 | + target="_blank" |
| 54 | + rel="noopener noreferrer" |
| 55 | + className="inline-block mt-3 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600" |
| 56 | + > |
| 57 | + Install Freighter |
| 58 | + </a> |
| 59 | + </div> |
178 | 60 | </div> |
179 | 61 | ); |
180 | 62 | } |
181 | 63 |
|
182 | | - // Connected state |
183 | 64 | return ( |
184 | | - <div className={`relative ${className}`}> |
185 | | - <button |
186 | | - onClick={() => setIsDropdownOpen(!isDropdownOpen)} |
187 | | - className="px-4 py-2.5 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-medium rounded-lg transition-all flex items-center gap-2 shadow-lg hover:shadow-xl" |
188 | | - aria-label="Wallet menu" |
189 | | - aria-expanded={isDropdownOpen} |
190 | | - > |
191 | | - <div className="w-2 h-2 bg-green-300 rounded-full animate-pulse" /> |
192 | | - <span className="hidden sm:inline">{formatAddress(wallet.address || '')}</span> |
193 | | - <span className="sm:hidden">{formatAddress(wallet.address || '', 3)}</span> |
194 | | - <ChevronDown |
195 | | - className={`w-4 h-4 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} |
196 | | - /> |
197 | | - </button> |
198 | | - |
199 | | - {/* Dropdown menu */} |
200 | | - {isDropdownOpen && ( |
201 | | - <div className="absolute right-0 mt-2 w-72 bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden z-50"> |
202 | | - {/* Address section */} |
203 | | - <div className="p-4 border-b border-gray-200 dark:border-gray-700"> |
204 | | - <p className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-2"> |
205 | | - Connected Address |
206 | | - </p> |
207 | | - <div className="flex items-center gap-2"> |
208 | | - <div className="flex-1"> |
209 | | - <p className="font-mono text-sm font-medium text-gray-900 dark:text-white break-all"> |
210 | | - {wallet.address} |
211 | | - </p> |
212 | | - </div> |
| 65 | + <div className="wallet-connector" role="region" aria-label="Wallet connection"> |
| 66 | + <div className="p-4 bg-gray-50 rounded-lg"> |
| 67 | + <p className="text-sm text-gray-600 mb-3" aria-live="polite"> |
| 68 | + {scanResult.message} |
| 69 | + </p> |
| 70 | + {!isConnected ? ( |
| 71 | + <div className="space-y-2"> |
| 72 | + <p className="text-sm font-medium text-gray-700">Select a wallet to connect:</p> |
| 73 | + {scanResult.hasFreighter && ( |
213 | 74 | <button |
214 | | - onClick={handleCopyAddress} |
215 | | - className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors" |
216 | | - aria-label="Copy address" |
217 | | - title="Copy address" |
| 75 | + onClick={() => handleConnect('freighter')} |
| 76 | + className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition" |
| 77 | + aria-label="Connect Freighter wallet" |
218 | 78 | > |
219 | | - {copiedAddress ? ( |
220 | | - <Check className="w-4 h-4 text-green-600 dark:text-green-400" /> |
221 | | - ) : ( |
222 | | - <Copy className="w-4 h-4 text-gray-600 dark:text-gray-400" /> |
223 | | - )} |
| 79 | + Connect Freighter |
224 | 80 | </button> |
225 | | - </div> |
226 | | - </div> |
227 | | - |
228 | | - {/* Provider and chain info */} |
229 | | - <div className="p-4 space-y-3 border-b border-gray-200 dark:border-gray-700"> |
230 | | - {wallet.provider && ( |
231 | | - <div> |
232 | | - <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Provider</p> |
233 | | - <p className="text-sm font-medium text-gray-900 dark:text-white capitalize"> |
234 | | - {wallet.provider} |
235 | | - </p> |
236 | | - </div> |
237 | 81 | )} |
238 | | - {wallet.chainId && ( |
239 | | - <div> |
240 | | - <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Network</p> |
241 | | - <p className="text-sm font-medium text-gray-900 dark:text-white"> |
242 | | - {wallet.supportedChains[wallet.chainId]?.chainName || wallet.chainId} |
243 | | - </p> |
244 | | - </div> |
| 82 | + {scanResult.hasEthereum && ( |
| 83 | + <button |
| 84 | + onClick={() => handleConnect('ethereum')} |
| 85 | + className="w-full px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition" |
| 86 | + aria-label="Connect Ethereum wallet" |
| 87 | + > |
| 88 | + Connect Ethereum Wallet |
| 89 | + </button> |
245 | 90 | )} |
246 | 91 | </div> |
247 | | - |
248 | | - {/* Balances section */} |
249 | | - {showBalance && wallet.balances.length > 0 && ( |
250 | | - <div className="p-4 border-b border-gray-200 dark:border-gray-700"> |
251 | | - <p className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold mb-3"> |
252 | | - Balances |
253 | | - </p> |
254 | | - <div className="space-y-2"> |
255 | | - {wallet.balances.map((balance) => ( |
256 | | - <div key={balance.token} className="flex justify-between items-center text-sm"> |
257 | | - <span className="text-gray-600 dark:text-gray-400">{balance.symbol}</span> |
258 | | - <span className="font-medium text-gray-900 dark:text-white"> |
259 | | - {parseFloat(balance.balance).toFixed(4)} |
260 | | - </span> |
261 | | - </div> |
262 | | - ))} |
263 | | - </div> |
264 | | - </div> |
265 | | - )} |
266 | | - |
267 | | - {/* Disconnect button */} |
268 | | - <button |
269 | | - onClick={handleDisconnect} |
270 | | - className="w-full px-4 py-3 text-left hover:bg-red-50 dark:hover:bg-red-900/20 text-red-600 dark:text-red-400 font-medium flex items-center gap-2 transition-colors" |
271 | | - aria-label="Disconnect wallet" |
272 | | - > |
273 | | - <LogOut className="w-4 h-4" /> |
274 | | - <span>Disconnect Wallet</span> |
275 | | - </button> |
276 | | - </div> |
277 | | - )} |
| 92 | + ) : ( |
| 93 | + <div className="text-center p-2 bg-green-50 rounded-lg"> |
| 94 | + <p className="text-green-700">✓ Connected to {selectedWallet}</p> |
| 95 | + </div> |
| 96 | + )} |
| 97 | + </div> |
278 | 98 | </div> |
279 | 99 | ); |
280 | 100 | }; |
281 | | - |
282 | | -export default WalletConnector; |
0 commit comments