From bbb3ab15e1bce0285c3491277a0cbfd48cdded24 Mon Sep 17 00:00:00 2001 From: Kien Ngo Date: Wed, 4 Dec 2024 18:07:06 +0700 Subject: [PATCH] update --- .changeset/metal-mails-ring.md | 20 ++ .../hooks/connection/ConnectButtonProps.ts | 7 + .../web/ui/ConnectWallet/ConnectButton.tsx | 11 + .../react/web/ui/ConnectWallet/Details.tsx | 268 +++++++++++------- .../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 ++++-- 10 files changed, 489 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..89c5f6f9e84 --- /dev/null +++ b/.changeset/metal-mails-ring.md @@ -0,0 +1,20 @@ +--- +"thirdweb": patch +--- + +- Add onClose callback to Connect Details modal + +```tsx + { + // The last screen name that was being shown when user closed the modal + console.log({ screen }); + } + }} +/> +``` + +- Small fix for ChainIcon: Always resolve IPFS URI + +- Improve test coverage \ No newline at end of file diff --git a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts index 2a25abd8191..f7b82c48569 100644 --- a/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts +++ b/packages/thirdweb/src/react/core/hooks/connection/ConnectButtonProps.ts @@ -327,6 +327,13 @@ export type ConnectButton_detailsModalOptions = { * Note: Not all tokens are resolvable to a fiat value. In that case, nothing will be shown. */ showBalanceInFiat?: SupportedFiatCurrency; + + /** + * + * @param screen The screen's name that was last shown when user closed the modal + * @returns + */ + onClose?: (screen: string) => void; }; /** diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx index 31dcf7aa530..4dde6ac5f52 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx @@ -262,6 +262,17 @@ const TW_CONNECT_WALLET = "tw-connect-wallet"; * /> * ``` * + * ### Callback for when the details modal is closed + * ```tsx + * { + * console.log({ screen }); + * } + * }} + * /> + * ``` + * * @param props * Props for the `ConnectButton` component * diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx index 6aa59573a4d..100361aaed3 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 */} { if (!_open) { closeModal(); + if (props.detailsModal?.onClose) { + props.detailsModal?.onClose(screen); + } } }} > @@ -1099,6 +1027,135 @@ export function DetailsModal(props: { ); } +/** + * When this button is clicked, it will switch to the screen where users + * can select a chain to switch to. + * @internal + */ +function NetworkSwitcherButton(props: { + setScreen: Dispatch>; + 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 { @@ -1614,12 +1671,19 @@ export type UseWalletDetailsModalOptions = { * Note: Not all tokens are resolvable to a fiat value. In that case, nothing will be shown. */ showBalanceInFiat?: SupportedFiatCurrency; + + /** + * The callback function for when the modal is closed + * @param screen The name of the screen that was being shown when user closed the modal + */ + onClose?: (screen: string) => void; }; /** * Hook to open the Wallet Details Modal that shows various information about the connected wallet and allows users to perform various actions like sending funds, receiving funds, switching networks, Buying tokens, etc. * * @example + * ### Basic usage * ```tsx * import { createThirdwebClient } from "thirdweb"; * import { useWalletDetailsModal } from "thirdweb/react"; @@ -1638,6 +1702,15 @@ export type UseWalletDetailsModalOptions = { * return * } * ``` + * + * ### Callback for when the modal is closed + * ```tsx + * detailsModal.open({ + * client, + * onClose: (screen: string) => console.log({ screen }) + * }); + * ``` + * * @wallet */ export function useWalletDetailsModal() { @@ -1672,6 +1745,7 @@ export function useWalletDetailsModal() { hideReceiveFunds: props.hideReceiveFunds, hideSendFunds: props.hideSendFunds, assetTabs: props.assetTabs, + onClose: props.onClose, }} displayBalanceToken={props.displayBalanceToken} theme={props.theme || "dark"} 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; +}