Creating a Custom Solana Wallet Connect UI
Creating a Custom Solana Wallet Connect UI with Next.js, Tailwind, and Shadcn
While solutions like the Solana Wallet Adapter provide a straightforward way to implement wallet connectivity, their customization options are often limited, leaving developers seeking more flexibility in their user interface designs. In this article, we delve into the realm of custom wallet connectors for Solana, exploring the @solana/wallet-adapter-react package and its associated UI library.
Setting Up the Next.js App with Tailwind CSS
1. Create a New Next.js Project:
Run the following command to initialize your Next.js application:
npx create-next-app@latest
After running this command, you’ll be prompted to set up various configurations such as TypeScript, ESLint, Tailwind CSS, and more.
2. Initial File Structure:
After the setup, your initial file structure should resemble something like this:
• .next/
• app/
• components/
• lib/
• node_modules/
• providers/
• public/
• .eslintrc.json
• .gitignore
• components.json
• next-env.d.ts
• next.config.mjs
• package-lock.json
• package.json
• postcss.config.js
• README.md
• tailwind.config.ts
• tsconfig.json
Setting Up Shadcn
Shadcn provides reusable components that are compatible with Tailwind CSS. It’s an open-source project that simplifies UI component integration.
1. Initialize Shadcn:
Run the following command:
npx shadcn-ui@latest init
2. Install Required NPM Libraries:
To create the wallet connector, install the following libraries:
npm i @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/web3.js @solana/wallet-adapter-wallets @solana/wallet-adapter-base
3. Add the Shadcn Dialog Component:
Run the following command to add the Dialog component:
npx shadcn-ui@latest add dialog
Coding the Wallet Connect Context Provider
Let’s create the wallet connect context provider and wrap the entire app with it.
"use client";
import { FC, ReactNode } from "react";
import {
ConnectionProvider,
WalletProvider,
} from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import "@solana/wallet-adapter-react-ui/styles.css";
import { clusterApiUrl } from "@solana/web3.js";
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
MathWalletAdapter,
TrustWalletAdapter,
CoinbaseWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import { useMemo } from "react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { useWallet } from "@solana/wallet-adapter-react";
const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
const network = WalletAdapterNetwork.Devnet;
// Initiate auto-connect
const { autoConnect } = useWallet();
// Provide a custom RPC endpoint
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
// Wallets
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
new MathWalletAdapter(),
new TrustWalletAdapter(),
new CoinbaseWalletAdapter(),
],
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
export default WalletContextProvider;
Explanation:
• Network Selection: The network is set to Devnet, which is a development network provided by Solana.
• Auto Connect: The useWallet hook from @solana/wallet-adapter-react is used to automatically connect to the wallet when the component is loaded.
• RPC Endpoint Setup: The clusterApiUrl function is used to get the RPC endpoint for the selected network, and this is memoized using React’s useMemo.
• Wallet Adapters Setup: An array of wallet adapters for different wallets is memoized.
• Context Providers: The context providers are set up in a hierarchy to provide the RPC connection context (ConnectionProvider), wallet context (WalletProvider), and wallet modal context (WalletModalProvider).
Wrapping the App with WalletContextProvider
Wrap the entire app using WalletContextProvider in layout.tsx:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Header from "./components/Header";
import WalletContextProvider from "@/providers/WalletContextProvider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<WalletContextProvider>
<Header />
{children}
</WalletContextProvider>
</body>
</html>
);
}
Creating a Header Component
Create a simple Header component to contain the wallet connect button:
import React from "react";
import WalletConnection from "./WalletConnection";
const Header = () => {
return (
<div className="h-[10vh] bg-black flex justify-center">
<div className="max-w-[900px] flex justify-between items-center w-full">
<div className="text-white font-bold text-[30px]">Hello</div>
<div>
<WalletConnection />
</div>
</div>
</div>
);
};
export default Header;
Adding Shadcn Components for the Wallet Connect Button
Run the following commands to add the Button and Dropdown Menu components:
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dropdown-menu
Writing the Wallet Connect Button UI and Functionalities
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "./ui/button";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import Image from "next/image";
import { ChevronRight } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
// Handle wallet balance fixed to 2 decimal numbers without rounding
export function toFixed(num: number, fixed: number): string {
const re = new RegExp(`^-?\\d+(?:\\.\\d{0,${fixed || -1}})?`);
return num.toString().match(re)![0];
}
const WalletConnection = () => {
const { connection } = useConnection();
const { select, wallets, publicKey, disconnect, connecting } = useWallet();
const [open, setOpen] = useState<boolean>(false);
const [balance, setBalance] = useState<number | null>(null);
const [userWalletAddress, setUserWalletAddress] = useState<string>("");
useEffect(() => {
if (!connection || !publicKey) {
return;
}
connection.onAccountChange(
publicKey,
(updatedAccountInfo) => {
setBalance(updatedAccountInfo.lamports / LAMPORTS_PER_SOL);
},
"confirmed"
);
connection.getAccountInfo(publicKey).then((info) => {
if (info) {
setBalance(info?.lamports / LAMPORTS_PER_SOL);
}
});
}, [publicKey, connection]);
useEffect(() => {
setUserWalletAddress(publicKey?.toBase58()!);
}, [publicKey]);
const handleWalletSelect = async (walletName: any) => {
if (walletName) {
try {
select(walletName);
setOpen(false);
} catch (error) {
console.log("wallet connection err : ", error);
}
}
};
const handleDisconnect = async () => {
disconnect();
};
return (
<div className="text-white">
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex gap-2 items-center">
{!publicKey ? (
<>
<DialogTrigger asChild>
<Button className="bg-black text-[20px] md:text-[30px] text-white ring-black ring-2 h
Last updated