Files
smom-dbis-138/frontend-dapp/src/components/wallet/WalletConnect.tsx
2026-03-28 15:38:51 -07:00

169 lines
7.4 KiB
TypeScript

import { useAccount, useConnect, useDisconnect, useChainId, useSwitchChain } from 'wagmi'
import { useEffect, useState, useRef } from 'react'
import {
defaultFrontendChainId,
defaultFrontendChainName,
defaultFrontendExplorerUrl,
frontendSourceChainIds,
} from '../../config/networks'
interface WalletConnectProps {
/** Callback before disconnect so we can treat it as user-initiated (no "disconnected" toast). */
onBeforeDisconnect?: () => void
}
export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps) {
const { address, isConnected } = useAccount()
const { connect, connectors, isPending } = useConnect()
const { disconnect } = useDisconnect()
const chainId = useChainId()
const { switchChain, isPending: isSwitching } = useSwitchChain()
const [showChainWarning, setShowChainWarning] = useState(false)
const [showConnectModal, setShowConnectModal] = useState(false)
const [showDropdown, setShowDropdown] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isConnected && !frontendSourceChainIds.includes(chainId)) {
setShowChainWarning(true)
} else {
setShowChainWarning(false)
}
}, [isConnected, chainId])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setShowDropdown(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handleSwitchChain = async () => {
try {
await switchChain({ chainId: defaultFrontendChainId })
} catch (error) {
console.error('Failed to switch chain:', error)
}
}
const handleConnect = (connector: (typeof connectors)[0]) => {
connect({ connector })
setShowConnectModal(false)
}
const copyAddress = () => {
if (address) {
navigator.clipboard.writeText(address)
setShowDropdown(false)
}
}
const viewOnExplorer = () => {
if (address) window.open(`${defaultFrontendExplorerUrl}/address/${address}`, '_blank', 'noopener')
setShowDropdown(false)
}
if (isConnected) {
return (
<div className="flex items-center gap-3 flex-shrink-0">
{showChainWarning && (
<button
onClick={handleSwitchChain}
disabled={isSwitching}
className="px-4 py-2 text-sm font-medium bg-amber-500/20 text-amber-200 rounded-lg hover:bg-amber-500/30 border border-amber-500/40 disabled:opacity-50 transition-colors flex items-center gap-2"
>
{isSwitching ? (
<>
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Switching...
</>
) : (
`Switch to ${defaultFrontendChainName}`
)}
</button>
)}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-2 px-4 py-2.5 bg-[#1a1d24] rounded-xl border border-white/20 hover:border-white/30 transition-colors min-h-[44px] w-full md:w-auto"
aria-expanded={showDropdown}
aria-haspopup="true"
>
<div className="h-2 w-2 bg-emerald-400 rounded-full" />
<span className="text-sm font-medium text-white font-mono">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<svg className={`w-4 h-4 text-[#A0A0A0] transition-transform ${showDropdown ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showDropdown && (
<div className="absolute right-0 mt-1 py-1 w-48 bg-[#252830] rounded-xl border border-white/10 shadow-xl z-50">
<button onClick={copyAddress} className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-white/5 transition-colors">
Copy address
</button>
<button onClick={viewOnExplorer} className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-white/5 transition-colors">
View on Explorer
</button>
<button
onClick={() => { onBeforeDisconnect?.(); disconnect(); setShowDropdown(false) }}
className="w-full px-4 py-2.5 text-left text-sm text-red-400 hover:bg-white/5 transition-colors"
>
Disconnect
</button>
</div>
)}
</div>
</div>
)
}
return (
<>
<button
onClick={() => setShowConnectModal(true)}
className="px-6 py-3 bg-teal-600 text-white font-medium rounded-xl hover:bg-teal-500 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] min-h-[44px] w-full md:w-auto"
>
Connect Wallet
</button>
{showConnectModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60" onClick={() => setShowConnectModal(false)}>
<div className="bg-[#252830] rounded-2xl border border-white/10 shadow-2xl w-full max-w-md p-6" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-white">Connect Wallet</h2>
<button onClick={() => setShowConnectModal(false)} className="p-2 text-[#A0A0A0] hover:text-white rounded-lg hover:bg-white/5 transition-colors" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div className="space-y-2">
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => handleConnect(connector)}
disabled={isPending}
className="w-full px-4 py-3 rounded-xl bg-[#1a1d24] border border-white/10 text-white font-medium hover:bg-white/5 hover:border-white/20 disabled:opacity-50 transition-colors text-left flex items-center justify-between"
>
<span>{connector.name}</span>
{isPending && (
<svg className="animate-spin h-5 w-5 text-teal-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
)}
</button>
))}
</div>
</div>
</div>
)}
</>
)
}