Wallet Connect
Integra wallets de criptomonedas en tu app de forma rápida y segura.- Typescript
- React
- Wagmi & Viem
-
Conexión instantánea
Vincula tu wallet en segundos con una experiencia moderna y sencilla.
-
Basado en Shadcn
El componente está basado en shadcn, permitiendo la reutilización de componentes.
-
Compatible con múltiples wallets
Funciona con MetaMask, WalletConnect, Coinbase Wallet y muchas más.
-
Totalmente personalizable
Ajusta el diseño y las funciones según las necesidades de tu proyecto.
Capturas

Que utilizamos aquí?
Librerías Web3
• Wagmi (React)https://wagmi.sh/react/installation
Librería de Componentes
• Shadcnhttps://ui.shadcn.com/docs/installation • Lucide React (Icons)
https://lucide.dev/
Componentes Shadcn
Instalación
Un componente listo para integrar: copia el código, crea el archivo y úsalo en tu proyecto. Puedes personalizarlo y reutilizar sus partes internas según tus necesidades.⚠️ Recuerda cambiar las importaciones en el caso de no tenerlas configuradas.
ConnectWallet.tsx
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import type * as AvatarPrimitive from "@radix-ui/react-avatar";
import {
ChevronDown,
Copy,
CopyCheck,
Droplet,
Loader,
LogOut,
Wallet,
X,
} from "lucide-react";
import type React from "react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import type { Address } from "viem";
import {
type Connector,
useAccount,
useBalance,
useChainId,
useConnectors,
useDisconnect,
useEnsAvatar,
useEnsName,
useSwitchChain,
} from "wagmi";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/AlertDialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/Avatar";
import { Button } from "@/components/Button";
function useAccountActions() {
const [isCopied, setIsCopied] = useState(false);
const { address } = useAccount();
const { disconnectAsync } = useDisconnect();
const handleCopyToClipboard = async () => {
if (address === undefined) {
toast.error("No account connected", { dismissible: true });
return;
}
try {
await navigator.clipboard.writeText(address);
setIsCopied(true);
} catch (err) {
toast.error("Failed address copy to clipboard", { dismissible: true });
console.error("Copying address failed: ", err);
} finally {
setTimeout(() => setIsCopied(false), 2000);
}
};
const handleDisconnect = async () => {
try {
await disconnectAsync();
} catch (err) {
toast.error("Disconnect account failed", { dismissible: true });
console.error("Disconnect account failed:", err);
}
};
return {
address,
isCopied,
handleCopyToClipboard,
handleDisconnect,
};
}
export function ConnectWallet() {
const { address } = useAccount();
if (address) {
return (
<div className="flex gap-2">
<WagmiAccount address={address}>
<Button className="w-max px-0.5 py-0.5" variant="outline">
<AccountBalance className="leading-relaxed ml-1.5 mr-1" />
<div className="flex gap-2 items-center bg-accent px-1.5 size-full rounded-md">
<Avatar className="size-5">
<AccountAvatarImage address={address} />
<AvatarFallback className="bg-transparent">
<Wallet />
</AvatarFallback>
</Avatar>
<span className="leading-relaxed">
{truncateAddress(address)}
</span>
</div>
</Button>
</WagmiAccount>
<ChainSwitcherDialog>
{(currentChainName) => (
<Button variant="outline" className="px-2">
<div className="flex gap-2 items-center size-full">
<span className="leading-relaxed">{currentChainName}</span>
</div>
<ChevronDown />
</Button>
)}
</ChainSwitcherDialog>
</div>
);
}
return (
<ConnectWalletDialog>
<Button>
<Wallet />
<span className="leading-relaxed">Connect Wallet</span>
</Button>
</ConnectWalletDialog>
);
}
function ConnectWalletDialog({ children }: { children: React.ReactNode }) {
const connectors = useConnectors();
return (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Pick Wallet</AlertDialogTitle>
<AlertDialogDescription>
Select your preferred wallet provider from the available wallet
list.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="p-0.5 grid grid-cols-1 md:grid-cols-2 max-h-40 md:max-h-72 gap-4 overflow-y-auto">
{connectors.map((connector) => (
<WalletConnectorCard key={connector.id} connector={connector} />
))}
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
function WalletConnectorCard({ connector }: { connector: Connector }) {
const [ready, setReady] = useState(false);
useEffect(() => {
(async () => {
const provider = await connector.getProvider();
setReady(!!provider);
})();
}, [connector]);
return (
<button
type="button"
className="bg-muted rounded-md dark:bg-card h-30 border hover:bg-muted-foreground/15 dark:hover:bg-foreground/10 transition-colors focus:outline-2 disabled:opacity-50 dark:hover:disabled:bg-card hover:disabled:bg-muted disabled:cursor-not-allowed"
disabled={!ready}
onClick={() => connector.connect()}
title={
!ready
? `${connector.name} not available`
: `Connect with ${connector.name}`
}
>
<div className="flex flex-col gap-2 items-center justify-center">
<Avatar className="size-8">
<AvatarImage src={connector.icon} alt={`Logo of ${connector.name}`} />
<AvatarFallback className="bg-transparent">
<Wallet />
</AvatarFallback>
</Avatar>
<span>{connector.name}</span>
</div>
</button>
);
}
export function ChainSwitcherDialog({
children,
}: {
children: (currentChainName: string) => React.ReactNode;
}) {
const { switchChain, chains } = useSwitchChain();
const chainId = useChainId();
const currentChain = useMemo(
() => chains.find(({ id }) => id === chainId),
[chainId, chains],
);
return (
<AlertDialog>
<AlertDialogTrigger asChild>
{children(currentChain?.name ?? "Unknown")}
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-xs gap-6">
<AlertDialogHeader className="flex relative gap-0">
<AlertDialogTitle>Switch Chain</AlertDialogTitle>
<AlertDialogDescription>
Switch to preferred chain
</AlertDialogDescription>
<AlertDialogPrimitive.AlertDialogCancel asChild>
<Button
size="icon"
variant="ghost"
className="absolute right-0 bottom-1/2 translate-y-1/2"
>
<X />
</Button>
</AlertDialogPrimitive.AlertDialogCancel>
</AlertDialogHeader>
<div className="flex flex-col gap-2 mt-1">
{chains.length === 0 ? (
<div className="flex flex-col gap-2 items-center justify-center">
<Droplet className="size-14" />
<div className="text-center">No chains available</div>
</div>
) : (
chains.map(({ id, name }) => (
<Button
key={id}
aria-label={`Switch to ${name}`}
aria-selected={id === chainId}
variant={id === chainId ? "default" : "outline"}
className="justify-between focus-visible:ring-0 focus-visible:dark:ring-0 aria-[selected=false]:focus-visible:bg-accent aria-[selected=true]:focus-visible:bg-primary/90"
onClick={() => {
switchChain(
{ chainId: id },
{
onError: (error) => {
console.error("Failed to switch chain", error);
toast.error("Failed to switch chain", {
dismissible: true,
});
},
},
);
}}
>
<span>{name}</span>
{id === chainId && <span className="text-xs">Connected</span>}
</Button>
))
)}
</div>
</AlertDialogContent>
</AlertDialog>
);
}
export function WagmiAccount({
address,
children,
}: {
address: Address;
children: React.ReactNode;
}) {
const { handleCopyToClipboard, handleDisconnect, isCopied } =
useAccountActions();
return (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-xs gap-0">
<AlertDialogHeader>
<AlertDialogTitle hidden>Account Details</AlertDialogTitle>
<AlertDialogDescription hidden>
Details of account {address}
</AlertDialogDescription>
<AlertDialogPrimitive.AlertDialogCancel asChild>
<Button size="icon" variant="ghost" className="ml-auto">
<X />
</Button>
</AlertDialogPrimitive.AlertDialogCancel>
</AlertDialogHeader>
<div className="flex flex-col items-center gap-3 text-foreground">
<Avatar className="size-16">
<AccountAvatarImage address={address} />
<AvatarFallback>
<Wallet />
</AvatarFallback>
</Avatar>
<div className="flex flex-col items-center">
<span className="opacity-80">{truncateAddress(address)}</span>
<AccountBalance as="h1" />
</div>
</div>
<AlertDialogFooter className="w-full flex gap-4 justify-between mt-4">
<WagmiAccountActionCard onClick={handleCopyToClipboard}>
{isCopied ? <CopyCheck /> : <Copy />}
Copy Address
</WagmiAccountActionCard>
<WagmiAccountActionCard onClick={handleDisconnect}>
<LogOut />
Disconnect
</WagmiAccountActionCard>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
function WagmiAccountActionCard({
onClick,
children,
}: {
onClick: () => void;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
className="select-none disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer w-full h-16 gap-1 hover:bg-accent/90 bg-accent/50 rounded-md flex items-center justify-center flex-col text-xs [&_svg]:size-5"
>
{children}
</button>
);
}
type AccountAvatarImageProps = Omit<
React.ComponentProps<typeof AvatarPrimitive.Image>,
"children" | "asChild"
> & {
address: Address;
defaultAvatarUrl?: string | undefined;
};
export function AccountAvatarImage({
className = "size-5",
address,
defaultAvatarUrl = `https://effigy.im/a/${address}.svg`,
...props
}: AccountAvatarImageProps) {
const { data: ensName } = useEnsName({
address,
query: { select: (data) => data ?? undefined },
});
const { data: ensAvatar } = useEnsAvatar({
name: ensName,
query: {
enabled: ensName !== undefined,
select: (data) => data ?? undefined,
},
});
return <AvatarImage {...props} src={ensAvatar ?? defaultAvatarUrl} />;
}
type AccountBalanceProps<T extends React.ElementType> = {
as?: T;
className?: string;
} & React.ComponentPropsWithoutRef<T>;
export function AccountBalance<T extends React.ElementType = "span">({
className = "font-medium text-lg",
as: Component,
...props
}: AccountBalanceProps<T>) {
const { address } = useAccount();
const { data, isLoading, error, isError } = useBalance({
address,
query: { enabled: address !== undefined },
});
const Comp = Component ?? "span";
useEffect(() => {
if (!isError) {
return;
}
toast.error("Error obtaining balance, reload the page", {
dismissible: true,
});
console.error("Error obtaining balance", error);
}, [isError, error]);
return (
<Comp className={className} {...props}>
{isLoading ? (
<Loader className="animate-spin" />
) : isError || data === undefined ? (
"Err"
) : (
`${data.formatted} ${data.symbol}`
)}
</Comp>
);
}
export function truncateAddress(address: Address): string {
const start = address.slice(0, 4);
const end = address.slice(-10, -1);
return `${start}...${end}`;
}
Ejemplo Básico
En un entorno donde el proyecto ya tiene shadcn y los respectivos componentes necesarios, además que cuente ya con wagmi & viem instalados el proceso de integrar el componente a su proyecto puede ser de la forma en la que se le explicará a continuación.En lugar de usar el ejemplo que se muestra a continuación, usted puedeseguir los pasos que dicta la documentación oficial de wagmi. Aquí solo tratamos de simplificar el proceso dedicado a el componente en cuestión.
https://wagmi.sh/react/getting-started
Configuración
El primer paso es configurar wagmi. Si planeas utilizar WalletConnect, necesitarás un projectId que puedes obtener en WalletConnect Cloud. Luego, configura QueryClient y crea un componente Providers que envuelva tu aplicación. Esto proporcionará el contexto necesario para wagmi y react-query. Envuelve tu aplicación de React con el componente Providers que creaste. Si utilizas Storybook, recuerda hacer lo mismo en tu archivo de configuración, como .storybook/preview.tsx, para asegurar que los componentes tengan acceso al contexto de wagmi. Una vez completada la configuración, puedes integrar el componente ConnectWallet en cualquier parte de tu aplicación para gestionar la conexión de la billetera.
providers.tsx
import { type Config, createConfig, http } from "wagmi";
import { base, mainnet } from "wagmi/chains";
import { injected, metaMask, safe, walletConnect } from "wagmi/connectors";
export const WAGMI_CONFIG: Config = createConfig({
chains: [mainnet, base],
connectors: [
injected(),
walletConnect({ projectId: "YOUR_PROJECT_ID" }),
metaMask(),
safe(),
],
transports: {
[mainnet.id]: http(),
[base.id]: http(),
},
});
providers.tsx
import { QueryClient } from "@tanstack/react-query"
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={WAGMI_CONFIG}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Providers } from "./providers";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Providers>
<App />
</Providers>
</React.StrictMode>
);
header.tsx
<header className="flex items-center justify-between p-4">
<h1 className="text-2xl font-bold">My DApp</h1>
<ConnectWallet />
</header>