diff --git a/apps/portal/src/app/react/v5/sidebar.tsx b/apps/portal/src/app/react/v5/sidebar.tsx
index e5ac08463fc..9499171acf3 100644
--- a/apps/portal/src/app/react/v5/sidebar.tsx
+++ b/apps/portal/src/app/react/v5/sidebar.tsx
@@ -358,6 +358,20 @@ export const sidebar: SideBar = {
icon: ,
})),
},
+ {
+ name: "Token",
+ isCollapsible: true,
+ links: [
+ "TokenProvider",
+ "TokenName",
+ "TokenSymbol",
+ "TokenIcon",
+ ].map((name) => ({
+ name,
+ href: `${slug}/${name}`,
+ icon: ,
+ })),
+ },
],
},
{
diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts
index 2dab3c98dca..c41928f6f42 100644
--- a/packages/thirdweb/src/exports/react.ts
+++ b/packages/thirdweb/src/exports/react.ts
@@ -235,3 +235,21 @@ export {
AccountAvatar,
type AccountAvatarProps,
} from "../react/web/ui/prebuilt/Account/avatar.js";
+
+// Token
+export {
+ TokenProvider,
+ type TokenProviderProps,
+} from "../react/web/ui/prebuilt/Token/provider.js";
+export {
+ TokenName,
+ type TokenNameProps,
+} from "../react/web/ui/prebuilt/Token/name.js";
+export {
+ TokenSymbol,
+ type TokenSymbolProps,
+} from "../react/web/ui/prebuilt/Token/symbol.js";
+export {
+ TokenIcon,
+ type TokenIconProps,
+} from "../react/web/ui/prebuilt/Token/icon.js";
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
new file mode 100644
index 00000000000..70094abe3ce
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
@@ -0,0 +1,164 @@
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type { JSX } from "react";
+import { getChainMetadata } from "../../../../../chains/utils.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { getContract } from "../../../../../contract/contract.js";
+import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
+import { resolveScheme } from "../../../../../utils/ipfs.js";
+import { useTokenContext } from "./provider.js";
+
+export interface TokenIconProps
+ extends Omit, "src"> {
+ /**
+ * This prop can be a string or a (async) function that resolves to a string, representing the icon url of the token
+ * This is particularly useful if you already have a way to fetch the token icon.
+ */
+ iconResolver?: string | (() => string) | (() => Promise);
+ /**
+ * This component will be shown while the avatar of the icon is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: JSX.Element;
+ /**
+ * This component will be shown if the request for fetching the avatar is done
+ * but could not retreive any result.
+ * You can pass a dummy avatar/image to this prop.
+ *
+ * If not passed, the component will return `null`
+ *
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ fallbackComponent?: JSX.Element;
+
+ /**
+ * Optional query options for `useQuery`
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component tries to resolve the icon of a given token, then return an image.
+ * @returns an
with the src of the token icon
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { TokenProvider, TokenIcon } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Result: An
component with the src of the icon
+ * ```html
+ *
+ * ```
+ *
+ * ### Override the icon with the `iconResolver` prop
+ * If you already have the icon url, you can skip the network requests and pass it directly to the TokenIcon
+ * ```tsx
+ *
+ * ```
+ *
+ * You can also pass in your own custom (async) function that retrieves the icon url
+ * ```tsx
+ * const getIcon = async () => {
+ * const icon = getIconFromCoinMarketCap(tokenAddress, etc);
+ * return icon;
+ * };
+ *
+ *
+ * ```
+ *
+ * ### Show a loading sign while the icon is being loaded
+ * ```tsx
+ * } />
+ * ```
+ *
+ * ### Fallback to a dummy image if the token icon fails to resolve
+ * ```tsx
+ * } />
+ * ```
+ *
+ * ### Usage with queryOptions
+ * TokenIcon uses useQuery() from tanstack query internally.
+ * It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @token
+ * @beta
+ */
+export function TokenIcon({
+ iconResolver,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ ...restProps
+}: TokenIconProps) {
+ const { address, client, chain } = useTokenContext();
+ const iconQuery = useQuery({
+ queryKey: ["_internal_token_icon_", chain.id, address] as const,
+ queryFn: async () => {
+ if (typeof iconResolver === "string") {
+ return iconResolver;
+ }
+ if (typeof iconResolver === "function") {
+ return iconResolver();
+ }
+ if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ const possibleUrl = await getChainMetadata(chain).then(
+ (data) => data.icon?.url,
+ );
+ if (!possibleUrl) {
+ throw new Error("Failed to resolve icon for native token");
+ }
+ return resolveScheme({ uri: possibleUrl, client });
+ }
+
+ // Try to get the icon from the contractURI
+ const contractMetadata = await getContractMetadata({
+ contract: getContract({
+ address,
+ chain,
+ client,
+ }),
+ });
+
+ if (
+ !contractMetadata.image ||
+ typeof contractMetadata.image !== "string"
+ ) {
+ throw new Error("Failed to resolve token icon from contract metadata");
+ }
+
+ return resolveScheme({
+ uri: contractMetadata.image,
+ client,
+ });
+ },
+ ...queryOptions,
+ });
+
+ if (iconQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!iconQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ return
;
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx
new file mode 100644
index 00000000000..806fcba3537
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/name.tsx
@@ -0,0 +1,201 @@
+"use client";
+
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type React from "react";
+import type { JSX } from "react";
+import { getChainMetadata } from "../../../../../chains/utils.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { getContract } from "../../../../../contract/contract.js";
+import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
+import { name } from "../../../../../extensions/common/read/name.js";
+import { useTokenContext } from "./provider.js";
+
+/**
+ * Props for the TokenName component
+ * @component
+ * @token
+ */
+export interface TokenNameProps
+ extends Omit, "children"> {
+ /**
+ * This prop can be a string or a (async) function that resolves to a string, representing the name of the token
+ * This is particularly useful if you already have a way to fetch the token name.
+ */
+ nameResolver?: string | (() => string) | (() => Promise);
+ /**
+ * A function to format the name's display value
+ * Particularly useful to avoid overflowing-UI issues
+ *
+ * ```tsx
+ * doSomething()} />
+ * ```
+ */
+ formatFn?: (str: string) => string;
+ /**
+ * This component will be shown while the name of the token is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: JSX.Element;
+ /**
+ * This component will be shown if the name fails to be retreived
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a descriptive text/component to this prop, indicating that the
+ * name was not fetched succesfully
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+ fallbackComponent?: JSX.Element;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component fetches then shows the name of a token. For ERC20 tokens, it calls the `name` function in the ERC20 contract.
+ * It inherits all the attributes of a HTML component, hence you can style it just like how you would style a normal
+ *
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { TokenProvider, TokenName } from "thirdweb/react";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * Ether
+ * ```
+ *
+ * ### Custom name resolver
+ * By default TokenName will call the `name` method of the token contract.
+ * However if you have a different way to fetch the name, you can pass the function to the `nameResolver` prop.
+ * Note: nameResolver should either be a string or a function (async) that returns a string.
+ * ```tsx
+ * async function fetchNameMethod() {
+ * // your own fetching logic
+ * return "the token name";
+ * }
+ *
+ *
+ * ```
+ *
+ * Alternatively you can also pass in a string directly:
+ * ```tsx
+ *
+ * ```
+ *
+ *
+ * ### Format the name (capitalize, truncate, etc.)
+ * The TokenName component accepts a `formatFn` which takes in a string and outputs a string
+ * The function is used to modify the name of the token
+ *
+ * ```tsx
+ * const concatStr = (str: string):string => str + "Token"
+ *
+ *
+ * ```
+ *
+ * Result:
+ * ```html
+ * Ether Token
+ * ```
+ *
+ * ### Show a loading sign when the name is being fetched
+ * ```tsx
+ * import { TokenProvider, TokenName } from "thirdweb/react";
+ *
+ *
+ * } />
+ *
+ * ```
+ *
+ * ### Fallback to something when the name fails to resolve
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @token
+ * @beta
+ */
+export function TokenName({
+ nameResolver,
+ formatFn,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ ...restProps
+}: TokenNameProps) {
+ const { address, client, chain } = useTokenContext();
+ const nameQuery = useQuery({
+ queryKey: ["_internal_token_name_", chain.id, address] as const,
+ queryFn: async () => {
+ if (typeof nameResolver === "string") {
+ return nameResolver;
+ }
+ if (typeof nameResolver === "function") {
+ return nameResolver();
+ }
+ if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ // Don't wanna use `getChainNativeCurrencyName` because it has some side effect (it catches error and defaults to "ETH")
+ return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
+ }
+ // Try to fetch the name from both the `name()` function and the contract metadata
+ // then prioritize the `name()`
+ const contract = getContract({ address, client, chain });
+ const [_name, contractMetadata] = await Promise.all([
+ name({ contract }),
+ getContractMetadata({ contract }),
+ ]);
+ if (!_name && !contractMetadata.name) {
+ throw new Error(
+ "Failed to resolve name from both name() and contract metadata",
+ );
+ }
+
+ return _name || contractMetadata.name;
+ },
+ ...queryOptions,
+ });
+
+ if (nameQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!nameQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ const displayValue = formatFn ? formatFn(nameQuery.data) : nameQuery.data;
+
+ return {displayValue};
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/provider.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/provider.tsx
new file mode 100644
index 00000000000..15eedafbf93
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/provider.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import type { Address } from "abitype";
+import type React from "react";
+import { createContext, useContext } from "react";
+import type { Chain } from "../../../../../chains/types.js";
+import type { ThirdwebClient } from "../../../../../client/client.js";
+
+/**
+ * Props for the component
+ * @component
+ * @token
+ */
+export type TokenProviderProps = {
+ /**
+ * The token (ERC20) contract address
+ */
+ address: Address;
+ /**
+ * thirdweb Client
+ */
+ client: ThirdwebClient;
+ /**
+ * The chain (network) that the token is on
+ */
+ chain: Chain;
+};
+
+const TokenProviderContext = /* @__PURE__ */ createContext<
+ TokenProviderProps | undefined
+>(undefined);
+
+/**
+ * A React context provider component that supplies Token-related data to its child components.
+ *
+ * This component serves as a wrapper around the `TokenProviderContext.Provider` and passes
+ * the provided token data down to all of its child components through the context API.
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { TokenProvider, TokenIcon, TokenName } from "thirdweb/react";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ### This component also works with native token!
+ * ```tsx
+ * import { NATIVE_TOKEN_ADDRESS} from "thirdweb";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ * // "ETH"
+ *
+ * ```
+ *
+ * @component
+ * @token
+ * @beta
+ */
+export function TokenProvider(
+ props: React.PropsWithChildren,
+) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+/**
+ * @internal
+ */
+export function useTokenContext() {
+ const ctx = useContext(TokenProviderContext);
+ if (!ctx) {
+ throw new Error(
+ "TokenProviderContext not found. Make sure you are using TokenName, TokenIcon, TokenSymbol etc. inside a component",
+ );
+ }
+ return ctx;
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx
new file mode 100644
index 00000000000..f770a600401
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.tsx
@@ -0,0 +1,201 @@
+"use client";
+
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type React from "react";
+import type { JSX } from "react";
+import { getChainMetadata } from "../../../../../chains/utils.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { getContract } from "../../../../../contract/contract.js";
+import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
+import { symbol } from "../../../../../extensions/common/read/symbol.js";
+import { useTokenContext } from "./provider.js";
+
+/**
+ * Props for the TokenSymbol component
+ * @component
+ * @token
+ */
+export interface TokenSymbolProps
+ extends Omit, "children"> {
+ /**
+ * This prop can be a string or a (async) function that resolves to a string, representing the symbol of the token
+ * This is particularly useful if you already have a way to fetch the token symbol.
+ */
+ symbolResolver?: string | (() => string) | (() => Promise);
+ /**
+ * A function to format the symbol's value
+ * Particularly useful to avoid overflowing-UI issues
+ *
+ * ```tsx
+ * doSomething()} />
+ * ```
+ */
+ formatFn?: (str: string) => string;
+ /**
+ * This component will be shown while the symbol of the token is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: JSX.Element;
+ /**
+ * This component will be shown if the symbol fails to be retreived
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a descriptive text/component to this prop, indicating that the
+ * symbol was not fetched succesfully
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+ fallbackComponent?: JSX.Element;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component fetches then shows the symbol of a token. For ERC20 tokens, it calls the `symbol` function in the ERC20 contract.
+ * It inherits all the attributes of a HTML component, hence you can style it just like how you would style a normal
+ *
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { TokenProvider, TokenSymbol } from "thirdweb/react";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * ETH
+ * ```
+ *
+ * ### Custom symbol resolver
+ * By default, TokenSymbol calls the `symbol` function of your contract,
+ * however, if your token as an unconventional way to fetch the symbol, you can pass the custom logic to the `symbolResolver` prop.
+ * It can either be a string or a function (async) that returns or resolves to a string.
+ * ```tsx
+ * async function getSymbol() {
+ * // your own fetching logic
+ * return "the symbol";
+ * }
+ *
+ *
+ * ```
+ * Alternatively, you can pass in a string directly:
+ * ```tsx
+ *
+ * ```
+ *
+ * ### Format the symbol (capitalize, truncate, etc.)
+ * The TokenSymbol component accepts a `formatFn` which takes in a string and outputs a string
+ * The function is used to modify the symbol of the token
+ *
+ * ```tsx
+ * const concatStr = (str: string):string => str + "Token"
+ *
+ *
+ * ```
+ *
+ * Result:
+ * ```html
+ * Ether Token
+ * ```
+ *
+ * ### Show a loading sign when the symbol is being fetched
+ * ```tsx
+ * import { TokenProvider, TokenSymbol } from "thirdweb/react";
+ *
+ *
+ * } />
+ *
+ * ```
+ *
+ * ### Fallback to something when the symbol fails to resolve
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @token
+ * @beta
+ */
+export function TokenSymbol({
+ symbolResolver,
+ formatFn,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ ...restProps
+}: TokenSymbolProps) {
+ const { address, client, chain } = useTokenContext();
+ const symbolQuery = useQuery({
+ queryKey: ["_internal_token_symbol_", chain.id, address] as const,
+ queryFn: async () => {
+ if (typeof symbolResolver === "string") {
+ return symbolResolver;
+ }
+ if (typeof symbolResolver === "function") {
+ return symbolResolver();
+ }
+ if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
+ // Don't wanna use `getChainSymbol` because it has some side effect (it catches error and defaults to "ETH")
+ return getChainMetadata(chain).then(
+ (data) => data.nativeCurrency.symbol,
+ );
+ }
+
+ // Try to fetch the symbol from both the `symbol` function and the contract metadata
+ // then prioritize the `symbol()`
+ const contract = getContract({ address, client, chain });
+ const [_symbol, contractMetadata] = await Promise.all([
+ symbol({ contract }),
+ getContractMetadata({ contract }),
+ ]);
+ if (!_symbol && !contractMetadata.symbol) {
+ throw new Error(
+ "Failed to resolve symbol from both symbol() and contract metadata",
+ );
+ }
+
+ return _symbol || contractMetadata.symbol;
+ },
+ ...queryOptions,
+ });
+
+ if (symbolQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!symbolQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ const displayValue = formatFn ? formatFn(symbolQuery.data) : symbolQuery.data;
+
+ return {displayValue};
+}