Skip to content

Commit

Permalink
feat: New sidebar wallet button design (#181)
Browse files Browse the repository at this point in the history
* Group addresses by protocol and show wallet logos

* Update WalletControlBar and TransfersDetailsModal
  • Loading branch information
jmrossy authored Jun 18, 2024
1 parent 59c04d1 commit c04400d
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 39 deletions.
23 changes: 23 additions & 0 deletions src/components/icons/WalletLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Image from 'next/image';

import { WalletDetails } from '../../features/wallet/hooks/types';
import Wallet from '../../images/icons/wallet.svg';

export function WalletLogo({
walletDetails,
size,
}: {
walletDetails: WalletDetails;
size?: number;
}) {
return (
<Image
src={walletDetails.logoUrl || Wallet}
alt=""
width={size}
height={size}
style={{ backgroundColor: !walletDetails.logoUrl ? walletDetails.logoAccent : undefined }}
className="rounded-full p-0.5"
/>
);
}
7 changes: 5 additions & 2 deletions src/features/transfer/TransfersDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Image from 'next/image';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { ProtocolType } from '@hyperlane-xyz/utils';
import { MessageStatus, MessageTimeline, useMessageTimeline } from '@hyperlane-xyz/widgets';

import { Spinner } from '../../components/animation/Spinner';
Expand All @@ -22,7 +23,7 @@ import {
isTransferSent,
} from '../../utils/transfer';
import { getChainDisplayName, hasPermissionlessChain } from '../chains/utils';
import { useAccountForChain } from '../wallet/hooks/multiProtocol';
import { useAccountForChain, useWalletDetails } from '../wallet/hooks/multiProtocol';

import { TransferContext, TransferStatus } from './types';

Expand Down Expand Up @@ -53,6 +54,8 @@ export function TransfersDetailsModal({
} = transfer || {};

const account = useAccountForChain(origin);
const walletDetails = useWalletDetails()[account?.protocol || ProtocolType.Ethereum];

const multiProvider = getMultiProvider();

const getMessageUrls = useCallback(async () => {
Expand Down Expand Up @@ -80,7 +83,7 @@ export function TransfersDetailsModal({
}, [transfer, getMessageUrls]);

const isAccountReady = !!account?.isReady;
const connectorName = account?.connectorName || 'wallet';
const connectorName = walletDetails.name || 'wallet';
const token = getWarpCore().findToken(origin, originTokenAddressOrDenom);

const isPermissionlessRoute = hasPermissionlessChain([destination, origin]);
Expand Down
38 changes: 21 additions & 17 deletions src/features/wallet/SideBarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { toast } from 'react-toastify';

import { SmallSpinner } from '../../components/animation/SmallSpinner';
import { ChainLogo } from '../../components/icons/ChainLogo';
import { Identicon } from '../../components/icons/Identicon';
import { PLACEHOLDER_COSMOS_CHAIN } from '../../consts/values';
import { WalletLogo } from '../../components/icons/WalletLogo';
import { tryFindToken } from '../../context/context';
import ArrowRightIcon from '../../images/icons/arrow-right.svg';
import CollapseIcon from '../../images/icons/collapse-icon.svg';
Expand All @@ -19,7 +18,7 @@ import { useStore } from '../store';
import { TransfersDetailsModal } from '../transfer/TransfersDetailsModal';
import { TransferContext } from '../transfer/types';

import { useAccounts, useDisconnectFns } from './hooks/multiProtocol';
import { useAccounts, useDisconnectFns, useWalletDetails } from './hooks/multiProtocol';
import { AccountInfo } from './hooks/types';

export function SideBarMenu({
Expand Down Expand Up @@ -88,12 +87,9 @@ export function SideBarMenu({
Connected Wallets
</div>
<div className="my-3 px-3 space-y-3">
{readyAccounts.map((acc, i) =>
acc.addresses.map((addr, j) => {
if (addr?.chainName?.includes(PLACEHOLDER_COSMOS_CHAIN)) return null;
return <AccountSummary key={`${i}-${j}`} account={acc} address={addr.address} />;
}),
)}
{readyAccounts.map((acc, i) => (
<AccountSummary key={i} account={acc} />
))}
<button onClick={onConnectWallet} className={styles.btn}>
<Icon src={Wallet} alt="" size={18} />
<div className="ml-2">Connect wallet</div>
Expand Down Expand Up @@ -143,25 +139,33 @@ export function SideBarMenu({
);
}

function AccountSummary({ account, address }: { account: AccountInfo; address: Address }) {
function AccountSummary({ account }: { account: AccountInfo }) {
const numAddresses = account?.addresses?.length || 0;
const onlyAddress = numAddresses === 1 ? account.addresses[0].address : undefined;

const onClickCopy = async () => {
if (!address) return;
await tryClipboardSet(address);
if (!onlyAddress) return;
await tryClipboardSet(account.addresses[0].address);
toast.success('Address copied to clipboard', { autoClose: 2000 });
};

const walletDetails = useWalletDetails()[account.protocol];

return (
<button
key={address}
onClick={onClickCopy}
className={`${styles.btn} border border-gray-200 rounded-xl`}
className={`${styles.btn} border border-gray-200 rounded-xl ${
numAddresses > 1 && 'cursor-default'
}`}
>
<div className="shrink-0">
<Identicon address={address} size={40} />
<WalletLogo walletDetails={walletDetails} size={42} />
</div>
<div className="flex flex-col mx-3 items-start">
<div className="text-gray-800 text-sm font-normal">{account.connectorName || 'Wallet'}</div>
<div className="text-xs text-left truncate w-64">{address ? address : 'Unknown'}</div>
<div className="text-gray-800 text-sm font-normal">{walletDetails.name || 'Wallet'}</div>
<div className="text-xs text-left truncate w-64">
{onlyAddress || `${numAddresses} known addresses`}
</div>
</div>
</button>
);
Expand Down
18 changes: 10 additions & 8 deletions src/features/wallet/WalletControlBar.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import Image from 'next/image';
import { useState } from 'react';

import { shortenAddress } from '@hyperlane-xyz/utils';
import { ProtocolType, shortenAddress } from '@hyperlane-xyz/utils';

import { SolidButton } from '../../components/buttons/SolidButton';
import { Identicon } from '../../components/icons/Identicon';
import { WalletLogo } from '../../components/icons/WalletLogo';
import Wallet from '../../images/icons/wallet.svg';
import { useIsSsr } from '../../utils/ssr';

import { SideBarMenu } from './SideBarMenu';
import { WalletEnvSelectionModal } from './WalletEnvSelectionModal';
import { useAccounts } from './hooks/multiProtocol';
import { useAccounts, useWalletDetails } from './hooks/multiProtocol';

export function WalletControlBar() {
const isSsr = useIsSsr();

const [showEnvSelectModal, setShowEnvSelectModal] = useState(false);
const [isSideBarOpen, setIsSideBarOpen] = useState(false);

const { readyAccounts } = useAccounts();
const isSsr = useIsSsr();
const walletDetails = useWalletDetails();

const numReady = readyAccounts.length;
const firstAccount = readyAccounts[0];
const firstWallet = walletDetails[firstAccount?.protocol || ProtocolType.Ethereum];

if (isSsr) {
// https://github.com/wagmi-dev/wagmi/issues/542#issuecomment-1144178142
Expand All @@ -44,11 +48,9 @@ export function WalletControlBar() {
{numReady === 1 && (
<SolidButton onClick={() => setIsSideBarOpen(true)} classes="px-2.5 py-1" color="white">
<div className="flex items-center justify-center">
<Identicon address={readyAccounts[0].addresses[0]?.address} size={26} />
<WalletLogo walletDetails={firstWallet} size={26} />
<div className="flex flex-col mx-3 items-start">
<div className="text-xs text-gray-500">
{readyAccounts[0].connectorName || 'Wallet'}
</div>
<div className="text-xs text-gray-500">{firstWallet.name || 'Wallet'}</div>
<div className="text-xs">
{readyAccounts[0].addresses.length
? shortenAddress(readyAccounts[0].addresses[0].address, true)
Expand Down
22 changes: 20 additions & 2 deletions src/features/wallet/hooks/cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { logger } from '../../../utils/logger';
import { getCosmosChainNames } from '../../chains/metadata';
import { getChainMetadata } from '../../chains/utils';

import { AccountInfo, ActiveChainInfo, ChainAddress, ChainTransactionFns } from './types';
import {
AccountInfo,
ActiveChainInfo,
ChainAddress,
ChainTransactionFns,
WalletDetails,
} from './types';

export function useCosmosAccount(): AccountInfo {
const chainToContext = useChains(getCosmosChainNames());
Expand All @@ -31,12 +37,24 @@ export function useCosmosAccount(): AccountInfo {
protocol: ProtocolType.Cosmos,
addresses,
publicKey,
connectorName,
isReady,
};
}, [chainToContext]);
}

export function useCosmosWalletDetails() {
const { wallet } = useChain(PLACEHOLDER_COSMOS_CHAIN);
const { logo, prettyName } = wallet || {};

return useMemo<WalletDetails>(
() => ({
name: prettyName,
logoUrl: typeof logo === 'string' ? logo : undefined,
}),
[prettyName, logo],
);
}

export function useCosmosConnectFn(): () => void {
const { openView } = useChain(PLACEHOLDER_COSMOS_CHAIN);
return openView;
Expand Down
40 changes: 36 additions & 4 deletions src/features/wallet/hooks/evm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useConnectModal } from '@rainbow-me/rainbowkit';
import { useQuery } from '@tanstack/react-query';
import { getNetwork, sendTransaction, switchNetwork, waitForTransaction } from '@wagmi/core';
import { useCallback, useMemo } from 'react';
import { useAccount, useDisconnect, useNetwork } from 'wagmi';
Expand All @@ -10,21 +11,52 @@ import { logger } from '../../../utils/logger';
import { getChainMetadata, tryGetChainMetadata } from '../../chains/utils';
import { ethers5TxToWagmiTx } from '../utils';

import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types';
import { AccountInfo, ActiveChainInfo, ChainTransactionFns, WalletDetails } from './types';

export function useEvmAccount(): AccountInfo {
const { address, isConnected, connector } = useAccount();
const isReady = !!(address && isConnected && connector);
const connectorName = connector?.name;

return useMemo<AccountInfo>(
() => ({
protocol: ProtocolType.Ethereum,
addresses: address ? [{ address: `${address}` }] : [],
connectorName: connectorName,
isReady: isReady,
}),
[address, connectorName, isReady],
[address, isReady],
);
}

export function useEvmWalletDetails() {
const { connector } = useAccount();
const name = connector?.name;
// @ts-ignore RainbowKit hooks on this extra useful info but it's not typed
const rainbowKitWalletDetails = connector?._wallets?.[0];

const { data } = useQuery({
queryKey: ['useEvmWalletDetails', name],
queryFn: async () => {
logger.debug('Fetching wallet details');
if (!rainbowKitWalletDetails) return null;
const { iconUrl, iconAccent: logoAccent } = rainbowKitWalletDetails;
let logoUrl: string | undefined = undefined;
if (typeof iconUrl === 'function') {
logoUrl = await iconUrl();
} else if (typeof iconUrl === 'string') {
logoUrl = iconUrl;
}
return { logoUrl, logoAccent };
},
staleTime: Infinity,
});

return useMemo<WalletDetails>(
() => ({
name,
logoUrl: data?.logoUrl,
logoAccent: data?.logoAccent,
}),
[name, data],
);
}

Expand Down
20 changes: 19 additions & 1 deletion src/features/wallet/hooks/multiProtocol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,25 @@ import {
useCosmosConnectFn,
useCosmosDisconnectFn,
useCosmosTransactionFns,
useCosmosWalletDetails,
} from './cosmos';
import {
useEvmAccount,
useEvmActiveChain,
useEvmConnectFn,
useEvmDisconnectFn,
useEvmTransactionFns,
useEvmWalletDetails,
} from './evm';
import {
useSolAccount,
useSolActiveChain,
useSolConnectFn,
useSolDisconnectFn,
useSolTransactionFns,
useSolWalletDetails,
} from './solana';
import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types';
import { AccountInfo, ActiveChainInfo, ChainTransactionFns, WalletDetails } from './types';

export function useAccounts(): {
accounts: Record<ProtocolType, AccountInfo>;
Expand Down Expand Up @@ -104,6 +107,21 @@ export function getAccountAddressAndPubKey(
return { address, publicKey };
}

export function useWalletDetails(): Record<ProtocolType, WalletDetails> {
const evmWallet = useEvmWalletDetails();
const solWallet = useSolWalletDetails();
const cosmosWallet = useCosmosWalletDetails();

return useMemo(
() => ({
[ProtocolType.Ethereum]: evmWallet,
[ProtocolType.Sealevel]: solWallet,
[ProtocolType.Cosmos]: cosmosWallet,
}),
[evmWallet, solWallet, cosmosWallet],
);
}

export function useConnectFns(): Record<ProtocolType, () => void> {
const onConnectEthereum = useEvmConnectFn();
const onConnectSolana = useSolConnectFn();
Expand Down
19 changes: 15 additions & 4 deletions src/features/wallet/hooks/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,33 @@ import { getMultiProvider } from '../../../context/context';
import { logger } from '../../../utils/logger';
import { getChainByRpcEndpoint } from '../../chains/utils';

import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types';
import { AccountInfo, ActiveChainInfo, ChainTransactionFns, WalletDetails } from './types';

export function useSolAccount(): AccountInfo {
const { publicKey, connected, wallet } = useWallet();
const isReady = !!(publicKey && wallet && connected);
const address = publicKey?.toBase58();
const connectorName = wallet?.adapter?.name;

return useMemo<AccountInfo>(
() => ({
protocol: ProtocolType.Sealevel,
addresses: address ? [{ address: address }] : [],
connectorName: connectorName,
isReady: isReady,
}),
[address, connectorName, isReady],
[address, isReady],
);
}

export function useSolWalletDetails() {
const { wallet } = useWallet();
const { name, icon } = wallet?.adapter || {};

return useMemo<WalletDetails>(
() => ({
name,
logoUrl: icon,
}),
[name, icon],
);
}

Expand Down
7 changes: 6 additions & 1 deletion src/features/wallet/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ export interface AccountInfo {
// And another Cosmos exception, public keys are needed
// for tx simulation and gas estimation
publicKey?: Promise<HexString>;
connectorName?: string;
isReady: boolean;
}

export interface WalletDetails {
name?: string;
logoUrl?: string;
logoAccent?: string;
}

export interface ActiveChainInfo {
chainDisplayName?: string;
chainName?: ChainName;
Expand Down

0 comments on commit c04400d

Please sign in to comment.