Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: chain modal v2 #1806

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
40 changes: 40 additions & 0 deletions .changeset/sweet-plants-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"@rainbow-me/rainbowkit": minor
"example": patch
"site": patch
---

`<ConnectButton>` now allows you to switch chains without having your wallet connected.

You just need to set `chainModalOnConnect` prop to `true` in `<RainbowKitProvider>` if you'd like to use the chain modal without having your wallet connected.

With this update, the `<ConnectButton />` will display the chains even when only one chain is configured. You can disable this behaviour by setting `chainStatus` to `none`.

**Example usage**

```ts
import "@rainbow-me/rainbowkit/styles.css";

import { RainbowKitProvider, getDefaultConfig } from "@rainbow-me/rainbowkit";
import { WagmiProvider } from "wagmi";
import { mainnet } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const config = getDefaultConfig({
appName: "RainbowKit demo",
projectId: "YOUR_PROJECT_ID",
chains: [mainnet],
});

const queryClient = new QueryClient();

const App = () => {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider chainModalOnConnect>{/* Your App */}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
};
```
23 changes: 23 additions & 0 deletions packages/example/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ function RainbowKitApp({
const [coolModeEnabled, setCoolModeEnabled] = useState(false);
const [modalSize, setModalSize] = useState<ModalSize>('wide');
const [showDisclaimer, setShowDisclaimer] = useState(false);
const [chainModalOnConnect, setChainModalOnConnect] = useState(false);
const [customAvatar, setCustomAvatar] = useState(false);

const routerLocale = router.locale as Locale;
Expand Down Expand Up @@ -343,6 +344,7 @@ function RainbowKitApp({
...demoAppInfo,
...(showDisclaimer && { disclaimer: DisclaimerDemo }),
}}
chainModalOnConnect={chainModalOnConnect}
avatar={customAvatar ? CustomAvatar : undefined}
locale={locale}
coolMode={coolModeEnabled}
Expand Down Expand Up @@ -484,6 +486,27 @@ function RainbowKitApp({
/>
</td>
</tr>
<tr>
<td>
<label
htmlFor="chainModalOnConnect"
style={{ userSelect: 'none' }}
>
chainModalOnConnect
</label>
</td>
<td>
<input
checked={chainModalOnConnect}
id="chainModalOnConnect"
name="chainModalOnConnect"
onChange={(e) =>
setChainModalOnConnect(e.target.checked)
}
type="checkbox"
/>
</td>
</tr>
<tr>
<td>modalSize</td>
<td>
Expand Down
4 changes: 3 additions & 1 deletion packages/rainbowkit/src/components/ChainModal/Chain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useRainbowKitChains } from '../RainbowKitProvider/RainbowKitChainContex
import { Text } from '../Text/Text';

interface ChainProps {
isConnected?: boolean;
chainId: number;
currentChainId: number;
switchChain: ReturnType<typeof useSwitchChain>['switchChain'];
Expand All @@ -31,6 +32,7 @@ const Chain = ({
name,
iconBackground,
idx,
isConnected,
}: ChainProps) => {
const mobile = isMobile();
const { i18n } = useContext(I18nContext);
Expand Down Expand Up @@ -74,7 +76,7 @@ const Chain = ({
)}
<div>{name ?? name}</div>
</Box>
{isCurrentChain && (
{isCurrentChain && isConnected && (
<Box
alignItems="center"
display="flex"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useContext, useState } from 'react';
import { useAccount, useDisconnect, useSwitchChain } from 'wagmi';
import { useAccount, useChainId, useDisconnect, useSwitchChain } from 'wagmi';
import { useConfig } from 'wagmi';
import { isMobile } from '../../utils/isMobile';
import { Box } from '../Box/Box';
Expand All @@ -23,7 +23,9 @@ export interface ChainModalProps {
}

export function ChainModal({ onClose, open }: ChainModalProps) {
const { chainId } = useAccount();
const { chainId: connectedChainId, isConnected } = useAccount();
const currentChainId = useChainId();
const chainId = isConnected ? connectedChainId : currentChainId;
const { chains } = useConfig();
const [pendingChainId, setPendingChainId] = useState<number | null>(null);
const { switchChain } = useSwitchChain({
Expand Down Expand Up @@ -102,6 +104,7 @@ export function ChainModal({ onClose, open }: ChainModalProps) {
chainId={id}
currentChainId={chainId}
switchChain={switchChain}
isConnected={isConnected}
chainIconSize={chainIconSize}
isLoading={pendingChainId === id}
src={iconUrl}
Expand Down
198 changes: 114 additions & 84 deletions packages/rainbowkit/src/components/ConnectButton/ConnectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { Avatar } from '../Avatar/Avatar';
import { Box } from '../Box/Box';
import { DropdownIcon } from '../Icons/Dropdown';
import { I18nContext } from '../RainbowKitProvider/I18nContext';
import { useRainbowKitChains } from '../RainbowKitProvider/RainbowKitChainContext';
import {
useChainModalOnConnect,
useInitialChainId,
useRainbowKitChains,
} from '../RainbowKitProvider/RainbowKitChainContext';
import { useShowBalance } from '../RainbowKitProvider/ShowBalanceContext';
import { ConnectButtonRenderer } from './ConnectButtonRenderer';

Expand Down Expand Up @@ -41,17 +45,34 @@ export function ConnectButton({
}: ConnectButtonProps) {
const chains = useRainbowKitChains();
const connectionStatus = useConnectionStatus();
const chainModalOnConnect = useChainModalOnConnect();
const { setShowBalance } = useShowBalance();
const [ready, setReady] = useState(false);

const initialChainId = useInitialChainId();
const { i18n } = useContext(I18nContext);
const [ready, setReady] = useState(false);

// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
setShowBalance(showBalance);
if (!ready) setReady(true);
}, [showBalance, setShowBalance]);

const computeResponsiveChainStatus = () => {
if (typeof chainStatus === 'string') {
return chainStatus;
}

if (chainStatus) {
return normalizeResponsiveValue(chainStatus)[
isMobile() ? 'smallScreen' : 'largeScreen'
];
}
};

const responsiveChainStatus = computeResponsiveChainStatus();

const isChainStatusHidden = responsiveChainStatus === 'none';

return ready ? (
<ConnectButtonRenderer>
{({
Expand All @@ -62,9 +83,18 @@ export function ConnectButton({
openChainModal,
openConnectModal,
}) => {
const isChainSwitchingDisabled =
((initialChainId || !chainModalOnConnect) &&
connectionStatus !== 'connected') ||
connectionStatus === 'unauthenticated';

const ready = mounted && connectionStatus !== 'loading';
const unsupportedChain = chain?.unsupported ?? false;

const hasMultipleChains = chains.length > 1;
const canShowCurrentChain =
chain && (!isChainStatusHidden || hasMultipleChains);

return (
<Box
display="flex"
Expand All @@ -78,98 +108,98 @@ export function ConnectButton({
},
})}
>
{ready && account && connectionStatus === 'connected' ? (
<>
{chain && (chains.length > 1 || unsupportedChain) && (
{ready && !isChainSwitchingDisabled && canShowCurrentChain && (
<Box
alignItems="center"
aria-label="Chain Selector"
as="button"
background={
unsupportedChain
? 'connectButtonBackgroundError'
: 'connectButtonBackground'
}
borderRadius="connectButton"
boxShadow="connectButton"
className={touchableStyles({
active: 'shrink',
hover: 'grow',
})}
color={
unsupportedChain
? 'connectButtonTextError'
: 'connectButtonText'
}
display={mapResponsiveValue(chainStatus, (value) =>
value === 'none' ? 'none' : 'flex',
)}
fontFamily="body"
fontWeight="bold"
gap="6"
key={
// Force re-mount to prevent CSS transition
unsupportedChain ? 'unsupported' : 'supported'
}
onClick={openChainModal}
paddingX="10"
paddingY="8"
testId={
unsupportedChain ? 'wrong-network-button' : 'chain-button'
}
transition="default"
type="button"
>
{unsupportedChain ? (
<Box
alignItems="center"
aria-label="Chain Selector"
as="button"
background={
unsupportedChain
? 'connectButtonBackgroundError'
: 'connectButtonBackground'
}
borderRadius="connectButton"
boxShadow="connectButton"
className={touchableStyles({
active: 'shrink',
hover: 'grow',
})}
color={
unsupportedChain
? 'connectButtonTextError'
: 'connectButtonText'
}
display={mapResponsiveValue(chainStatus, (value) =>
value === 'none' ? 'none' : 'flex',
)}
fontFamily="body"
fontWeight="bold"
gap="6"
key={
// Force re-mount to prevent CSS transition
unsupportedChain ? 'unsupported' : 'supported'
}
onClick={openChainModal}
paddingX="10"
paddingY="8"
testId={
unsupportedChain ? 'wrong-network-button' : 'chain-button'
}
transition="default"
type="button"
display="flex"
height="24"
paddingX="4"
>
{unsupportedChain ? (
{i18n.t('connect_wallet.wrong_network.label')}
</Box>
) : (
<Box alignItems="center" display="flex" gap="6">
{chain.hasIcon ? (
<Box
alignItems="center"
display="flex"
display={mapResponsiveValue(chainStatus, (value) =>
value === 'full' || value === 'icon'
? 'block'
: 'none',
)}
height="24"
paddingX="4"
width="24"
>
{i18n.t('connect_wallet.wrong_network.label')}
<AsyncImage
alt={chain.name ?? 'Chain icon'}
background={chain.iconBackground}
borderRadius="full"
height="24"
src={chain.iconUrl}
width="24"
/>
</Box>
) : (
<Box alignItems="center" display="flex" gap="6">
{chain.hasIcon ? (
<Box
display={mapResponsiveValue(chainStatus, (value) =>
value === 'full' || value === 'icon'
? 'block'
: 'none',
)}
height="24"
width="24"
>
<AsyncImage
alt={chain.name ?? 'Chain icon'}
background={chain.iconBackground}
borderRadius="full"
height="24"
src={chain.iconUrl}
width="24"
/>
</Box>
) : null}
<Box
display={mapResponsiveValue(chainStatus, (value) => {
if (value === 'icon' && !chain.iconUrl) {
return 'block'; // Show the chain name if there is no iconUrl
}
) : null}
<Box
display={mapResponsiveValue(chainStatus, (value) => {
if (value === 'icon' && !chain.iconUrl) {
return 'block'; // Show the chain name if there is no iconUrl
}

return value === 'full' || value === 'name'
? 'block'
: 'none';
})}
>
{chain.name ?? chain.id}
</Box>
</Box>
)}
<DropdownIcon />
return value === 'full' || value === 'name'
? 'block'
: 'none';
})}
>
{chain.name ?? chain.id}
</Box>
</Box>
)}
<DropdownIcon />
</Box>
)}

{ready && account && connectionStatus === 'connected' ? (
<>
{!unsupportedChain && (
<Box
alignItems="center"
Expand Down