"use client";
import { FC, useCallback, useState, useMemo, useEffect } from "react";
import {
useDynamicContext,
useUserWallets,
useSwitchWallet,
ChainEnum,
} from "@dynamic-labs/sdk-react-core";
import { WalletModal } from "./WalletModal";
import { ChainSelector } from "./ChainSelector";
import { SUPPORTED_CHAINS } from "@/lib/consts";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { ChevronDown } from "lucide-react";
export const HeadlessBridgeWidget: FC<{
className?: string;
sourceChain?: ChainEnum | string;
destinationChain?: ChainEnum | string;
availableChains?: typeof SUPPORTED_CHAINS;
}> = ({
className,
sourceChain: initialSourceChain = ChainEnum.Evm,
destinationChain: initialDestinationChain = ChainEnum.Sol,
availableChains = SUPPORTED_CHAINS,
}) => {
const { sdkHasLoaded, removeWallet, primaryWallet } = useDynamicContext();
const connectedWallets = useUserWallets();
const switchWallet = useSwitchWallet();
const [sourceChain, setSourceChain] = useState<ChainEnum | string>(
initialSourceChain
);
const [destinationChain, setDestinationChain] = useState<ChainEnum | string>(
initialDestinationChain
);
const [selectedChain, setSelectedChain] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSwitching, setIsSwitching] = useState(false);
const [selectedDestinationWalletId, setSelectedDestinationWalletId] =
useState<string | null>(null);
const sourceWallets = useMemo(() => {
return connectedWallets.filter(({ chain }) => chain === sourceChain);
}, [connectedWallets, sourceChain]);
const destinationWallets = useMemo(() => {
return connectedWallets.filter(({ chain }) => chain === destinationChain);
}, [connectedWallets, destinationChain]);
const sourceWallet = useMemo(() => {
// Source chain wallet should always be the primary wallet if available
if (primaryWallet && primaryWallet.chain === sourceChain) {
return primaryWallet;
}
return sourceWallets[0];
}, [sourceWallets, primaryWallet, sourceChain]);
const destinationWallet = useMemo(() => {
// For destination, use selected wallet or first available
if (selectedDestinationWalletId) {
const found = destinationWallets.find(
(w) => w.id === selectedDestinationWalletId
);
if (found) return found;
}
return destinationWallets[0];
}, [destinationWallets, selectedDestinationWalletId]);
// Ensure source chain wallet is always primary when it changes
useEffect(() => {
if (sourceWallet && sourceWallet.id !== primaryWallet?.id) {
switchWallet(sourceWallet.id).catch(() => {
// Silently handle errors
});
}
}, [sourceWallet, primaryWallet?.id, switchWallet]);
const handleConnectSource = useCallback(() => {
setSelectedChain(sourceChain);
setIsSwitching(false);
setIsModalOpen(true);
}, [sourceChain]);
const handleConnectDestination = useCallback(() => {
setSelectedChain(destinationChain);
setIsSwitching(false);
setIsModalOpen(true);
}, [destinationChain]);
const handleSwitchSource = useCallback(() => {
setSelectedChain(sourceChain);
setIsSwitching(true);
setIsModalOpen(true);
}, [sourceChain]);
const handleSwitchDestination = useCallback(() => {
setSelectedChain(destinationChain);
setIsSwitching(true);
setIsModalOpen(true);
}, [destinationChain]);
const handleDisconnectSource = useCallback(() => {
if (sourceWallet) {
removeWallet(sourceWallet.id);
}
}, [sourceWallet, removeWallet]);
const handleDisconnectDestination = useCallback(() => {
if (destinationWallet) {
removeWallet(destinationWallet.id);
}
}, [destinationWallet, removeWallet]);
const handleSelectSourceWallet = useCallback(
async (walletId: string) => {
// Switching source wallet makes it the primary wallet
await switchWallet(walletId);
},
[switchWallet]
);
const handleSelectDestinationWallet = useCallback((walletId: string) => {
// For destination, just track which one to display (don't make it primary)
setSelectedDestinationWalletId(walletId);
}, []);
const handleCloseModal = useCallback(() => {
setIsModalOpen(false);
setSelectedChain(null);
setIsSwitching(false);
}, []);
const handleSourceChainChange = useCallback((chain: ChainEnum) => {
setSourceChain(chain);
}, []);
const handleDestinationChainChange = useCallback((chain: ChainEnum) => {
setDestinationChain(chain);
}, []);
// Filter available chains to exclude the other selected chain
const availableSourceChains = useMemo(() => {
return availableChains.filter((c) => c.id !== destinationChain);
}, [availableChains, destinationChain]);
const availableDestinationChains = useMemo(() => {
return availableChains.filter((c) => c.id !== sourceChain);
}, [availableChains, sourceChain]);
const renderWalletDropdown = (
chain: string,
wallets: ReturnType<typeof useUserWallets>,
currentWallet: ReturnType<typeof useUserWallets>[0] | undefined,
onConnect: () => void,
onSwitch: () => void,
onDisconnect: () => void,
onSelectWallet: (walletId: string) => void,
canDisconnect: boolean
) => {
if (!currentWallet) {
return (
<button
onClick={onConnect}
type="button"
className="w-full px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors cursor-pointer"
>
Connect {chain}
</button>
);
}
const hasMultipleWallets = wallets.length > 1;
return (
<div className="space-y-2">
{hasMultipleWallets ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="w-full text-sm flex items-center justify-between p-3 rounded-lg border hover:bg-muted transition-colors text-left cursor-pointer"
>
<div className="flex-1">
<div className="font-medium">{chain}</div>
<div className="text-muted-foreground">
{currentWallet.address.slice(0, 6)}...
{currentWallet.address.slice(-4)}
</div>
</div>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-(--radix-dropdown-menu-trigger-width)"
>
{wallets.map((wallet) => (
<DropdownMenuItem
key={wallet.id}
onClick={() => onSelectWallet(wallet.id)}
className={wallet.id === currentWallet?.id ? "bg-muted" : ""}
>
<div className="flex-1">
<div className="font-medium">
{wallet.address.slice(0, 6)}...{wallet.address.slice(-4)}
</div>
{wallet.id === currentWallet?.id && (
<div className="text-xs text-muted-foreground">
Current
</div>
)}
</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onSwitch}>
Connect Different Wallet
</DropdownMenuItem>
{canDisconnect && (
<DropdownMenuItem onClick={onDisconnect} variant="destructive">
Disconnect
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
) : (
<div className="space-y-2">
<div className="text-sm flex-1 p-3 rounded-lg border">
<div className="font-medium">{chain}</div>
<div className="text-muted-foreground">
{currentWallet.address.slice(0, 6)}...
{currentWallet.address.slice(-4)}
</div>
</div>
<div className="flex gap-2">
<button
onClick={onSwitch}
type="button"
className="flex-1 text-xs text-primary hover:text-primary/80 px-2 py-1 rounded border border-primary/20 hover:border-primary/40 transition-colors cursor-pointer"
>
Connect Different Wallet
</button>
{canDisconnect && (
<button
onClick={onDisconnect}
type="button"
className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded border border-red-500/20 hover:border-red-500/40 transition-colors cursor-pointer"
>
Disconnect
</button>
)}
</div>
</div>
)}
</div>
);
};
return (
<>
<div className={className} data-testid="headless-bridge-widget">
<div className="space-y-4">
{/* Source Chain */}
<div className="space-y-2">
<ChainSelector
selectedChain={sourceChain}
availableChains={availableSourceChains}
onSelectChain={handleSourceChainChange}
label="Source Chain"
/>
{renderWalletDropdown(
sourceChain,
sourceWallets,
sourceWallet,
handleConnectSource,
handleSwitchSource,
handleDisconnectSource,
handleSelectSourceWallet,
connectedWallets.length > 1
)}
</div>
{/* Destination Chain */}
<div className="space-y-2">
<ChainSelector
selectedChain={destinationChain}
availableChains={availableDestinationChains}
onSelectChain={handleDestinationChainChange}
label="Destination Chain"
/>
{renderWalletDropdown(
destinationChain,
destinationWallets,
destinationWallet,
handleConnectDestination,
handleSwitchDestination,
handleDisconnectDestination,
handleSelectDestinationWallet,
connectedWallets.length > 1
)}
</div>
</div>
</div>
{selectedChain && (
<WalletModal
chain={selectedChain}
isOpen={isModalOpen}
onClose={handleCloseModal}
title={isSwitching ? `Switch ${selectedChain} Wallet` : undefined}
/>
)}
</>
);
};