Skip to content

Commit 89a698b

Browse files
committed
update
1 parent c5c7dd0 commit 89a698b

File tree

6 files changed

+597
-7
lines changed

6 files changed

+597
-7
lines changed

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

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,29 @@ export const sidebar: SideBar = {
336336
links: [
337337
{
338338
name: "UI Components",
339-
links: ["ClaimButton", "TransactionButton", "MediaRenderer"].map(
340-
(name) => ({
341-
name,
342-
href: `${slug}/${name}`,
343-
icon: <CodeIcon />,
344-
}),
345-
),
339+
links: [
340+
...["ClaimButton", "TransactionButton", "MediaRenderer"].map(
341+
(name) => ({
342+
name,
343+
href: `${slug}/${name}`,
344+
icon: <CodeIcon />,
345+
}),
346+
),
347+
{
348+
name: "Token (Native & ERC20)",
349+
isCollapsible: true,
350+
links: [
351+
"TokenProvider",
352+
"TokenName",
353+
"TokenSymbol",
354+
"TokenIcon",
355+
].map((name) => ({
356+
name,
357+
href: `${slug}/${name}`,
358+
icon: <CodeIcon />,
359+
})),
360+
},
361+
],
346362
},
347363
{
348364
name: "Reading State",

packages/thirdweb/src/exports/react.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,21 @@ export {
222222
AccountAvatar,
223223
type AccountAvatarProps,
224224
} from "../react/web/ui/prebuilt/Account/avatar.js";
225+
226+
// Token
227+
export {
228+
TokenProvider,
229+
type TokenProviderProps,
230+
} from "../react/web/ui/prebuilt/Token/provider.js";
231+
export {
232+
TokenName,
233+
type TokenNameProps,
234+
} from "../react/web/ui/prebuilt/Token/name.js";
235+
export {
236+
TokenSymbol,
237+
type TokenSymbolProps,
238+
} from "../react/web/ui/prebuilt/Token/symbol.js";
239+
export {
240+
TokenIcon,
241+
type TokenIconProps,
242+
} from "../react/web/ui/prebuilt/Token/icon.js";
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 account 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+
* @param param0
51+
* @returns
52+
*
53+
* @example
54+
*
55+
* ### Override the icon with the `iconResolver` prop
56+
* If you already have the icon url, you can skip the network requests and pass it directly to the TokenIcon
57+
* ```tsx
58+
* <TokenIcon iconResolver="/usdc.png" />
59+
* ```
60+
*
61+
* You can also pass in your own custom function that retrieves the icon url
62+
* ```tsx
63+
* const getIcon = async () => {
64+
* const icon = getIconFromCoinMarketCap(tokenAddress, etc);
65+
* return icon;
66+
* };
67+
*
68+
* <TokenIcon iconResolver={getIcon} />
69+
* ```
70+
*/
71+
export function TokenIcon({
72+
iconResolver,
73+
loadingComponent,
74+
fallbackComponent,
75+
queryOptions,
76+
...restProps
77+
}: TokenIconProps) {
78+
const { address, client, chain } = useTokenContext();
79+
const iconQuery = useQuery({
80+
queryKey: ["_internal_token_icon_", chain.id, address] as const,
81+
queryFn: async () => {
82+
if (typeof iconResolver === "string") {
83+
return iconResolver;
84+
}
85+
if (typeof iconResolver === "function") {
86+
return iconResolver();
87+
}
88+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
89+
const possibleUrl = await getChainMetadata(chain).then(
90+
(data) => data.icon?.url,
91+
);
92+
if (!possibleUrl) {
93+
throw new Error("Failed to resolve icon for native token");
94+
}
95+
return resolveScheme({ uri: possibleUrl, client });
96+
}
97+
98+
// Try to get the icon from the contractURI
99+
const contractMetadata = await getContractMetadata({
100+
contract: getContract({
101+
address,
102+
chain,
103+
client,
104+
}),
105+
});
106+
107+
if (
108+
!contractMetadata.image ||
109+
typeof contractMetadata.image !== "string"
110+
) {
111+
throw new Error("Failed to resolve token icon from contract metadata");
112+
}
113+
114+
return resolveScheme({
115+
uri: contractMetadata.image,
116+
client,
117+
});
118+
},
119+
...queryOptions,
120+
});
121+
122+
if (iconQuery.isLoading) {
123+
return loadingComponent || null;
124+
}
125+
126+
if (!iconQuery.data) {
127+
return fallbackComponent || null;
128+
}
129+
130+
return <img src={iconQuery.data} alt={restProps.alt} />;
131+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"use client";
2+
3+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
4+
import type React from "react";
5+
import type { JSX } from "react";
6+
import { getChainMetadata } from "../../../../../chains/utils.js";
7+
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
8+
import { getContract } from "../../../../../contract/contract.js";
9+
import { name } from "../../../../../extensions/common/read/name.js";
10+
import { useTokenContext } from "./provider.js";
11+
12+
/**
13+
* Props for the TokenName component
14+
* @component
15+
* @token
16+
*/
17+
export interface TokenNameProps
18+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
19+
/**
20+
* This prop can be a string or a (async) function that resolves to a string, representing the name of the token
21+
* This is particularly useful if you already have a way to fetch the token name.
22+
*/
23+
nameResolver?: string | (() => string) | (() => Promise<string>);
24+
/**
25+
* A function to format the name's display value
26+
* Particularly useful to avoid overflowing-UI issues
27+
*
28+
* ```tsx
29+
* <TokenName formatFn={(str: string) => doSomething()} />
30+
* ```
31+
*/
32+
formatFn?: (num: number) => number;
33+
/**
34+
* This component will be shown while the name of the token is being fetched
35+
* If not passed, the component will return `null`.
36+
*
37+
* You can/should pass a loading sign or spinner to this prop.
38+
* @example
39+
* ```tsx
40+
* <TokenName loadingComponent={<Spinner />} />
41+
* ```
42+
*/
43+
loadingComponent?: JSX.Element;
44+
/**
45+
* This component will be shown if the name fails to be retreived
46+
* If not passed, the component will return `null`.
47+
*
48+
* You can/should pass a descriptive text/component to this prop, indicating that the
49+
* name was not fetched succesfully
50+
* @example
51+
* ```tsx
52+
* <TokenName fallbackComponent={"Failed to load"}
53+
* />
54+
* ```
55+
*/
56+
fallbackComponent?: JSX.Element;
57+
/**
58+
* Optional `useQuery` params
59+
*/
60+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
61+
}
62+
63+
/**
64+
* This component fetches then shows the name of a token. For ERC20 tokens, it calls the `name` function in the ERC20 contract.
65+
* It inherits all the attributes of a HTML <span> component, hence you can style it just like how you would style a normal <span>
66+
*
67+
*
68+
* @example
69+
* ### Basic usage
70+
* ```tsx
71+
* import { TokenProvider, TokenName } from "thirdweb/react";
72+
* import { ethereum } from "thirdweb/chains";
73+
*
74+
* <TokenProvider {...props}>
75+
* <TokenName />
76+
* </TokenProvider>
77+
* ```
78+
* Result:
79+
* ```html
80+
* <span>Ether</span>
81+
* ```
82+
*
83+
*
84+
* ### Format the name (capitalize, truncate, etc.)
85+
* The TokenName component accepts a `formatFn` which takes in a string and outputs a string
86+
* The function is used to modify the name of the token
87+
*
88+
* ```tsx
89+
* const concatStr = (str: string):string => str + "Token"
90+
*
91+
* <TokenName formatFn={concatStr} />
92+
* ```
93+
*
94+
* Result:
95+
* ```html
96+
* <span>Ether Token</span>
97+
* ```
98+
*
99+
* ### Show a loading sign when the name is being fetched
100+
* ```tsx
101+
* import { TokenProvider, TokenName } from "thirdweb/react";
102+
*
103+
* <TokenProvider address="0x...">
104+
* <TokenName loadingComponent={<Spinner />} />
105+
* </TokenProvider>
106+
* ```
107+
*
108+
* ### Fallback to something when the name fails to resolve
109+
* ```tsx
110+
* <TokenProvider address="0x...">
111+
* <TokenName fallbackComponent={"Failed to load"} />
112+
* </TokenProvider>
113+
* ```
114+
*
115+
* ### Custom query options for useQuery
116+
* This component uses `@tanstack-query`'s useQuery internally.
117+
* You can use the `queryOptions` prop for more fine-grained control
118+
* ```tsx
119+
* <TokenName queryOption={{
120+
* enabled: isEnabled,
121+
* retry: 4,
122+
* }}
123+
* />
124+
* ```
125+
*
126+
* @component
127+
* @token
128+
*/
129+
export function TokenName({
130+
nameResolver,
131+
formatFn,
132+
loadingComponent,
133+
fallbackComponent,
134+
queryOptions,
135+
...restProps
136+
}: TokenNameProps) {
137+
const { address, client, chain } = useTokenContext();
138+
const nameQuery = useQuery({
139+
queryKey: ["_internal_token_name_", chain.id, address] as const,
140+
queryFn: async () => {
141+
if (typeof nameResolver === "string") {
142+
return nameResolver;
143+
}
144+
if (typeof nameResolver === "function") {
145+
return nameResolver();
146+
}
147+
if (address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()) {
148+
// Don't wanna use `getChainNativeCurrencyName` because it has some side effect (it catches error and defaults to "ETH")
149+
return getChainMetadata(chain).then((data) => data.nativeCurrency.name);
150+
}
151+
return name({ contract: getContract({ address, client, chain }) });
152+
},
153+
...queryOptions,
154+
});
155+
156+
if (nameQuery.isLoading) {
157+
return loadingComponent || null;
158+
}
159+
160+
if (!nameQuery.data) {
161+
return fallbackComponent || null;
162+
}
163+
164+
const displayValue = formatFn
165+
? formatFn(Number(nameQuery.data))
166+
: nameQuery.data;
167+
168+
return <span {...restProps}>{displayValue}</span>;
169+
}

0 commit comments

Comments
 (0)