diff --git a/app/src/components/connector/connect-button.tsx b/app/src/components/connector/connect-button.tsx
new file mode 100644
index 0000000..47c5f1a
--- /dev/null
+++ b/app/src/components/connector/connect-button.tsx
@@ -0,0 +1,108 @@
+'use client';
+
+import { useConnector } from '@solana/connector/react';
+import { useState } from 'react';
+import { WalletModal } from './wallet-modal';
+import { WalletDropdownContent } from './wallet-dropdown-content';
+import { Button } from '@/components/ui-base/button';
+import { Menu, MenuTrigger, MenuPortal, MenuPositioner, MenuPopup } from '@/components/ui-base/menu';
+import { Wallet, ChevronDown } from 'lucide-react';
+
+interface ConnectButtonProps {
+ className?: string;
+}
+
+// Custom Avatar component for Base UI
+function Avatar({
+ src,
+ alt,
+ fallback,
+ className,
+}: {
+ src?: string;
+ alt?: string;
+ fallback?: React.ReactNode;
+ className?: string;
+}) {
+ const [hasError, setHasError] = useState(false);
+
+ return (
+
+ {src && !hasError ? (
+

setHasError(true)}
+ />
+ ) : (
+
{fallback}
+ )}
+
+ );
+}
+
+// Spinner component
+function Spinner({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+export function ConnectButton({ className }: ConnectButtonProps) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const { isConnected, isConnecting, account, connector } = useConnector();
+
+ if (isConnected && account && connector) {
+ const shortAddress = `${account.slice(0, 4)}...${account.slice(-4)}`;
+ const walletIcon = connector.icon || undefined;
+
+ return (
+
+ );
+ }
+
+ // Show loading button when connecting (but modal stays rendered)
+ const buttonContent = isConnecting ? (
+ <>
+
+ Connecting...
+ >
+ ) : (
+ Connect Wallet
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/src/components/connector/shared/hidden-wallet-icons.tsx b/app/src/components/connector/shared/hidden-wallet-icons.tsx
new file mode 100644
index 0000000..2676067
--- /dev/null
+++ b/app/src/components/connector/shared/hidden-wallet-icons.tsx
@@ -0,0 +1,45 @@
+/* eslint-disable @next/next/no-img-element */
+import { cn } from '@/lib/utils';
+
+interface WalletIconSource {
+ id: string;
+ name: string;
+ icon?: string | null;
+}
+
+interface HiddenWalletIconsProps {
+ wallets: WalletIconSource[];
+ maxIcons?: number;
+ className?: string;
+}
+
+export function HiddenWalletIcons({ wallets, maxIcons = 4, className }: HiddenWalletIconsProps) {
+ const previewWallets = wallets.slice(0, maxIcons);
+ const placeholderCount = Math.max(0, maxIcons - previewWallets.length);
+
+ return (
+
+ {previewWallets.map(wallet => (
+
+ {wallet.icon ? (
+

{
+ e.currentTarget.style.display = 'none';
+ }}
+ />
+ ) : null}
+
+ ))}
+ {Array.from({ length: placeholderCount }).map((_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/app/src/components/connector/wallet-dropdown-content.tsx b/app/src/components/connector/wallet-dropdown-content.tsx
new file mode 100644
index 0000000..7f39fe3
--- /dev/null
+++ b/app/src/components/connector/wallet-dropdown-content.tsx
@@ -0,0 +1,85 @@
+'use client'
+
+import { useConnector } from '@solana/connector/react'
+import { useState } from 'react'
+
+interface Props {
+ selectedAccount: string
+ walletIcon?: string
+ walletName: string
+}
+
+export function WalletDropdownContent({ selectedAccount, walletIcon, walletName }: Props) {
+ const { disconnectWallet } = useConnector()
+ const [copied, setCopied] = useState(false)
+
+ function copyAddress() {
+ navigator.clipboard.writeText(selectedAccount)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1500)
+ }
+
+ return (
+
+ {walletIcon && (
+
+

+
{walletName}
+
+ )}
+
+ CONNECTED WALLET
+
+
+ {selectedAccount}
+
+
+
+
+ )
+}
diff --git a/app/src/components/connector/wallet-modal.tsx b/app/src/components/connector/wallet-modal.tsx
new file mode 100644
index 0000000..d1ac56c
--- /dev/null
+++ b/app/src/components/connector/wallet-modal.tsx
@@ -0,0 +1,435 @@
+'use client';
+
+import { useConnector, type WalletConnectorId, type WalletConnectorMetadata } from '@solana/connector/react';
+import { Dialog, DialogContent, DialogTitle, DialogClose } from '@/components/ui-base/dialog';
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui-base/collapsible';
+import { Button } from '@/components/ui-base/button';
+import { HiddenWalletIcons } from '@/components/connector/shared/hidden-wallet-icons';
+import { Wallet, ExternalLink, X } from 'lucide-react';
+import { useState, useEffect } from 'react';
+
+interface WalletModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+// Custom Avatar component
+function Avatar({
+ src,
+ alt,
+ fallback,
+ className,
+}: {
+ src?: string;
+ alt?: string;
+ fallback?: React.ReactNode;
+ className?: string;
+}) {
+ const [hasError, setHasError] = useState(false);
+
+ return (
+
+ {src && !hasError ? (
+

setHasError(true)}
+ />
+ ) : (
+
{fallback}
+ )}
+
+ );
+}
+
+// Badge component
+function Badge({ children, className }: { children: React.ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+// Spinner component
+function Spinner({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+// Separator component
+function Separator({ className }: { className?: string }) {
+ return ;
+}
+
+// Error Alert component
+function ErrorAlert({ message, onDismiss }: { message: string; onDismiss: () => void }) {
+ return (
+
+
+
Connection failed
+
{message}
+
+
+
+ );
+}
+
+export function WalletModal({ open, onOpenChange }: WalletModalProps) {
+ const { walletStatus, isConnecting, connectorId, connectors, connectWallet, disconnectWallet } = useConnector();
+ const status = walletStatus.status;
+
+ const [connectingConnectorId, setConnectingConnectorId] = useState(null);
+ const [isClient, setIsClient] = useState(false);
+ const [recentlyConnectedConnectorId, setRecentlyConnectedConnectorId] = useState(null);
+ const [isOtherWalletsOpen, setIsOtherWalletsOpen] = useState(false);
+ const [errorConnectorId, setErrorConnectorId] = useState(null);
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ useEffect(() => {
+ const recent = localStorage.getItem('recentlyConnectedConnectorId');
+ if (recent) {
+ setRecentlyConnectedConnectorId(recent as WalletConnectorId);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (status !== 'connected') return;
+ if (!connectorId) return;
+ localStorage.setItem('recentlyConnectedConnectorId', connectorId);
+ setRecentlyConnectedConnectorId(connectorId);
+ }, [status, connectorId]);
+
+ function cancelConnection() {
+ setConnectingConnectorId(null);
+ disconnectWallet().catch(() => {});
+ }
+
+ // Clear error state when modal closes or user tries another wallet
+ const clearError = () => {
+ setErrorConnectorId(null);
+ setErrorMessage(null);
+ };
+
+ const handleSelectWallet = async (connector: WalletConnectorMetadata) => {
+ clearError();
+ setConnectingConnectorId(connector.id);
+ try {
+ await connectWallet(connector.id);
+ localStorage.setItem('recentlyConnectedConnectorId', connector.id);
+ setRecentlyConnectedConnectorId(connector.id);
+ onOpenChange(false);
+ } catch (error) {
+ // Extract user-friendly error message
+ const message = error instanceof Error ? error.message : 'An unexpected error occurred';
+ if (message.includes('Connection cancelled')) return;
+
+ // Set error state for UI feedback
+ setErrorConnectorId(connector.id);
+ setErrorMessage(message);
+
+ // Log for telemetry/debugging (includes full error details)
+ console.error('Failed to connect wallet:', {
+ wallet: connector.name,
+ connectorId: connector.id,
+ error,
+ message,
+ timestamp: new Date().toISOString(),
+ });
+ } finally {
+ setConnectingConnectorId(null);
+ }
+ };
+
+ const readyConnectors = connectors.filter(c => c.ready);
+ const notReadyConnectors = connectors.filter(c => !c.ready);
+
+ const sortedReadyConnectors = [...readyConnectors].sort((a, b) => {
+ const aIsRecent = recentlyConnectedConnectorId === a.id;
+ const bIsRecent = recentlyConnectedConnectorId === b.id;
+ if (aIsRecent && !bIsRecent) return -1;
+ if (!aIsRecent && bIsRecent) return 1;
+ return 0;
+ });
+
+ const primaryWallets = sortedReadyConnectors.slice(0, 3);
+ const otherWallets = sortedReadyConnectors.slice(3);
+
+ const getInstallUrl = (walletName: string, walletUrl?: string): string | undefined => {
+ // Prefer wallet metadata URL if available
+ if (walletUrl) return walletUrl;
+
+ // Known wallet install URLs
+ const name = walletName.toLowerCase();
+ if (name.includes('phantom')) return 'https://phantom.app';
+ if (name.includes('solflare')) return 'https://solflare.com';
+ if (name.includes('backpack')) return 'https://backpack.app';
+ if (name.includes('glow')) return 'https://glow.app';
+ if (name.includes('coinbase')) return 'https://www.coinbase.com/wallet';
+ if (name.includes('ledger')) return 'https://www.ledger.com';
+ if (name.includes('trust')) return 'https://trustwallet.com';
+ if (name.includes('exodus')) return 'https://www.exodus.com';
+
+ // Return undefined for unknown wallets to avoid misleading users
+ return undefined;
+ };
+
+ const handleOpenChange = (isOpen: boolean) => {
+ if (!isOpen) {
+ clearError();
+ if (isConnecting || connectingConnectorId) {
+ cancelConnection();
+ }
+ }
+ onOpenChange(isOpen);
+ };
+
+ return (
+
+ );
+}