169 lines
7.4 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
)
|
|
}
|