Skip to content

Commit 7973d64

Browse files
committed
[React] Feature: Headless UI Token components (#5433)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces new components related to token management in a React application, including `TokenProvider`, `TokenName`, `TokenSymbol`, and `TokenIcon`. These components allow users to fetch and display token-related data such as name, symbol, and icon. ### Detailed summary - Added `Token` section in the sidebar with links to token components. - Created `TokenProvider` for managing token data context. - Implemented `TokenName`, `TokenSymbol`, and `TokenIcon` components for fetching and displaying token information. - Exported new token-related types and components from `react.ts`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 017b13f commit 7973d64

File tree

6 files changed

+684
-0
lines changed

6 files changed

+684
-0
lines changed

apps/portal/src/app/react/v5/sidebar.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,20 @@ export const sidebar: SideBar = {
358358
icon: <CodeIcon />,
359359
})),
360360
},
361+
{
362+
name: "Token",
363+
isCollapsible: true,
364+
links: [
365+
"TokenProvider",
366+
"TokenName",
367+
"TokenSymbol",
368+
"TokenIcon",
369+
].map((name) => ({
370+
name,
371+
href: `${slug}/${name}`,
372+
icon: <CodeIcon />,
373+
})),
374+
},
361375
],
362376
},
363377
{

packages/thirdweb/src/exports/react.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,21 @@ export {
235235
AccountAvatar,
236236
type AccountAvatarProps,
237237
} from "../react/web/ui/prebuilt/Account/avatar.js";
238+
239+
// Token
240+
export {
241+
TokenProvider,
242+
type TokenProviderProps,
243+
} from "../react/web/ui/prebuilt/Token/provider.js";
244+
export {
245+
TokenName,
246+
type TokenNameProps,
247+
} from "../react/web/ui/prebuilt/Token/name.js";
248+
export {
249+
TokenSymbol,
250+
type TokenSymbolProps,
251+
} from "../react/web/ui/prebuilt/Token/symbol.js";
252+
export {
253+
TokenIcon,
254+
type TokenIconProps,
255+
} from "../react/web/ui/prebuilt/Token/icon.js";
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
2+
import type { JSX } from "react";
3+
import { getChainMetadata } from "../../../../../chains/utils.js";
4+
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
5+
import { getContract } from "../../../../../contract/contract.js";
6+
import { getContractMetadata } from "../../../../../extensions/common/read/getContractMetadata.js";
7+
import { resolveScheme } from "../../../../../utils/ipfs.js";
8+
import { useTokenContext } from "./provider.js";
9+
10+
export interface TokenIconProps
11+
extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> {
12+
/**
13+
* This prop can be a string or a (async) function that resolves to a string, representing the icon url of the token
14+
* This is particularly useful if you already have a way to fetch the token icon.
15+
*/
16+
iconResolver?: string | (() => string) | (() => Promise<string>);
17+
/**
18+
* This component will be shown while the avatar of the icon is being fetched
19+
* If not passed, the component will return `null`.
20+
*
21+
* You can pass a loading sign or spinner to this prop.
22+
* @example
23+
* ```tsx
24+
* <TokenIcon loadingComponent={<Spinner />} />
25+
* ```
26+
*/
27+
loadingComponent?: JSX.Element;
28+
/**
29+
* This component will be shown if the request for fetching the avatar is done
30+
* but could not retreive any result.
31+
* You can pass a dummy avatar/image to this prop.
32+
*
33+
* If not passed, the component will return `null`
34+
*
35+
* @example
36+
* ```tsx
37+
* <TokenIcon fallbackComponent={<DummyImage />} />
38+
* ```
39+
*/
40+
fallbackComponent?: JSX.Element;
41+
42+
/**
43+
* Optional query options for `useQuery`
44+
*/
45+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
46+
}
47+
48+
/**
49+
* This component tries to resolve the icon of a given token, then return an image.
50+
* @returns an <img /> with the src of the token icon
51+
*
52+
* @example
53+
* ### Basic usage
54+
* ```tsx
55+
* import { TokenProvider, TokenIcon } from "thirdweb/react";
56+
*
57+
* <TokenProvider address="0x-token-address" chain={chain} client={client}>
58+
* <TokenIcon />
59+
* </TokenProvider>
60+
* ```
61+
*
62+
* Result: An <img /> component with the src of the icon
63+
* ```html
64+
* <img src="token-icon.png" />
65+
* ```
66+
*
67+
* ### Override the icon with the `iconResolver` prop
68+
* If you already have the icon url, you can skip the network requests and pass it directly to the TokenIcon
69+
* ```tsx
70+
* <TokenIcon iconResolver="/usdc.png" />
71+
* ```
72+
*
73+
* You can also pass in your own custom (async) function that retrieves the icon url
74+
* ```tsx
75+
* const getIcon = async () => {
76+
* const icon = getIconFromCoinMarketCap(tokenAddress, etc);
77+
* return icon;
78+
* };
79+
*
80+
* <TokenIcon iconResolver={getIcon} />
81+
* ```
82+
*
83+
* ### Show a loading sign while the icon is being loaded
84+
* ```tsx
85+
* <TokenIcon loadingComponent={<Spinner />} />
86+
* ```
87+
*
88+
* ### Fallback to a dummy image if the token icon fails to resolve
89+
* ```tsx
90+
* <TokenIcon fallbackComponent={<img src="blank-image.png" />} />
91+
* ```
92+
*
93+
* ### Usage with queryOptions
94+
* TokenIcon uses useQuery() from tanstack query internally.
95+
* It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
96+
* ```tsx
97+
* <TokenIcon queryOptions={{ enabled: someLogic, retry: 3, }} />
98+
* ```
99+
*
100+
* @component
101+
* @token
102+
* @beta
103+
*/
104+
export function TokenIcon({
105+
iconResolver,
106+
loadingComponent,
107+
fallbackComponent,
108+
queryOptions,
109+
...restProps
110+
}: TokenIconProps) {
111+
const { address, client, chain } = useTokenContext();
112+
const iconQuery = useQuery({
113+
queryKey: ["_internal_token_icon_", chain.id, address] as const,
114+
queryFn: async () => {
115+
if (typeof iconResolver === "string") {
116+
return iconResolver;
117+
}
118+
if (typeof iconResolver === "function") {
119+
return iconResolver();
120+
}
121+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
122+
const possibleUrl = await getChainMetadata(chain).then(
123+
(data) => data.icon?.url,
124+
);
125+
if (!possibleUrl) {
126+
throw new Error("Failed to resolve icon for native token");
127+
}
128+
return resolveScheme({ uri: possibleUrl, client });
129+
}
130+
131+
// Try to get the icon from the contractURI
132+
const contractMetadata = await getContractMetadata({
133+
contract: getContract({
134+
address,
135+
chain,
136+
client,
137+
}),
138+
});
139+
140+
if (
141+
!contractMetadata.image ||
142+
typeof contractMetadata.image !== "string"
143+
) {
144+
throw new Error("Failed to resolve token icon from contract metadata");
145+
}
146+
147+
return resolveScheme({
148+
uri: contractMetadata.image,
149+
client,
150+
});
151+
},
152+
...queryOptions,
153+
});
154+
155+
if (iconQuery.isLoading) {
156+
return loadingComponent || null;
157+
}
158+
159+
if (!iconQuery.data) {
160+
return fallbackComponent || null;
161+
}
162+
163+
return <img src={iconQuery.data} alt={restProps.alt} />;
164+
}

0 commit comments

Comments
 (0)