From 4aebca2e20506371fb339e9d77a940cdbc90236c Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Thu, 12 Dec 2024 19:23:04 -0800 Subject: [PATCH] feat: miscellaneous chain icon changes --- .changeset/metal-mails-ring.md | 6 + .../react/web/ui/ConnectWallet/Details.tsx | 248 +++++++++++------- .../web/ui/ConnectWallet/NetworkSelector.tsx | 119 +++++---- .../web/ui/components/ChainActiveDot.tsx | 17 ++ .../src/react/web/ui/components/ChainIcon.tsx | 19 +- .../web/ui/components/fallbackChainIcon.ts | 6 + .../react/web/ui/prebuilt/Chain/icon.test.tsx | 129 +++++++++ .../src/react/web/ui/prebuilt/Chain/icon.tsx | 84 +++--- 8 files changed, 437 insertions(+), 191 deletions(-) create mode 100644 .changeset/metal-mails-ring.md create mode 100644 packages/thirdweb/src/react/web/ui/components/ChainActiveDot.tsx create mode 100644 packages/thirdweb/src/react/web/ui/components/fallbackChainIcon.ts create mode 100644 packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.test.tsx diff --git a/.changeset/metal-mails-ring.md b/.changeset/metal-mails-ring.md new file mode 100644 index 00000000000..48b5c22ccf1 --- /dev/null +++ b/.changeset/metal-mails-ring.md @@ -0,0 +1,6 @@ +--- +"thirdweb": patch +--- +- Small fix for ChainIcon: Always resolve IPFS URI + +- Improve test coverage \ No newline at end of file diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx index 6aa59573a4d..a1f2af13600 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx @@ -9,7 +9,15 @@ import { TextAlignJustifyIcon, } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; -import { type JSX, useCallback, useContext, useEffect, useState } from "react"; +import { + type Dispatch, + type JSX, + type SetStateAction, + useCallback, + useContext, + useEffect, + useState, +} from "react"; import { trackPayEvent } from "../../../../analytics/track/pay.js"; import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; @@ -51,11 +59,7 @@ import type { ConnectButton_detailsModalOptions, PayUIOptions, } from "../../../core/hooks/connection/ConnectButtonProps.js"; -import { - useChainFaucets, - useChainIconUrl, - useChainName, -} from "../../../core/hooks/others/useChainQuery.js"; +import { useChainFaucets } from "../../../core/hooks/others/useChainQuery.js"; import { useActiveAccount } from "../../../core/hooks/wallets/useActiveAccount.js"; import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js"; import { useActiveWalletChain } from "../../../core/hooks/wallets/useActiveWalletChain.js"; @@ -70,7 +74,7 @@ import type { import { hasSmartAccount } from "../../../core/utils/isSmartWallet.js"; import { useWalletInfo } from "../../../core/utils/wallet.js"; import { WalletUIStatesProvider } from "../../providers/wallet-ui-states-provider.js"; -import { ChainIcon } from "../components/ChainIcon.js"; +import { ChainActiveDot } from "../components/ChainActiveDot.js"; import { CopyIcon } from "../components/CopyIcon.js"; import { IconContainer } from "../components/IconContainer.js"; import { Modal } from "../components/Modal.js"; @@ -81,6 +85,7 @@ import { ToolTip } from "../components/Tooltip.js"; import { WalletImage } from "../components/WalletImage.js"; import { Container, Line } from "../components/basic.js"; import { Button, IconButton } from "../components/buttons.js"; +import { fallbackChainIcon } from "../components/fallbackChainIcon.js"; import { Link, Text } from "../components/text.js"; import { fadeInAnimation } from "../design-system/animations.js"; import { StyledButton } from "../design-system/elements.js"; @@ -95,6 +100,9 @@ import { import { AccountBlobbie } from "../prebuilt/Account/blobbie.js"; import { AccountName } from "../prebuilt/Account/name.js"; import { AccountProvider } from "../prebuilt/Account/provider.js"; +import { ChainIcon } from "../prebuilt/Chain/icon.js"; +import { ChainName } from "../prebuilt/Chain/name.js"; +import { ChainProvider } from "../prebuilt/Chain/provider.js"; import type { LocaleId } from "../types.js"; import { MenuButton, MenuLink } from "./MenuButton.js"; import { ScreenSetupContext, useSetupScreen } from "./Modal/screen.js"; @@ -386,8 +394,6 @@ export function DetailsModal(props: { const theme = parseTheme(props.theme); const activeWallet = useActiveWallet(); - const chainIconQuery = useChainIconUrl(walletChain); - const chainNameQuery = useChainName(walletChain); const chainFaucetsQuery = useChainFaucets(walletChain); const disableSwitchChain = !activeWallet?.switchChain; @@ -417,93 +423,6 @@ export function DetailsModal(props: { } }, [activeAccount, closeModal]); - const networkSwitcherButton = ( - { - setScreen("network-switcher"); - }} - data-variant="primary" - > -
- {!chainIconQuery.isLoading ? ( - - ) : ( - - )} -
- - {chainNameQuery.isLoading ? ( - - ) : ( - - {chainNameQuery.name || `Unknown chain #${walletChain?.id}`} - - {props.showBalanceInFiat ? ( - <> - } - loadingComponent={} - chain={walletChain} - tokenAddress={ - props.displayBalanceToken?.[Number(walletChain?.id)] - } - formatFn={(props: AccountBalanceInfo) => - formatAccountTokenBalance({ ...props, decimals: 7 }) - } - />{" "} - } - chain={walletChain} - tokenAddress={ - props.displayBalanceToken?.[Number(walletChain?.id)] - } - formatFn={(props: AccountBalanceInfo) => - ` (${formatAccountFiatBalance({ ...props, decimals: 3 })})` - } - showBalanceInFiat="USD" - /> - - ) : ( - } - loadingComponent={} - formatFn={(props: AccountBalanceInfo) => - formatAccountTokenBalance({ ...props, decimals: 7 }) - } - chain={walletChain} - tokenAddress={ - props.displayBalanceToken?.[Number(walletChain?.id)] - } - /> - )} - - - )} - - -
- ); - const { hideSendFunds, hideReceiveFunds, hideBuyFunds } = props.detailsModal || {}; @@ -749,7 +668,13 @@ export function DetailsModal(props: { }} > {/* Network Switcher */} - {networkSwitcherButton} + setScreen("network-switcher")} + disableSwitchChain={disableSwitchChain} + showBalanceInFiat={props.detailsModal?.showBalanceInFiat} + displayBalanceToken={props.displayBalanceToken} + /> {/* Transactions */} >; + disableSwitchChain: boolean; + displayBalanceToken: Record | undefined; + client: ThirdwebClient; + showBalanceInFiat?: SupportedFiatCurrency; +}) { + const { disableSwitchChain, setScreen, showBalanceInFiat, client } = props; + const walletChain = useActiveWalletChain(); + if (!walletChain) { + return null; + } + return ( + { + setScreen("network-switcher"); + }} + data-variant="primary" + > + +
+ + + } + fallbackComponent={ + + } + style={{ + width: `${iconSize.md}px`, + height: `${iconSize.md}px`, + }} + /> + + +
+ + + } + fallbackComponent={Unknown chain #{walletChain?.id}} + /> + + {showBalanceInFiat ? ( + <> + } + loadingComponent={} + chain={walletChain} + tokenAddress={ + props.displayBalanceToken?.[Number(walletChain?.id)] + } + formatFn={(props: AccountBalanceInfo) => + formatAccountTokenBalance({ ...props, decimals: 7 }) + } + />{" "} + } + chain={walletChain} + tokenAddress={ + props.displayBalanceToken?.[Number(walletChain?.id)] + } + formatFn={(props: AccountBalanceInfo) => + ` (${formatAccountFiatBalance({ ...props, decimals: 3 })})` + } + showBalanceInFiat="USD" + /> + + ) : ( + } + loadingComponent={} + formatFn={(props: AccountBalanceInfo) => + formatAccountTokenBalance({ ...props, decimals: 7 }) + } + chain={walletChain} + tokenAddress={ + props.displayBalanceToken?.[Number(walletChain?.id)] + } + /> + )} + + +
+ + +
+ ); +} + const WalletInfoButton = /* @__PURE__ */ StyledButton((_) => { const theme = useCustomTheme(); return { diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx index b0ce9861ec4..a35acb297fe 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx @@ -28,26 +28,28 @@ import { spacing, } from "../../../core/design-system/index.js"; import { - useChainIconUrl, useChainName, useChainsQuery, } from "../../../core/hooks/others/useChainQuery.js"; import { useActiveWalletChain } from "../../../core/hooks/wallets/useActiveWalletChain.js"; import { useSwitchActiveWalletChain } from "../../../core/hooks/wallets/useSwitchActiveWalletChain.js"; import { SetRootElementContext } from "../../../core/providers/RootElementContext.js"; -import { ChainIcon } from "../components/ChainIcon.js"; +import { ChainActiveDot } from "../components/ChainActiveDot.js"; import { Modal } from "../components/Modal.js"; import { Skeleton } from "../components/Skeleton.js"; import { Spacer } from "../components/Spacer.js"; import { Spinner } from "../components/Spinner.js"; import { Container, Line, ModalHeader } from "../components/basic.js"; import { Button } from "../components/buttons.js"; +import { fallbackChainIcon } from "../components/fallbackChainIcon.js"; import { Input } from "../components/formElements.js"; import { ModalTitle } from "../components/modalElements.js"; import { Text } from "../components/text.js"; import { StyledButton, StyledP, StyledUl } from "../design-system/elements.js"; import { useDebouncedValue } from "../hooks/useDebouncedValue.js"; import { useShowMore } from "../hooks/useShowMore.js"; +import { ChainIcon } from "../prebuilt/Chain/icon.js"; +import { ChainProvider } from "../prebuilt/Chain/provider.js"; import type { LocaleId } from "../types.js"; import { getConnectLocale } from "./locale/getConnectLocale.js"; import type { ConnectLocale } from "./locale/types.js"; @@ -640,7 +642,6 @@ export const ChainButton = /* @__PURE__ */ memo(function ChainButton(props: { const activeChain = useActiveWalletChain(); const chainNameQuery = useChainName(chain); - const chainIconQuery = useChainIconUrl(chain); let chainName: React.ReactNode; if (chainNameQuery.name) { @@ -650,54 +651,78 @@ export const ChainButton = /* @__PURE__ */ memo(function ChainButton(props: { } return ( - - {!chainIconQuery.isLoading ? ( - - ) : ( - - )} - - {confirming || switchingFailed ? ( -
+ + - {chainName} - - {confirming && ( - <> - - {locale.confirmInWallet} - - - - )} - - {switchingFailed && ( - - - {locale.networkSelector.failedToSwitch} - - - )} - -
- ) : ( - chainName - )} -
+ + } + fallbackComponent={ + + } + style={{ + width: `${iconSize.lg}px`, + height: `${iconSize.lg}px`, + }} + loading="lazy" + /> + {activeChain?.id === chain.id && } + + {confirming || switchingFailed ? ( +
+ {chainName} + + {confirming && ( + <> + + {locale.confirmInWallet} + + + + )} + + {switchingFailed && ( + + + {locale.networkSelector.failedToSwitch} + + + )} + +
+ ) : ( + chainName + )} + + ); }); diff --git a/packages/thirdweb/src/react/web/ui/components/ChainActiveDot.tsx b/packages/thirdweb/src/react/web/ui/components/ChainActiveDot.tsx new file mode 100644 index 00000000000..af7015e08eb --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/components/ChainActiveDot.tsx @@ -0,0 +1,17 @@ +import { StyledDiv } from "../design-system/elements.js"; + +/** + * The greet dot that is placed at the corner of the chain icon - + * indicating that the chain is currently active (connected to) + * @internal + */ +export const ChainActiveDot = /* @__PURE__ */ StyledDiv({ + width: "28%", + height: "28%", + borderRadius: "50%", + position: "absolute", + bottom: 0, + right: 0, + backgroundColor: "#00d395", + boxShadow: "0 0 0 2px var(--bg)", +}); diff --git a/packages/thirdweb/src/react/web/ui/components/ChainIcon.tsx b/packages/thirdweb/src/react/web/ui/components/ChainIcon.tsx index 3f213c0ca6e..fba534115f1 100644 --- a/packages/thirdweb/src/react/web/ui/components/ChainIcon.tsx +++ b/packages/thirdweb/src/react/web/ui/components/ChainIcon.tsx @@ -1,11 +1,9 @@ import type { ThirdwebClient } from "../../../../client/client.js"; import { resolveScheme } from "../../../../utils/ipfs.js"; -import { StyledDiv } from "../design-system/elements.js"; +import { ChainActiveDot } from "./ChainActiveDot.js"; import { Img } from "./Img.js"; import { Container } from "./basic.js"; - -const fallbackChainIcon = - "data:image/svg+xml;charset=UTF-8,%3csvg width='15' height='14' viewBox='0 0 15 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M7 8.04238e-07C5.1435 8.04238e-07 3.36301 0.737501 2.05025 2.05025C0.7375 3.36301 0 5.1435 0 7C0 7.225 -1.52737e-07 7.445 0.0349998 7.665C0.16385 9.0151 0.68213 10.2988 1.52686 11.3598C2.37158 12.4209 3.50637 13.2137 4.79326 13.642C6.0801 14.0702 7.4637 14.1153 8.7758 13.7719C10.0879 13.4285 11.2719 12.7113 12.184 11.7075C13.0961 10.7038 13.6969 9.4567 13.9135 8.1178C14.1301 6.7789 13.9531 5.406 13.4039 4.16587C12.8548 2.92574 11.9573 1.87184 10.8204 1.13228C9.6835 0.392721 8.3563 -0.000649196 7 8.04238e-07ZM7 1C8.581 1.00137 10.0975 1.62668 11.22 2.74V3.24C9.2438 2.55991 7.0956 2.56872 5.125 3.265C4.96758 3.1116 4.76997 3.00586 4.555 2.96H4.43C4.37 2.75 4.315 2.54 4.27 2.325C4.225 2.11 4.2 1.92 4.175 1.715C5.043 1.24658 6.0137 1.00091 7 1ZM5.5 3.935C7.3158 3.32693 9.2838 3.34984 11.085 4C10.8414 5.2703 10.3094 6.4677 9.53 7.5C9.312 7.4077 9.0707 7.3855 8.8395 7.4366C8.6083 7.4877 8.3988 7.6094 8.24 7.785C8.065 7.685 7.89 7.585 7.74 7.47C6.7307 6.7966 5.8877 5.9023 5.275 4.855C5.374 4.73221 5.4461 4.58996 5.4866 4.43749C5.5271 4.28502 5.5351 4.12575 5.51 3.97L5.5 3.935ZM3.5 2.135C3.5 2.24 3.53 2.35 3.55 2.455C3.595 2.675 3.655 2.89 3.715 3.105C3.52353 3.21838 3.36943 3.38531 3.2717 3.58522C3.17397 3.78513 3.13688 4.00927 3.165 4.23C2.37575 4.7454 1.67078 5.3795 1.075 6.11C1.19455 5.3189 1.47112 4.55966 1.88843 3.87701C2.30575 3.19437 2.85539 2.60208 3.505 2.135H3.5ZM3.5 9.99C3.30481 10.0555 3.13037 10.1714 2.9943 10.3259C2.85822 10.4804 2.76533 10.6681 2.725 10.87H2.405C1.59754 9.9069 1.1146 8.7136 1.025 7.46L1.08 7.365C1.70611 6.3942 2.52463 5.562 3.485 4.92C3.62899 5.0704 3.81094 5.179 4.01162 5.2345C4.2123 5.2899 4.42423 5.2901 4.625 5.235C5.2938 6.3652 6.208 7.3306 7.3 8.06C7.505 8.195 7.715 8.32 7.925 8.44C7.9082 8.6312 7.9391 8.8237 8.015 9C7.1 9.7266 6.0445 10.256 4.915 10.555C4.78401 10.3103 4.57028 10.1201 4.31199 10.0184C4.05369 9.9167 3.76766 9.9102 3.505 10L3.5 9.99ZM7 12.99C5.9831 12.9903 4.98307 12.7304 4.095 12.235L4.235 12.205C4.43397 12.1397 4.61176 12.0222 4.74984 11.8648C4.88792 11.7074 4.98122 11.5158 5.02 11.31C6.2985 10.984 7.4921 10.3872 8.52 9.56C8.7642 9.7027 9.0525 9.75 9.3295 9.6927C9.6064 9.6355 9.8524 9.4778 10.02 9.25C10.7254 9.4334 11.4511 9.5275 12.18 9.53H12.445C11.9626 10.5673 11.1938 11.4451 10.2291 12.0599C9.2643 12.6747 8.144 13.0009 7 13V12.99ZM10.255 8.54C10.2545 8.3304 10.1975 8.1249 10.09 7.945C10.9221 6.8581 11.5012 5.5991 11.785 4.26C12.035 4.37667 12.2817 4.50667 12.525 4.65C13.0749 5.9495 13.1493 7.4012 12.735 8.75C11.9049 8.8142 11.0698 8.7484 10.26 8.555L10.255 8.54Z' fill='%23646D7A'/%3e%3c/svg%3e"; +import { fallbackChainIcon } from "./fallbackChainIcon.js"; /** * @internal @@ -49,18 +47,7 @@ export const ChainIcon: React.FC<{ fallbackImage={fallbackChainIcon} client={props.client} /> - {props.active && } + {props.active && } ); }; - -const ActiveDot = /* @__PURE__ */ StyledDiv({ - width: "28%", - height: "28%", - borderRadius: "50%", - position: "absolute", - bottom: 0, - right: 0, - backgroundColor: "#00d395", - boxShadow: "0 0 0 2px var(--bg)", -}); diff --git a/packages/thirdweb/src/react/web/ui/components/fallbackChainIcon.ts b/packages/thirdweb/src/react/web/ui/components/fallbackChainIcon.ts new file mode 100644 index 00000000000..7074985b9e0 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/components/fallbackChainIcon.ts @@ -0,0 +1,6 @@ +/** + * The dummy image to be used in case the chain icon does not exist + * @internal + */ +export const fallbackChainIcon = + "data:image/svg+xml;charset=UTF-8,%3csvg width='15' height='14' viewBox='0 0 15 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M7 8.04238e-07C5.1435 8.04238e-07 3.36301 0.737501 2.05025 2.05025C0.7375 3.36301 0 5.1435 0 7C0 7.225 -1.52737e-07 7.445 0.0349998 7.665C0.16385 9.0151 0.68213 10.2988 1.52686 11.3598C2.37158 12.4209 3.50637 13.2137 4.79326 13.642C6.0801 14.0702 7.4637 14.1153 8.7758 13.7719C10.0879 13.4285 11.2719 12.7113 12.184 11.7075C13.0961 10.7038 13.6969 9.4567 13.9135 8.1178C14.1301 6.7789 13.9531 5.406 13.4039 4.16587C12.8548 2.92574 11.9573 1.87184 10.8204 1.13228C9.6835 0.392721 8.3563 -0.000649196 7 8.04238e-07ZM7 1C8.581 1.00137 10.0975 1.62668 11.22 2.74V3.24C9.2438 2.55991 7.0956 2.56872 5.125 3.265C4.96758 3.1116 4.76997 3.00586 4.555 2.96H4.43C4.37 2.75 4.315 2.54 4.27 2.325C4.225 2.11 4.2 1.92 4.175 1.715C5.043 1.24658 6.0137 1.00091 7 1ZM5.5 3.935C7.3158 3.32693 9.2838 3.34984 11.085 4C10.8414 5.2703 10.3094 6.4677 9.53 7.5C9.312 7.4077 9.0707 7.3855 8.8395 7.4366C8.6083 7.4877 8.3988 7.6094 8.24 7.785C8.065 7.685 7.89 7.585 7.74 7.47C6.7307 6.7966 5.8877 5.9023 5.275 4.855C5.374 4.73221 5.4461 4.58996 5.4866 4.43749C5.5271 4.28502 5.5351 4.12575 5.51 3.97L5.5 3.935ZM3.5 2.135C3.5 2.24 3.53 2.35 3.55 2.455C3.595 2.675 3.655 2.89 3.715 3.105C3.52353 3.21838 3.36943 3.38531 3.2717 3.58522C3.17397 3.78513 3.13688 4.00927 3.165 4.23C2.37575 4.7454 1.67078 5.3795 1.075 6.11C1.19455 5.3189 1.47112 4.55966 1.88843 3.87701C2.30575 3.19437 2.85539 2.60208 3.505 2.135H3.5ZM3.5 9.99C3.30481 10.0555 3.13037 10.1714 2.9943 10.3259C2.85822 10.4804 2.76533 10.6681 2.725 10.87H2.405C1.59754 9.9069 1.1146 8.7136 1.025 7.46L1.08 7.365C1.70611 6.3942 2.52463 5.562 3.485 4.92C3.62899 5.0704 3.81094 5.179 4.01162 5.2345C4.2123 5.2899 4.42423 5.2901 4.625 5.235C5.2938 6.3652 6.208 7.3306 7.3 8.06C7.505 8.195 7.715 8.32 7.925 8.44C7.9082 8.6312 7.9391 8.8237 8.015 9C7.1 9.7266 6.0445 10.256 4.915 10.555C4.78401 10.3103 4.57028 10.1201 4.31199 10.0184C4.05369 9.9167 3.76766 9.9102 3.505 10L3.5 9.99ZM7 12.99C5.9831 12.9903 4.98307 12.7304 4.095 12.235L4.235 12.205C4.43397 12.1397 4.61176 12.0222 4.74984 11.8648C4.88792 11.7074 4.98122 11.5158 5.02 11.31C6.2985 10.984 7.4921 10.3872 8.52 9.56C8.7642 9.7027 9.0525 9.75 9.3295 9.6927C9.6064 9.6355 9.8524 9.4778 10.02 9.25C10.7254 9.4334 11.4511 9.5275 12.18 9.53H12.445C11.9626 10.5673 11.1938 11.4451 10.2291 12.0599C9.2643 12.6747 8.144 13.0009 7 13V12.99ZM10.255 8.54C10.2545 8.3304 10.1975 8.1249 10.09 7.945C10.9221 6.8581 11.5012 5.5991 11.785 4.26C12.035 4.37667 12.2817 4.50667 12.525 4.65C13.0749 5.9495 13.1493 7.4012 12.735 8.75C11.9049 8.8142 11.0698 8.7484 10.26 8.555L10.255 8.54Z' fill='%23646D7A'/%3e%3c/svg%3e"; diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.test.tsx new file mode 100644 index 00000000000..ae37c4bc1b7 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.test.tsx @@ -0,0 +1,129 @@ +import { describe, expect, it } from "vitest"; +import { TEST_CLIENT } from "~test/test-clients.js"; +import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js"; +import { defineChain } from "../../../../../chains/utils.js"; +import { getFunctionId } from "../../../../../utils/function-id.js"; +import { fetchChainIcon, getQueryKeys } from "./icon.js"; + +const client = TEST_CLIENT; + +describe.runIf(process.env.TW_SECRET_KEY)("ChainIcon", () => { + it("fetchChainIcon should respect iconResolver as a string", async () => { + expect( + await fetchChainIcon({ chain: ethereum, client, iconResolver: "test" }), + ).toBe("test"); + }); + + it("fetchChainIcon should respect iconResolver as a non-async function", async () => { + expect( + await fetchChainIcon({ + chain: ethereum, + client, + iconResolver: () => "test", + }), + ).toBe("test"); + }); + + it("fetchChainIcon should respect iconResolver as an async function", async () => { + expect( + await fetchChainIcon({ + chain: ethereum, + client, + iconResolver: async () => "test", + }), + ).toBe("test"); + }); + + it("fetchChainIcon should return a resolved url from the backend server, NOT ipfs uri", async () => { + const resolvedUrl = await fetchChainIcon({ chain: ethereum, client }); + expect( + resolvedUrl.endsWith( + ".ipfscdn.io/ipfs/QmdwQDr6vmBtXmK2TmknkEuZNoaDqTasFdZdu3DRw8b2wt", + ), + ).toBe(true); + expect(resolvedUrl.startsWith("https://")).toBe(true); + }); + + it("fetchChainIcon should return a resolved url from the chain object, NOT the ipfs uri", async () => { + const mockEthereum = defineChain({ + id: 1, + name: "Ethereum", + nativeCurrency: { + name: "Ether", + symbol: "ETH", + decimals: 18, + }, + blockExplorers: [ + { + name: "Etherscan", + url: "https://etherscan.io", + }, + ], + icon: { + url: "ipfs://QmdwQDr6vmBtXmK2TmknkEuZNoaDqTasFdZdu3DRw8b2wt", + format: "png", + width: 100, + height: 100, + }, + }); + const resolvedUrl = await fetchChainIcon({ chain: mockEthereum, client }); + expect( + resolvedUrl.endsWith( + ".ipfscdn.io/ipfs/QmdwQDr6vmBtXmK2TmknkEuZNoaDqTasFdZdu3DRw8b2wt", + ), + ).toBe(true); + expect(resolvedUrl.startsWith("https://")).toBe(true); + }); + + it("getQueryKeys should work without resolver", () => { + expect(getQueryKeys({ chainId: 1 })).toStrictEqual([ + "_internal_chain_icon_", + 1, + { + resolver: undefined, + }, + ]); + }); + + it("getQueryKeys should work with resolver being a string", () => { + expect(getQueryKeys({ chainId: 1, iconResolver: "tw" })).toStrictEqual([ + "_internal_chain_icon_", + 1, + { + resolver: "tw", + }, + ]); + }); + + it("getQueryKeys should work with resolver being a non-async fn that returns a string", () => { + const fn = () => "tw"; + const fnId = getFunctionId(fn); + expect(getQueryKeys({ chainId: 1, iconResolver: fn })).toStrictEqual([ + "_internal_chain_icon_", + 1, + { + resolver: fnId, + }, + ]); + }); + + it("getQueryKeys should work with resolver being an async fn that returns a string", () => { + const fn = async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + return "tw"; + }; + const fnId = getFunctionId(fn); + expect( + getQueryKeys({ + chainId: 1, + iconResolver: fn, + }), + ).toStrictEqual([ + "_internal_chain_icon_", + 1, + { + resolver: fnId, + }, + ]); + }); +}); diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx index b90e4b2485c..773bb7e2128 100644 --- a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx @@ -2,6 +2,7 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import type { JSX } from "react"; +import type { Chain } from "../../../../../chains/types.js"; import { getChainMetadata } from "../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; import { getFunctionId } from "../../../../../utils/function-id.js"; @@ -122,37 +123,8 @@ export function ChainIcon({ }: ChainIconProps) { const { chain } = useChainContext(); const iconQuery = useQuery({ - queryKey: [ - "_internal_chain_icon_", - chain.id, - { - resolver: - typeof iconResolver === "string" - ? iconResolver - : typeof iconResolver === "function" - ? getFunctionId(iconResolver) - : undefined, - }, - ] as const, - queryFn: async () => { - if (typeof iconResolver === "string") { - return iconResolver; - } - if (typeof iconResolver === "function") { - return iconResolver(); - } - // Check if the chain object already has "icon" - if (chain.icon?.url) { - return chain.icon.url; - } - const possibleUrl = await getChainMetadata(chain).then( - (data) => data.icon?.url, - ); - if (!possibleUrl) { - throw new Error("Failed to resolve icon for chain"); - } - return resolveScheme({ uri: possibleUrl, client }); - }, + queryKey: getQueryKeys({ chainId: chain.id, iconResolver }), + queryFn: async () => fetchChainIcon({ chain, client, iconResolver }), ...queryOptions, }); @@ -166,3 +138,53 @@ export function ChainIcon({ return {restProps.alt}; } + +/** + * @internal Exported for tests only + */ +export async function fetchChainIcon(props: { + chain: Chain; + client: ThirdwebClient; + iconResolver?: string | (() => string) | (() => Promise); +}) { + const { chain, client, iconResolver } = props; + if (typeof iconResolver === "string") { + return iconResolver; + } + if (typeof iconResolver === "function") { + return iconResolver(); + } + // Check if the chain object already has "icon" + if (chain.icon?.url) { + return resolveScheme({ uri: chain.icon.url, client }); + } + const possibleUrl = await getChainMetadata(chain).then( + (data) => data.icon?.url, + ); + if (!possibleUrl) { + throw new Error("Failed to resolve icon for chain"); + } + return resolveScheme({ uri: possibleUrl, client }); +} + +/** + * @internal + */ +export function getQueryKeys(props: { + chainId: number; + iconResolver?: string | (() => string) | (() => Promise); +}) { + const { chainId, iconResolver } = props; + return [ + "_internal_chain_icon_", + chainId, + { + resolver: + typeof iconResolver === "string" + ? iconResolver + : typeof iconResolver === "function" + ? getFunctionId(iconResolver) + : undefined, + }, + ] as const; +}