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..a3337fcdf9a 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..bd3f6f35af3 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.test.tsx @@ -0,0 +1,70 @@ +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 { fetchChainIcon } 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 }); + const omit = resolvedUrl.replace(process.env.TW_SECRET_KEY || "", ""); + expect(omit).toBe( + "https://18ae07e41f09cfc85e670365eed39f5a.ipfscdn.io/ipfs/QmdwQDr6vmBtXmK2TmknkEuZNoaDqTasFdZdu3DRw8b2wt", + ); + }); + + 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, + }, + }); + expect(await fetchChainIcon({ chain: mockEthereum, client })).toBe( + "https://18ae07e41f09cfc85e670365eed39f5a.ipfscdn.io/ipfs/QmdwQDr6vmBtXmK2TmknkEuZNoaDqTasFdZdu3DRw8b2wt", + ); + }); +}); 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..fd05266c2f5 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"; @@ -134,25 +135,7 @@ export function ChainIcon({ : 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 }); - }, + queryFn: async () => fetchChainIcon({ chain, client, iconResolver }), ...queryOptions, }); @@ -166,3 +149,31 @@ 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 }); +}