Most Web3 wallet integrations are either too simple (only connect button) or built on outdated patterns. Here's a complete implementation using modern tools.
The Stack
Use wagmi + viem -- the current standard:
npm install wagmi viem @tanstack/react-query
Setup
// app/providers.tsx
'use client'
import { WagmiProvider, createConfig, http } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
import { metaMask, coinbaseWallet, walletConnect } from 'wagmi/connectors'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const config = createConfig({
chains: [mainnet, sepolia],
connectors: [
metaMask(),
coinbaseWallet({ appName: 'My App' }),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! })
],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_MAINNET_RPC!),
[sepolia.id]: http(),
}
})
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
)
}
Connect Button
'use client'
import { useConnect, useDisconnect, useAccount, useChainId, useSwitchChain } from 'wagmi'
import { mainnet } from 'wagmi/chains'
export function ConnectButton() {
const { address, isConnected } = useAccount()
const chainId = useChainId()
const { connect, connectors } = useConnect()
const { disconnect } = useDisconnect()
const { switchChain } = useSwitchChain()
if (isConnected) {
return (
<div className="flex items-center gap-2">
{chainId !== mainnet.id && (
<button
onClick={() => switchChain({ chainId: mainnet.id })}
className="px-3 py-1.5 bg-red-500 text-white rounded text-sm"
>
Wrong Network
</button>
)}
<span className="font-mono text-sm">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<button onClick={() => disconnect()} className="btn-secondary">
Disconnect
</button>
</div>
)
}
return (
<div className="flex gap-2">
{connectors.map(connector => (
<button
key={connector.id}
onClick={() => connect({ connector })}
className="btn-primary"
>
{connector.name}
</button>
))}
</div>
)
}
Reading Contract Data
import { useReadContract } from 'wagmi'
import { erc20Abi } from 'viem'
const TOKEN_ADDRESS = '0x...'
export function TokenBalance({ userAddress }: { userAddress: `0x${string}` }) {
const { data: balance, isLoading, error } = useReadContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
})
if (isLoading) return <span>Loading...</span>
if (error) return <span>Error loading balance</span>
return (
<span>
{balance ? (Number(balance) / 1e18).toFixed(4) : '0'} MTK
</span>
)
}
Writing to Contracts
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { parseEther } from 'viem'
export function DepositButton() {
const { writeContract, data: hash, isPending, error } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
const deposit = () => {
writeContract({
address: VAULT_ADDRESS,
abi: vaultAbi,
functionName: 'deposit',
value: parseEther('0.01'),
})
}
return (
<div>
<button
onClick={deposit}
disabled={isPending || isConfirming}
className="btn-primary"
>
{isPending ? 'Confirm in MetaMask...' :
isConfirming ? 'Waiting for confirmation...' :
'Deposit 0.01 ETH'}
</button>
{isSuccess && <p className="text-green-500">Deposited successfully!</p>}
{error && <p className="text-red-500">Error: {error.message}</p>}
</div>
)
}
Signing Messages
import { useSignMessage } from 'wagmi'
export function SigninButton() {
const { signMessage, data: signature, isPending } = useSignMessage()
const handleSignin = () => {
const nonce = crypto.randomUUID()
// Store nonce to verify on server
localStorage.setItem('auth_nonce', nonce)
signMessage({
message: `Sign in to My App\n\nNonce: ${nonce}\nTimestamp: ${Date.now()}`
})
}
// When signature is ready, send to your backend
useEffect(() => {
if (signature) {
verifySignatureOnServer(signature)
}
}, [signature])
return (
<button onClick={handleSignin} disabled={isPending}>
{isPending ? 'Check MetaMask...' : 'Sign In with Ethereum'}
</button>
)
}
Common Errors and Fixes
// User rejected transaction
if (error.code === 4001) {
showToast("Transaction rejected")
}
// Wrong network
if (error.code === -32002) {
switchChain({ chainId: mainnet.id })
}
// Insufficient funds
if (error.message.includes("insufficient funds")) {
showToast("Not enough ETH for gas")
}
// Contract reverted
if (error.message.includes("execution reverted")) {
const reason = error.message.match(/reason: (.+)/)?.[1]
showToast(reason || "Transaction failed")
}
wagmi handles wallet reconnection, chain switching, and real-time balance updates automatically. The key is proper error handling -- wallet interactions fail more often than regular API calls.