Skip to content

Commit 26f83cb

Browse files
committed
update
1 parent ac39139 commit 26f83cb

File tree

6 files changed

+678
-0
lines changed

6 files changed

+678
-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: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
*
54+
* ### Basic usage
55+
* ```tsx
56+
* import { TokenProvider, TokenIcon } from "thirdweb/react";
57+
*
58+
* <TokenProvider address="0x-token-address" chain={chain} client={client}>
59+
* <TokenIcon />
60+
* </TokenProvider>
61+
* ```
62+
*
63+
* Result: An <img /> component with the src of the icon
64+
* ```html
65+
* <img src="token-icon.png" />
66+
* ```
67+
*
68+
* ### Override the icon with the `iconResolver` prop
69+
* If you already have the icon url, you can skip the network requests and pass it directly to the TokenIcon
70+
* ```tsx
71+
* <TokenIcon iconResolver="/usdc.png" />
72+
* ```
73+
*
74+
* You can also pass in your own custom (async) function that retrieves the icon url
75+
* ```tsx
76+
* const getIcon = async () => {
77+
* const icon = getIconFromCoinMarketCap(tokenAddress, etc);
78+
* return icon;
79+
* };
80+
*
81+
* <TokenIcon iconResolver={getIcon} />
82+
* ```
83+
*
84+
* ### Show a loading sign while the icon is being loaded
85+
* ```tsx
86+
* <TokenIcon loadingComponent={<Spinner />} />
87+
* ```
88+
*
89+
* ### Fallback to a dummy image if the token icon fails to resolve
90+
* ```tsx
91+
* <TokenIcon fallbackComponent={<img src="blank-image.png" />} />
92+
* ```
93+
*
94+
* ### Usage with queryOptions
95+
* TokenIcon uses useQuery() from tanstack query internally.
96+
* It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
97+
* ```tsx
98+
* <TokenIcon queryOptions={{ enabled: someLogic, retry: 3, }} />
99+
* ```
100+
*/
101+
export function TokenIcon({
102+
iconResolver,
103+
loadingComponent,
104+
fallbackComponent,
105+
queryOptions,
106+
...restProps
107+
}: TokenIconProps) {
108+
const { address, client, chain } = useTokenContext();
109+
const iconQuery = useQuery({
110+
queryKey: ["_internal_token_icon_", chain.id, address] as const,
111+
queryFn: async () => {
112+
if (typeof iconResolver === "string") {
113+
return iconResolver;
114+
}
115+
if (typeof iconResolver === "function") {
116+
return iconResolver();
117+
}
118+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
119+
const possibleUrl = await getChainMetadata(chain).then(
120+
(data) => data.icon?.url,
121+
);
122+
if (!possibleUrl) {
123+
throw new Error("Failed to resolve icon for native token");
124+
}
125+
return resolveScheme({ uri: possibleUrl, client });
126+
}
127+
128+
// Try to get the icon from the contractURI
129+
const contractMetadata = await getContractMetadata({
130+
contract: getContract({
131+
address,
132+
chain,
133+
client,
134+
}),
135+
});
136+
137+
if (
138+
!contractMetadata.image ||
139+
typeof contractMetadata.image !== "string"
140+
) {
141+
throw new Error("Failed to resolve token icon from contract metadata");
142+
}
143+
144+
return resolveScheme({
145+
uri: contractMetadata.image,
146+
client,
147+
});
148+
},
149+
...queryOptions,
150+
});
151+
152+
if (iconQuery.isLoading) {
153+
return loadingComponent || null;
154+
}
155+
156+
if (!iconQuery.data) {
157+
return fallbackComponent || null;
158+
}
159+
160+
return <img src={iconQuery.data} alt={restProps.alt} />;
161+
}

0 commit comments

Comments
 (0)