Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/vaunblu/SimpleKit
Responsive connect wallet and account component built on top of Wagmi and shadcn/ui.
https://github.com/vaunblu/SimpleKit
Last synced: 23 days ago
JSON representation
Responsive connect wallet and account component built on top of Wagmi and shadcn/ui.
- Host: GitHub
- URL: https://github.com/vaunblu/SimpleKit
- Owner: vaunblu
- Created: 2024-07-14T18:21:57.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2024-07-17T15:16:18.000Z (5 months ago)
- Last Synced: 2024-07-25T19:23:49.938Z (5 months ago)
- Language: TypeScript
- Homepage: https://simplekit.vaunb.lu
- Size: 786 KB
- Stars: 5
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-shadcn-ui - simplekit - Responsive connect wallet and account component built on top of Wagmi and shadcn/ui. (Libs and Components)
- awesome-shadcn-ui - Simplekit - Responsive connect wallet and account component built on top of Wagmi and shadcn/ui. (Components)
- awesome-shadcn-ui - Simplekit - Responsive connect wallet and account component built on top of Wagmi and shadcn/ui. (Components)
README
# SimpleKit
Responsive connect wallet and account component built on top of Wagmi and shadcn/ui.
SimpleKit is the simplest way to integrate a connect wallet experience into your React.js web application. It is built on top of primitives where it's your components, your code. No more editing style/theme props.
![Demo](https://utfs.io/f/77740eed-6f9e-4379-a9c9-e7122ceea01f-bfjzn0.gif)
## Installation
### 1. Install Wagmi
Install Wagmi and its peer dependencies:
```bash
pnpm add wagmi [email protected] @tanstack/react-query
```- [Wagmi](https://wagmi.sh/) is a React Hooks library for Ethereum, this is the library you will use to interact with the connected wallet.
- [Viem](https://viem.sh/) is a TypeScript interface for Ethereum that performs blockchain operations.
- [TanStack](https://tanstack.com/query/v5) Query is an async state manager that handles requests, caching, and more.
- [TypeScript](https://wagmi.sh/react/typescript) is optional, but highly recommended.### 2. API Keys
SimpleKit utilises [WalletConnect's](https://walletconnect.com/) SDK to help with connecting wallets. WalletConnect 2.0 requires a `projectId` which you can create quickly and easily for free over at [WalletConnect Cloud](https://cloud.walletconnect.com/).
### 3. Set up the `Web3Provider`: [web3-provider.tsx](src/components/web3-provider.tsx)
Make sure to replace the `projectId` with your own WalletConnect Project ID, if you wish to use WalletConnect (highly recommended)!
```tsx
"use client";// 1. Import modules
import * as React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider, http, createConfig } from "wagmi";
import { mainnet } from "wagmi/chains";
import { injected, coinbaseWallet, walletConnect } from "wagmi/connectors";
import { SimpleKitProvider } from "@/components/simplekit";// Make sure to replace the projectId with your own WalletConnect Project ID,
// if you wish to use WalletConnect (recommended)!
const projectId = "123...abc";// 2. Define your Wagmi config
const config = createConfig({
chains: [mainnet],
connectors: [
injected({ target: "metaMask" }),
coinbaseWallet(),
walletConnect({ projectId }),
],
transports: {
[mainnet.id]: http(),
},
});// 3. Initialize your new QueryClient
const queryClient = new QueryClient();// 4. Create your Wagmi provider
export function Web3Provider(props: { children: React.ReactNode }) {
return (
{props.children}
);
}
```> :warning: When using a framework that doesn't support [React Server Components](https://react.dev/learn/start-a-new-react-project#bleeding-edge-react-frameworks), you will need to remove the `"use client"` directive at the beginning of this file.
Now that you have your `Web3Provider` component, you can wrap your app with it
```tsx
import { Web3Provider } from "@/components/web3-provider";const App = () => {
return (
...
{children}
...
);
};
```### 4. Install the `dialog`, `drawer`, `scroll-area`, and `button` components from shadcn/ui.
```bash
pnpm dlx shadcn-ui@latest add dialog drawer scroll-area button
```> :warning: You will have to run the command below to initialize shadcn/ui if it is not already configured. Please see the official [shadcn/ui install](https://ui.shadcn.com/docs/installation) documentation if you are not using Next.js.
```bash
pnpm dlx shadcn-ui@latest init
```Alternatively, if you are not using shadcn/ui cli, you can manually copy the components from [shadcn/ui](https://ui.shadcn.com/docs) or directly copy from [dialog.tsx](src/components/ui/dialog.tsx), [drawer.tsx](src/components/ui/drawer.tsx), [scroll-area.tsx](src/components/ui/scroll-area.tsx), and [button.tsx](src/components/ui/button.tsx).
If you copied the drawer component manually, make sure to install vaul.
```bash
pnpm add vaul
```### 5. Copy the `simplekit-modal` component: [simplekit-modal.tsx](src/components/simplekit-modal.tsx)
This component is a modified version of the [Credenza](https://github.com/redpangilinan/credenza) component that combines the shadcn/ui `dialog` and `drawer`.
Click to show code
```tsx
"use client";import * as React from "react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { ScrollArea } from "@/components/ui/scroll-area";interface BaseProps {
children: React.ReactNode;
}interface RootSimpleKitModalProps extends BaseProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
}interface SimpleKitModalProps extends BaseProps {
className?: string;
asChild?: true;
}const desktop = "(min-width: 768px)";
const SimpleKitModal = ({ children, ...props }: RootSimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModal = isDesktop ? Dialog : Drawer;return {children};
};const SimpleKitModalTrigger = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalTrigger = isDesktop ? DialogTrigger : DrawerTrigger;return (
{children}
);
};const SimpleKitModalClose = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalClose = isDesktop ? DialogClose : DrawerClose;return (
{children}
);
};const SimpleKitModalContent = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalContent = isDesktop ? DialogContent : DrawerContent;return (
button]:right-[26px] [&>button]:top-[26px]",
className,
)}
onOpenAutoFocus={(e) => e.preventDefault()}
{...props}
>
{children}
);
};const SimpleKitModalDescription = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalDescription = isDesktop
? DialogDescription
: DrawerDescription;return (
{children}
);
};const SimpleKitModalHeader = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalHeader = isDesktop ? DialogHeader : DrawerHeader;return (
{children}
);
};const SimpleKitModalTitle = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalTitle = isDesktop ? DialogTitle : DrawerTitle;return (
{children}
);
};const SimpleKitModalBody = ({
className,
children,
...props
}: SimpleKitModalProps) => {
return (
{children}
);
};const SimpleKitModalFooter = ({
className,
children,
...props
}: SimpleKitModalProps) => {
const isDesktop = useMediaQuery(desktop);
const SimpleKitModalFooter = isDesktop ? DialogFooter : DrawerFooter;return (
{children}
);
};export {
SimpleKitModal,
SimpleKitModalTrigger,
SimpleKitModalClose,
SimpleKitModalContent,
SimpleKitModalDescription,
SimpleKitModalHeader,
SimpleKitModalTitle,
SimpleKitModalBody,
SimpleKitModalFooter,
};/*
* Hook used to calculate the width of the screen using the
* MediaQueryListEvent. This can be moved to a separate file
* if desired (src/hooks/use-media-query.tsx).
*/
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false);React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches);
}const result = matchMedia(query);
result.addEventListener("change", onChange);
setValue(result.matches);return () => result.removeEventListener("change", onChange);
}, [query]);return value;
}
```> :warning: When using a framework that doesn't support [React Server Components](https://react.dev/learn/start-a-new-react-project#bleeding-edge-react-frameworks), you will need to remove the `"use client"` directive at the beginning of this file.
### 6. Copy the `simplekit` component: [simplekit.tsx](src/components/simplekit.tsx)
Click to show code
```tsx
"use client";import * as React from "react";
import {
SimpleKitModal,
SimpleKitModalBody,
SimpleKitModalContent,
SimpleKitModalDescription,
SimpleKitModalFooter,
SimpleKitModalHeader,
SimpleKitModalTitle,
} from "@/components/simplekit-modal";
import { Button } from "@/components/ui/button";
import {
type Connector,
useAccount,
useConnect,
useDisconnect,
useEnsAvatar,
useEnsName,
useBalance,
} from "wagmi";
import { formatEther } from "viem";
import { Check, ChevronLeft, Copy, RotateCcw } from "lucide-react";const MODAL_CLOSE_DURATION = 320;
const SimpleKitContext = React.createContext<{
pendingConnector: Connector | null;
setPendingConnector: React.Dispatch>;
isConnectorError: boolean;
setIsConnectorError: React.Dispatch>;
open: boolean;
setOpen: React.Dispatch>;
}>({
pendingConnector: null,
setPendingConnector: () => null,
isConnectorError: false,
setIsConnectorError: () => false,
open: false,
setOpen: () => false,
});function SimpleKitProvider(props: { children: React.ReactNode }) {
const { status, address } = useAccount();
const [pendingConnector, setPendingConnector] =
React.useState(null);
const [isConnectorError, setIsConnectorError] = React.useState(false);
const [open, setOpen] = React.useState(false);
const isConnected = address && !pendingConnector;React.useEffect(() => {
if (status === "connected" && pendingConnector) {
setOpen(false);const timeout = setTimeout(() => {
setPendingConnector(null);
setIsConnectorError(false);
}, MODAL_CLOSE_DURATION);return () => clearTimeout(timeout);
}
}, [status, setOpen, pendingConnector, setPendingConnector]);return (
{props.children}
{isConnected ? : }
);
}function ConnectWalletButton() {
const simplekit = useSimpleKit();
const { address } = useAccount();
const { data: ensName } = useEnsName({ address });
const { data: ensAvatar } = useEnsAvatar({ name: ensName! });return (
{simplekit.isConnected ? (
<>
{ensAvatar && }
{address && (
{ensName ? `${ensName}` : simplekit.formattedAddress}
)}
>
) : (
"Connect Wallet"
)}
);
}function Account() {
const { address } = useAccount();
const { disconnect } = useDisconnect();
const { data: ensName } = useEnsName({ address });
const { data: userBalance } = useBalance({ address });
const context = React.useContext(SimpleKitContext);const formattedAddress = address?.slice(0, 6) + "•••" + address?.slice(-4);
const formattedUserBalace = userBalance?.value
? parseFloat(formatEther(userBalance.value)).toFixed(4)
: undefined;function handleDisconnect() {
context.setOpen(false);
setTimeout(() => {
disconnect();
}, MODAL_CLOSE_DURATION);
}return (
<>
Connected
Account modal for your connected Web3 wallet.
{ensName ? `${ensName}` : formattedAddress}
{`${formattedUserBalace ?? "0.00"} ETH`}
Disconnect
>
);
}function Connectors() {
const context = React.useContext(SimpleKitContext);return (
<>
{context.pendingConnector?.name ?? "Connect Wallet"}
Connect your Web3 wallet or create a new one.
{context.pendingConnector ? : }
>
);
}function WalletConnecting() {
const context = React.useContext(SimpleKitContext);return (
{context.pendingConnector?.icon && (
{context.isConnectorError ? : null}
)}
{context.isConnectorError ? "Request Error" : "Requesting Connection"}
{context.isConnectorError
? "There was an error with the request. Click above to try again."
: `Open the ${context.pendingConnector?.name} browser extension to connect your wallet.`}
);
}function WalletOptions() {
const context = React.useContext(SimpleKitContext);
const { connectors, connect } = useConnectors();return (
{connectors.map((connector) => (
{
context.setIsConnectorError(false);
context.setPendingConnector(connector);
connect({ connector });
}}
/>
))}
);
}function WalletOption(props: { connector: Connector; onClick: () => void }) {
const [ready, setReady] = React.useState(false);React.useEffect(() => {
async function checkReady() {
const provider = await props.connector.getProvider();
setReady(!!provider);
}
checkReady()
.then(() => null)
.catch(() => null);
}, [props.connector]);return (
{props.connector.name}
{props.connector.icon && (
)}
);
}function CopyAddressButton() {
const { address } = useAccount();
const [copied, setCopied] = React.useState(false);React.useEffect(() => {
const timeout = setTimeout(() => {
if (copied) setCopied(false);
}, 1000);
return () => clearTimeout(timeout);
}, [copied, setCopied]);async function handleCopy() {
setCopied(true);
await navigator.clipboard.writeText(address!);
}return (
{copied ? (
) : (
)}
);
}function BackChevron() {
const context = React.useContext(SimpleKitContext);if (!context.pendingConnector) {
return null;
}function handleClick() {
context.setIsConnectorError(false);
context.setPendingConnector(null);
}return (
Cancel connection
);
}function RetryConnectorButton() {
const context = React.useContext(SimpleKitContext);
const { connect } = useConnect({
mutation: {
onError: () => context.setIsConnectorError(true),
},
});function handleClick() {
if (context.pendingConnector) {
context.setIsConnectorError(false);
connect({ connector: context.pendingConnector });
}
}return (
);
}function useConnectors() {
const context = React.useContext(SimpleKitContext);
const { connect, connectors } = useConnect({
mutation: {
onError: () => context.setIsConnectorError(true),
},
});const sortedConnectors = React.useMemo(() => {
let metaMaskConnector: Connector | undefined;
let injectedConnector: Connector | undefined;const formattedConnectors = connectors.reduce(
(acc: Array, curr) => {
console.log(curr.id);
switch (curr.id) {
case "metaMaskSDK":
metaMaskConnector = {
...curr,
icon: "https://utfs.io/f/be0bd88f-ce87-4cbc-b2e5-c578fa866173-sq4a0b.png",
};
return acc;
case "metaMask":
injectedConnector = {
...curr,
icon: "https://utfs.io/f/be0bd88f-ce87-4cbc-b2e5-c578fa866173-sq4a0b.png",
};
return acc;
case "safe":
acc.push({
...curr,
icon: "https://utfs.io/f/164ea200-3e15-4a9b-9ce5-a397894c442a-awpd29.png",
});
return acc;
case "coinbaseWalletSDK":
acc.push({
...curr,
icon: "https://utfs.io/f/53e47f86-5f12-404f-a98b-19dc7b760333-chngxw.png",
});
return acc;
case "walletConnect":
acc.push({
...curr,
icon: "https://utfs.io/f/5bfaa4d1-b872-48a7-9d37-c2517d4fc07a-utlf4g.png",
});
return acc;
default:
acc.unshift(curr);
return acc;
}
},
[],
);if (
metaMaskConnector &&
!formattedConnectors.find(
({ id }) =>
id === "io.metamask" ||
id === "io.metamask.mobile" ||
id === "injected",
)
) {
return [metaMaskConnector, ...formattedConnectors];
}if (injectedConnector) {
const nonMetaMaskConnectors = formattedConnectors.filter(
({ id }) => id !== "io.metamask" && id !== "io.metamask.mobile",
);
return [injectedConnector, ...nonMetaMaskConnectors];
}
return formattedConnectors;
}, [connectors]);return { connectors: sortedConnectors, connect };
}/*
* This hook can be moved to a separate file
* if desired (src/hooks/use-simple-kit.tsx).
*/
function useSimpleKit() {
const { address } = useAccount();
const context = React.useContext(SimpleKitContext);const isModalOpen = context.open;
const isConnected = address && !context.pendingConnector;
const formattedAddress = address?.slice(0, 6) + "•••" + address?.slice(-4);function open() {
context.setOpen(true);
}function close() {
context.setOpen(false);
}function toggleModal() {
context.setOpen((prevState) => !prevState);
}return {
isModalOpen,
isConnected,
formattedAddress,
open,
close,
toggleModal,
};
}export {
SimpleKitProvider,
ConnectWalletButton,
useSimpleKit,
SimpleKitContext,
};
```> :warning: When using a framework that doesn't support [React Server Components](https://react.dev/learn/start-a-new-react-project#bleeding-edge-react-frameworks), you will need to remove the `"use client"` directive at the beginning of this file.
### 7. Update the import paths based on your project structure.
## Usage
An example [connect wallet button](https://github.com/vaunblu/SimpleKit/blob/main/src/components/simplekit.tsx#L87) component is exported from `simplekit` and can be used as follows
```tsx
import { ConnectWalletButton } from "@/components/simplekit";
``````tsx
```
A [useSimpleKit](https://github.com/vaunblu/SimpleKit/blob/main/src/components/simplekit.tsx#L430) hook is also exported from `simplekit` and can be used to trigger the SimpleKit modal from any component. Below is an example of a custom component that opens the SimpleKit modal.
```tsx
"use client";import { useSimpleKit } from "@/components/simplekit";
import { Button } from "@/components/ui/button";export function OpenModalButton() {
const simplekit = useSimpleKit();return Open SimpleKit Modal;
}
```## Additional Build Tooling Setup
### Next.js support and using SSR with the app router
SimpleKit uses [WalletConnect's](https://walletconnect.com/) SDK to help with connecting wallets. WalletConnect 2.0 pulls in Node.js dependencies that Next.js does not support by default. You can mitigate this by adding the following to your `next.config.js` file:
```js
const nextConfig = {
webpack: (config) => {
config.resolve.fallback = { fs: false, net: false, tls: false };
config.externals.push("pino-pretty", "encoding");
return config;
},
};
```If you are looking to use SimpleKit with the Next.js app router, you can follow the official [Wagmi SSR](https://wagmi.sh/react/guides/ssr) documentation or change the following:
1. Copy the `wagmi-config` file: [wagmi-config.ts](src/lib/wagmi-config.ts)
The config needs to be separate from the WagmiProvider given that is a client component with the `"use client"` directive.
```ts
import { http, createConfig, cookieStorage, createStorage } from "wagmi";
import { mainnet } from "wagmi/chains";
import { injected, coinbaseWallet, walletConnect } from "wagmi/connectors";// Make sure to replace the projectId with your own WalletConnect Project ID,
// if you wish to use WalletConnect (recommended)!
const projectId = "123...abc";export function getConfig() {
return createConfig({
chains: [mainnet],
connectors: [
injected({ target: "metaMask" }),
coinbaseWallet(),
walletConnect({ projectId }),
],
ssr: true,
storage: createStorage({
storage: cookieStorage,
}),
transports: {
[mainnet.id]: http(),
},
});
}export const config = getConfig();
```This version of the config sets the `ssr` config property to `true` and uses Wagmi's `createStorage` to initialize a `cookieStorage` on the `storage` config property.
2. Hydrate your cookie in Layout
In our [layout.tsx](src/app/layout.tsx) file (a Server Component), we will need to extract the cookie from the `headers` function and pass it to `cookieToInitialState`.
We use the `getConfig()` helper from [wagmi-config.ts](src/lib/wagmi-config) to pass in `cookieToInitialState`.
```tsx
import { Web3Provider } from "@/components/web3-provider";
import { headers } from "next/headers";
import { cookieToInitialState } from "wagmi";
import { getConfig } from "@/lib/wagmi-config";
...export default function Layout() {
...
const initialState = cookieToInitialState(
getConfig(),
headers().get("cookie"),
);return (
...
{children}
...
);
}
```3. Replace the contents of `web3-provider` with the content from the `web3-provider-ssr` component: [web3-provider-ssr.tsx](src/components/web3-provider-ssr.tsx)
```tsx
"use client";// 1. Import modules
import * as React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider, State } from "wagmi";
import { getConfig } from "@/lib/wagmi-config";
import { SimpleKitProvider } from "@/components/simplekit";// 2. Define your Wagmi config
const config = getConfig();// 3. Initialize your new QueryClient
const queryClient = new QueryClient();// 4. Create your Wagmi provider
export function Web3Provider(props: {
initialState: State | undefined;
children: React.ReactNode;
}) {
return (
{props.children}
);
}
```The two changes here are we use `getConfig()` to initialize our Wagmi config and our `WagmiProvider` consumes the `initialState` we passed from our Layout.
---
### Vaul background scaling
If you want to enable background scaling, wrap your app with the `vaul-drawer-wrapper`.
```html
{children}
```See my implementation at [layout.tsx](src/app/layout.tsx). Make sure to update the background color to match your project's theme.
---
### Local connector icons
Imported Wagmi connectors do not have their own icons. I provided URLs to hosted files so you don't need to worry about them. However, if you want to self host your icons you can copy the files in the [icons](public/icons) directory into your `public` folder.
Then change the following code in your `simplekit` component:
```tsx
const formattedConnectors = connectors.reduce((acc: Array, curr) => {
switch (curr.id) {
case "metaMaskSDK":
metaMaskConnector = {
...curr,
icon: "/icons/metamask-icon.png",
};
return acc;
case "injected":
injectedConnector = {
...curr,
icon: "/icons/metamask-icon.png",
};
return acc;
case "safe":
acc.push({
...curr,
icon: "/icons/gnosis-safe-icon.png",
});
return acc;
case "coinbaseWalletSDK":
acc.push({
...curr,
icon: "coinbase-icon.png",
});
return acc;
case "walletConnect":
acc.push({
...curr,
icon: "wallet-connect-icon.png",
});
return acc;
default:
acc.unshift(curr);
return acc;
}
}, []);
```## Credits
- [shadcn/ui](https://github.com/shadcn-ui/ui) by [shadcn](https://github.com/shadcn)
- [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski](https://github.com/emilkowalski)
- [Credenza](https://github.com/redpangilinan/credenza) by [redpangilinan](https://github.com/redpangilinan)
- [ConnectKit](https://docs.family.co/connectkit) by [Family](https://family.co/)