feat: add connector wallet components (ConnectButton, WalletModal)
This commit is contained in:
108
app/src/components/connector/connect-button.tsx
Normal file
108
app/src/components/connector/connect-button.tsx
Normal file
@@ -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 (
|
||||
<div className={`relative flex shrink-0 overflow-hidden rounded-full ${className}`}>
|
||||
{src && !hasError ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="aspect-square h-full w-full object-cover"
|
||||
onError={() => setHasError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-muted">{fallback}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Spinner component
|
||||
function Spinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`animate-spin rounded-full border-2 border-current border-t-transparent ${className || 'h-4 w-4'}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Menu>
|
||||
<MenuTrigger className={`h-8 px-3 ${className || ''}`}>
|
||||
<Avatar
|
||||
src={walletIcon}
|
||||
alt={connector.name}
|
||||
fallback={<Wallet className="h-3 w-3" />}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span className="text-xs">{shortAddress}</span>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</MenuTrigger>
|
||||
<MenuPortal>
|
||||
<MenuPositioner sideOffset={8} align="end">
|
||||
<MenuPopup className="rounded-[20px] p-0 outline-none">
|
||||
<WalletDropdownContent
|
||||
selectedAccount={account}
|
||||
walletIcon={walletIcon}
|
||||
walletName={connector.name}
|
||||
/>
|
||||
</MenuPopup>
|
||||
</MenuPositioner>
|
||||
</MenuPortal>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading button when connecting (but modal stays rendered)
|
||||
const buttonContent = isConnecting ? (
|
||||
<>
|
||||
<Spinner className="h-4 w-4" />
|
||||
<span className="text-xs">Connecting...</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="!text-[14px]">Connect Wallet</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => setIsModalOpen(true)} className={className}>
|
||||
{buttonContent}
|
||||
</Button>
|
||||
<WalletModal open={isModalOpen} onOpenChange={setIsModalOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
45
app/src/components/connector/shared/hidden-wallet-icons.tsx
Normal file
45
app/src/components/connector/shared/hidden-wallet-icons.tsx
Normal file
@@ -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 (
|
||||
<div className={cn('grid grid-cols-2 gap-1', className)} aria-hidden="true">
|
||||
{previewWallets.map(wallet => (
|
||||
<div
|
||||
key={wallet.id}
|
||||
className="relative h-4.5 w-4.5 overflow-hidden rounded-full border border-border bg-muted"
|
||||
>
|
||||
{wallet.icon ? (
|
||||
<img
|
||||
src={wallet.icon}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
draggable={false}
|
||||
onError={e => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{Array.from({ length: placeholderCount }).map((_, index) => (
|
||||
<div key={`placeholder-${index}`} className="h-4.5 w-4.5 rounded-full border border-border bg-muted" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
app/src/components/connector/wallet-dropdown-content.tsx
Normal file
85
app/src/components/connector/wallet-dropdown-content.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
width: 230,
|
||||
background: 'var(--bg2)',
|
||||
border: '1px solid var(--bd)',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
{walletIcon && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<img src={walletIcon} width={20} height={20} alt={walletName} style={{ borderRadius: 4 }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600 }}>{walletName}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 10, fontWeight: 600, letterSpacing: '.14em', color: 'var(--mut)', marginBottom: 6 }}>
|
||||
CONNECTED WALLET
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12,
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: 1.5,
|
||||
color: 'var(--tx)',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{selectedAccount}
|
||||
</div>
|
||||
<button
|
||||
onClick={copyAddress}
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 6,
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
background: 'var(--bg3)',
|
||||
border: '1px solid var(--bd)',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy address'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => disconnectWallet()}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
background: 'transparent',
|
||||
border: '1px solid var(--danger)',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: 'var(--danger)',
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
435
app/src/components/connector/wallet-modal.tsx
Normal file
435
app/src/components/connector/wallet-modal.tsx
Normal file
@@ -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 (
|
||||
<div className={`relative flex shrink-0 overflow-hidden rounded-full ${className}`}>
|
||||
{src && !hasError ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="aspect-square h-full w-full object-cover"
|
||||
onError={() => setHasError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-muted">{fallback}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Badge component
|
||||
function Badge({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-md border border-primary/10 px-2.5 py-0.5 text-xs font-semibold transition-colors bg-secondary text-secondary-foreground hover:bg-secondary/80 ${className || ''}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Spinner component
|
||||
function Spinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`animate-spin rounded-full border-2 border-current border-t-transparent ${className || 'h-4 w-4'}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Separator component
|
||||
function Separator({ className }: { className?: string }) {
|
||||
return <div className={`shrink-0 bg-border h-[1px] w-full ${className || ''}`} />;
|
||||
}
|
||||
|
||||
// Error Alert component
|
||||
function ErrorAlert({ message, onDismiss }: { message: string; onDismiss: () => void }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3 rounded-[12px] border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">Connection failed</p>
|
||||
<p className="text-xs opacity-90 mt-0.5">{message}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="shrink-0 rounded-md p-1 hover:bg-destructive/20 transition-colors"
|
||||
aria-label="Dismiss error"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletModal({ open, onOpenChange }: WalletModalProps) {
|
||||
const { walletStatus, isConnecting, connectorId, connectors, connectWallet, disconnectWallet } = useConnector();
|
||||
const status = walletStatus.status;
|
||||
|
||||
const [connectingConnectorId, setConnectingConnectorId] = useState<WalletConnectorId | null>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [recentlyConnectedConnectorId, setRecentlyConnectedConnectorId] = useState<WalletConnectorId | null>(null);
|
||||
const [isOtherWalletsOpen, setIsOtherWalletsOpen] = useState(false);
|
||||
const [errorConnectorId, setErrorConnectorId] = useState<WalletConnectorId | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(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 (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent showCloseButton={false} className="max-w-md rounded-[24px] p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<DialogTitle className="text-lg font-semibold">Connect your wallet</DialogTitle>
|
||||
<DialogClose className="rounded-[16px] h-8 w-8 p-2 border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground inline-flex items-center justify-center cursor-pointer">
|
||||
<X className="h-3 w-3" />
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Error Alert */}
|
||||
{errorMessage && <ErrorAlert message={errorMessage} onDismiss={clearError} />}
|
||||
|
||||
{!isClient ? (
|
||||
<div className="text-center py-8">
|
||||
<Spinner className="h-6 w-6 mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">Detecting wallets...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{primaryWallets.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-2">
|
||||
{primaryWallets.map(connector => {
|
||||
const isThisConnecting =
|
||||
connectingConnectorId === connector.id ||
|
||||
(isConnecting && connectorId === connector.id);
|
||||
const isRecent = recentlyConnectedConnectorId === connector.id;
|
||||
const hasError = errorConnectorId === connector.id;
|
||||
return (
|
||||
<Button
|
||||
key={connector.id}
|
||||
variant="outline"
|
||||
className={`h-auto justify-between p-4 rounded-[16px] w-full ${
|
||||
hasError
|
||||
? 'border-destructive/50 bg-destructive/5 hover:bg-destructive/10'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleSelectWallet(connector)}
|
||||
disabled={isThisConnecting}
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-md">
|
||||
{connector.name}
|
||||
</span>
|
||||
{isRecent && (
|
||||
<Badge className="text-xs">Recent</Badge>
|
||||
)}
|
||||
</div>
|
||||
{isThisConnecting && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Connecting...
|
||||
</div>
|
||||
)}
|
||||
{hasError && !isConnecting && (
|
||||
<div className="text-xs text-destructive">
|
||||
Click to retry
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isThisConnecting && <Spinner className="h-4 w-4" />}
|
||||
<Avatar
|
||||
src={connector.icon}
|
||||
alt={connector.name}
|
||||
fallback={<Wallet className="h-5 w-5" />}
|
||||
className="h-10 w-10"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{otherWallets.length > 0 && (
|
||||
<>
|
||||
{primaryWallets.length > 0 && <Separator />}
|
||||
<Collapsible open={isOtherWalletsOpen} onOpenChange={setIsOtherWalletsOpen}>
|
||||
<CollapsibleTrigger className="w-full flex items-center justify-between border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground hover:no-underline dark:bg-input/30 dark:border-input dark:hover:bg-input/50 rounded-[16px] px-4 py-2 cursor-pointer active:scale-[0.98]">
|
||||
<span className="font-semibold text-md">Other Wallets</span>
|
||||
<HiddenWalletIcons wallets={otherWallets} className="shrink-0" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="grid gap-2 pt-2">
|
||||
{otherWallets.map(connector => {
|
||||
const isThisConnecting =
|
||||
connectingConnectorId === connector.id ||
|
||||
(isConnecting && connectorId === connector.id);
|
||||
const isRecent = recentlyConnectedConnectorId === connector.id;
|
||||
const hasError = errorConnectorId === connector.id;
|
||||
return (
|
||||
<Button
|
||||
key={connector.id}
|
||||
variant="outline"
|
||||
className={`h-auto justify-between p-4 rounded-[16px] w-full ${
|
||||
hasError
|
||||
? 'border-destructive/50 bg-destructive/5 hover:bg-destructive/10'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => handleSelectWallet(connector)}
|
||||
disabled={isThisConnecting}
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm">
|
||||
{connector.name}
|
||||
</span>
|
||||
{isRecent && (
|
||||
<Badge className="text-xs">
|
||||
Recent
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{isThisConnecting && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Connecting...
|
||||
</div>
|
||||
)}
|
||||
{hasError && !isConnecting && (
|
||||
<div className="text-xs text-destructive">
|
||||
Click to retry
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isThisConnecting && (
|
||||
<Spinner className="h-4 w-4" />
|
||||
)}
|
||||
<Avatar
|
||||
src={connector.icon}
|
||||
alt={connector.name}
|
||||
fallback={<Wallet className="h-5 w-5" />}
|
||||
className="h-10 w-10"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</>
|
||||
)}
|
||||
|
||||
{notReadyConnectors.length > 0 && (
|
||||
<>
|
||||
{(primaryWallets.length > 0 || otherWallets.length > 0) && <Separator />}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground px-1">
|
||||
{readyConnectors.length > 0 ? 'Unavailable Wallets' : 'Wallets'}
|
||||
</h3>
|
||||
<div className="grid gap-2">
|
||||
{notReadyConnectors.slice(0, 3).map(connector => {
|
||||
const installUrl = getInstallUrl(connector.name);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={connector.id}
|
||||
className="flex items-center justify-between p-4 rounded-[16px] w-full border bg-background shadow-xs"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar
|
||||
src={connector.icon}
|
||||
alt={connector.name}
|
||||
fallback={<Wallet className="h-4 w-4" />}
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-sm">
|
||||
{connector.name}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Not available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{installUrl && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 cursor-pointer"
|
||||
onClick={() => window.open(installUrl, '_blank')}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{connectors.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed p-8 text-center">
|
||||
<Wallet className="mx-auto h-12 w-12 text-muted-foreground mb-3" />
|
||||
<h3 className="font-semibold mb-2">No Wallets Detected</h3>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
Install a Solana wallet extension to get started
|
||||
</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button
|
||||
onClick={() => window.open('https://phantom.app', '_blank')}
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
Get Phantom
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.open('https://backpack.app', '_blank')}
|
||||
>
|
||||
Get Backpack
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user