Skip to content

Commit 4635d12

Browse files
committed
draft: swap example
1 parent 634b06a commit 4635d12

File tree

10 files changed

+1004
-6
lines changed

10 files changed

+1004
-6
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useWallets } from "@wallet-standard/react";
5+
import { useWallet } from "../contexts/WalletContext";
6+
import { Button } from "./ui/button";
7+
import { WalletListModal } from "./WalletListModal";
8+
9+
export function ConnectWalletButton() {
10+
const [isModalOpen, setIsModalOpen] = useState(false);
11+
const wallets = useWallets();
12+
const { account, isConnected } = useWallet();
13+
14+
const solanaWallets = wallets.filter((wallet) =>
15+
wallet.chains.some((chain) => chain.startsWith("solana:"))
16+
);
17+
18+
if (isConnected && account) {
19+
return (
20+
<div className="wallet-connected">
21+
<div className="status-dot"></div>
22+
<span className="wallet-address">
23+
{account.address.slice(0, 4)}...{account.address.slice(-4)}
24+
</span>
25+
</div>
26+
);
27+
}
28+
29+
return (
30+
<>
31+
<Button onClick={() => setIsModalOpen(true)}>Connect Wallet</Button>
32+
<WalletListModal
33+
isOpen={isModalOpen}
34+
onClose={() => setIsModalOpen(false)}
35+
wallets={solanaWallets}
36+
/>
37+
</>
38+
);
39+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"use client";
2+
3+
import {
4+
useConnect,
5+
useDisconnect,
6+
type UiWallet,
7+
} from "@wallet-standard/react";
8+
import { useEffect } from "react";
9+
import {
10+
Dialog,
11+
DialogContent,
12+
DialogDescription,
13+
DialogHeader,
14+
DialogTitle,
15+
} from "./ui/dialog";
16+
import { useWallet } from "../contexts/WalletContext";
17+
18+
interface WalletListModalProps {
19+
isOpen: boolean;
20+
onClose: () => void;
21+
wallets: UiWallet[];
22+
}
23+
24+
export function WalletListModal({
25+
isOpen,
26+
onClose,
27+
wallets,
28+
}: WalletListModalProps) {
29+
return (
30+
<Dialog open={isOpen} onOpenChange={onClose}>
31+
<DialogContent>
32+
<DialogHeader>
33+
<DialogTitle>Connect Wallet</DialogTitle>
34+
</DialogHeader>
35+
<DialogDescription>Select a wallet to connect to.</DialogDescription>
36+
<div className="flex flex-col gap-2">
37+
{wallets.map((wallet) => (
38+
<WalletListItem
39+
key={wallet.name}
40+
wallet={wallet}
41+
onConnect={onClose}
42+
/>
43+
))}
44+
</div>
45+
</DialogContent>
46+
</Dialog>
47+
);
48+
}
49+
50+
interface WalletItemProps {
51+
wallet: UiWallet;
52+
onConnect?: () => void;
53+
}
54+
55+
export const WalletListItem = ({ wallet, onConnect }: WalletItemProps) => {
56+
const [isConnecting, connect] = useConnect(wallet);
57+
const [isDisconnecting, disconnect] = useDisconnect(wallet);
58+
const { setConnectedWallet, isConnected } = useWallet();
59+
60+
useEffect(() => {
61+
if (isDisconnecting) {
62+
setConnectedWallet(null);
63+
}
64+
}, [isDisconnecting, setConnectedWallet]);
65+
66+
const handleConnect = async () => {
67+
try {
68+
const connectedAccount = await connect();
69+
if (!connectedAccount.length) {
70+
console.warn(`Connect to ${wallet.name} but there are no accounts.`);
71+
return connectedAccount;
72+
}
73+
74+
const first = connectedAccount[0];
75+
setConnectedWallet({ account: first, wallet });
76+
onConnect?.(); // Close modal after successful connection
77+
return connectedAccount;
78+
} catch (error) {
79+
console.error("Failed to connect wallet:", error);
80+
}
81+
};
82+
83+
return (
84+
<button
85+
className="wallet-item"
86+
onClick={isConnected ? disconnect : handleConnect}
87+
disabled={isConnecting}
88+
>
89+
{wallet.icon ? (
90+
<img
91+
src={wallet.icon}
92+
alt={wallet.name}
93+
className="wallet-icon"
94+
/>
95+
) : (
96+
<div className="wallet-icon-fallback">
97+
<span>
98+
{wallet.name.charAt(0).toUpperCase()}
99+
</span>
100+
</div>
101+
)}
102+
<div className="wallet-info">
103+
<div className="wallet-name">
104+
{isConnecting ? "Connecting..." : wallet.name}
105+
</div>
106+
<div className="wallet-status">
107+
{isConnecting ? "Please wait..." : "Click to connect"}
108+
</div>
109+
</div>
110+
{isConnecting && (
111+
<div className="spinner" />
112+
)}
113+
</button>
114+
);
115+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { forwardRef } from "react";
2+
3+
export interface ButtonProps
4+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5+
variant?: "default" | "outline" | "ghost";
6+
}
7+
8+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
9+
({ className = "", variant = "default", ...props }, ref) => {
10+
const baseClass = "btn";
11+
const variantClass = variant === "outline" ? "btn-outline" : "btn-primary";
12+
const finalClassName = `${baseClass} ${variantClass} ${className}`.trim();
13+
14+
return (
15+
<button
16+
className={finalClassName}
17+
ref={ref}
18+
{...props}
19+
/>
20+
);
21+
}
22+
);
23+
24+
Button.displayName = "Button";
25+
26+
export { Button };
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"use client";
2+
3+
import { createContext, useContext, useState, useEffect } from "react";
4+
5+
const DialogContext = createContext<{
6+
open: boolean;
7+
setOpen: (open: boolean) => void;
8+
}>({
9+
open: false,
10+
setOpen: () => {},
11+
});
12+
13+
interface DialogProps {
14+
children: React.ReactNode;
15+
open?: boolean;
16+
onOpenChange?: (open: boolean) => void;
17+
}
18+
19+
export function Dialog({ children, open, onOpenChange }: DialogProps) {
20+
const [internalOpen, setInternalOpen] = useState(false);
21+
const isControlled = open !== undefined;
22+
const dialogOpen = isControlled ? open : internalOpen;
23+
24+
const setOpen = (newOpen: boolean) => {
25+
if (isControlled) {
26+
onOpenChange?.(newOpen);
27+
} else {
28+
setInternalOpen(newOpen);
29+
}
30+
};
31+
32+
return (
33+
<DialogContext.Provider value={{ open: dialogOpen, setOpen }}>
34+
{children}
35+
</DialogContext.Provider>
36+
);
37+
}
38+
39+
interface DialogContentProps {
40+
children: React.ReactNode;
41+
className?: string;
42+
}
43+
44+
export function DialogContent({ children, className = "" }: DialogContentProps) {
45+
const { open, setOpen } = useContext(DialogContext);
46+
47+
useEffect(() => {
48+
if (open) {
49+
document.body.style.overflow = "hidden";
50+
} else {
51+
document.body.style.overflow = "unset";
52+
}
53+
54+
return () => {
55+
document.body.style.overflow = "unset";
56+
};
57+
}, [open]);
58+
59+
if (!open) return null;
60+
61+
return (
62+
<div
63+
className="modal-overlay"
64+
onClick={(e) => {
65+
if (e.target === e.currentTarget) {
66+
setOpen(false);
67+
}
68+
}}
69+
>
70+
<div
71+
className={`modal-content ${className}`}
72+
onClick={(e) => e.stopPropagation()}
73+
>
74+
{children}
75+
</div>
76+
</div>
77+
);
78+
}
79+
80+
interface DialogHeaderProps {
81+
children: React.ReactNode;
82+
className?: string;
83+
}
84+
85+
export function DialogHeader({ children, className = "" }: DialogHeaderProps) {
86+
return (
87+
<div className={className} style={{ marginBottom: '16px' }}>
88+
{children}
89+
</div>
90+
);
91+
}
92+
93+
interface DialogTitleProps {
94+
children: React.ReactNode;
95+
className?: string;
96+
}
97+
98+
export function DialogTitle({ children, className = "" }: DialogTitleProps) {
99+
return (
100+
<h2 className={`modal-title ${className}`}>
101+
{children}
102+
</h2>
103+
);
104+
}
105+
106+
interface DialogDescriptionProps {
107+
children: React.ReactNode;
108+
className?: string;
109+
}
110+
111+
export function DialogDescription({ children, className = "" }: DialogDescriptionProps) {
112+
return (
113+
<p className={`modal-description ${className}`}>
114+
{children}
115+
</p>
116+
);
117+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { createContext, useContext, useState, useMemo } from "react";
2+
import type { ReactNode } from "react";
3+
import type { UiWalletAccount, UiWallet } from "@wallet-standard/react";
4+
import { useWalletAccountTransactionSendingSigner } from "@solana/react";
5+
import type { TransactionSendingSigner } from "@solana/kit";
6+
7+
interface ConnectedWallet {
8+
account: UiWalletAccount;
9+
wallet: UiWallet;
10+
}
11+
12+
interface WalletContextType {
13+
account: UiWalletAccount | null;
14+
wallet: UiWallet | null;
15+
connectedWallet: ConnectedWallet | null;
16+
setConnectedWallet: (wallet: ConnectedWallet | null) => void;
17+
isConnected: boolean;
18+
signer: TransactionSendingSigner | null;
19+
}
20+
21+
const WalletContext = createContext<WalletContextType | undefined>(undefined);
22+
23+
function WalletProviderInner({
24+
children,
25+
connectedWallet,
26+
setConnectedWallet
27+
}: {
28+
children: ReactNode;
29+
connectedWallet: ConnectedWallet;
30+
setConnectedWallet: (wallet: ConnectedWallet | null) => void;
31+
}) {
32+
const signer = useWalletAccountTransactionSendingSigner(connectedWallet.account, "solana:mainnet");
33+
34+
return (
35+
<WalletContext.Provider value={{
36+
account: connectedWallet.account,
37+
wallet: connectedWallet.wallet,
38+
connectedWallet,
39+
setConnectedWallet,
40+
isConnected: true,
41+
signer,
42+
}}>
43+
{children}
44+
</WalletContext.Provider>
45+
);
46+
}
47+
48+
export function WalletProvider({ children }: { children: ReactNode }) {
49+
const [connectedWallet, setConnectedWallet] =
50+
useState<ConnectedWallet | null>(null);
51+
52+
if (connectedWallet) {
53+
return (
54+
<WalletProviderInner
55+
connectedWallet={connectedWallet}
56+
setConnectedWallet={setConnectedWallet}
57+
>
58+
{children}
59+
</WalletProviderInner>
60+
);
61+
}
62+
63+
const value = {
64+
account: null,
65+
wallet: null,
66+
connectedWallet: null,
67+
setConnectedWallet,
68+
isConnected: false,
69+
signer: null,
70+
};
71+
72+
return (
73+
<WalletContext.Provider value={value}>{children}</WalletContext.Provider>
74+
);
75+
}
76+
77+
export function useWallet() {
78+
const context = useContext(WalletContext);
79+
if (context === undefined) {
80+
throw new Error("useWallet must be used within a WalletProvider");
81+
}
82+
return context;
83+
}

0 commit comments

Comments
 (0)