Skip to content

Commit 64d6599

Browse files
committed
update
1 parent b5baeae commit 64d6599

File tree

9 files changed

+627
-0
lines changed

9 files changed

+627
-0
lines changed

packages/thirdweb/src/exports/react.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,17 @@ export {
253253
TokenIcon,
254254
type TokenIconProps,
255255
} from "../react/web/ui/prebuilt/Token/icon.js";
256+
257+
// Chain
258+
export {
259+
ChainProvider,
260+
type ChainProviderProps,
261+
} from "../react/web/ui/prebuilt/Chain/provider.js";
262+
export {
263+
ChainName,
264+
type ChainNameProps,
265+
} from "../react/web/ui/prebuilt/Chain/name.js";
266+
export {
267+
ChainIcon,
268+
type ChainIconProps,
269+
} from "../react/web/ui/prebuilt/Chain/icon.js";
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
2+
import type { JSX } from "react";
3+
import { getChainMetadata } from "../../../../../chains/utils.js";
4+
import type { ThirdwebClient } from "../../../../../client/client.js";
5+
import {
6+
findIPFSCidFromUri,
7+
resolveScheme,
8+
} from "../../../../../utils/ipfs.js";
9+
import { useChainContext } from "./provider.js";
10+
11+
/**
12+
* Props for the ChainIcon component
13+
* @chain
14+
* @component
15+
*/
16+
export interface ChainIconProps
17+
extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> {
18+
/**
19+
* While marked as "optional", you should pass a ThirdwebClient to this prop
20+
* if you want to use your clientId for the chain icon
21+
* (since most chain icons are hosted on IPFS, loading them via thirdweb gateway will ensure better performance)
22+
*/
23+
client?: ThirdwebClient;
24+
/**
25+
* This prop can be a string or a (async) function that resolves to a string, representing the icon url of the chain
26+
* This is particularly useful if you already have a way to fetch the chain icon.
27+
*/
28+
iconResolver?: string | (() => string) | (() => Promise<string>);
29+
/**
30+
* This component will be shown while the avatar of the icon is being fetched
31+
* If not passed, the component will return `null`.
32+
*
33+
* You can pass a loading sign or spinner to this prop.
34+
* @example
35+
* ```tsx
36+
* <ChainIcon loadingComponent={<Spinner />} />
37+
* ```
38+
*/
39+
loadingComponent?: JSX.Element;
40+
/**
41+
* This component will be shown if the request for fetching the avatar is done
42+
* but could not retreive any result.
43+
* You can pass a dummy avatar/image to this prop.
44+
*
45+
* If not passed, the component will return `null`
46+
*
47+
* @example
48+
* ```tsx
49+
* <ChainIcon fallbackComponent={<DummyImage />} />
50+
* ```
51+
*/
52+
fallbackComponent?: JSX.Element;
53+
54+
/**
55+
* Optional query options for `useQuery`
56+
*/
57+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
58+
}
59+
60+
/**
61+
* This component tries to resolve the icon of a given chain, then return an image.
62+
* @returns an <img /> with the src of the chain icon
63+
*
64+
* @example
65+
* ### Basic usage
66+
* ```tsx
67+
* import { ChainProvider, ChainIcon } from "thirdweb/react";
68+
*
69+
* <ChainProvider chain={chain}>
70+
* <ChainIcon />
71+
* </ChainProvider>
72+
* ```
73+
*
74+
* Result: An <img /> component with the src of the icon
75+
* ```html
76+
* <img src="chain-icon.png" />
77+
* ```
78+
*
79+
* ### Override the icon with the `iconResolver` prop
80+
* If you already have the icon url, you can skip the network requests and pass it directly to the ChainIcon
81+
* ```tsx
82+
* <ChainIcon iconResolver="/ethereum-icon.png" />
83+
* ```
84+
*
85+
* You can also pass in your own custom (async) function that retrieves the icon url
86+
* ```tsx
87+
* const getIcon = async () => {
88+
* const icon = getIconFromCoinMarketCap(chainId, etc);
89+
* return icon;
90+
* };
91+
*
92+
* <ChainIcon iconResolver={getIcon} />
93+
* ```
94+
*
95+
* ### Show a loading sign while the icon is being loaded
96+
* ```tsx
97+
* <ChainIcon loadingComponent={<Spinner />} />
98+
* ```
99+
*
100+
* ### Fallback to a dummy image if the chain icon fails to resolve
101+
* ```tsx
102+
* <ChainIcon fallbackComponent={<img src="blank-image.png" />} />
103+
* ```
104+
*
105+
* ### Usage with queryOptions
106+
* ChainIcon uses useQuery() from tanstack query internally.
107+
* It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
108+
* ```tsx
109+
* <ChainIcon queryOptions={{ enabled: someLogic, retry: 3, }} />
110+
* ```
111+
*
112+
* @component
113+
* @chain
114+
* @beta
115+
*/
116+
export function ChainIcon({
117+
iconResolver,
118+
loadingComponent,
119+
fallbackComponent,
120+
queryOptions,
121+
client,
122+
...restProps
123+
}: ChainIconProps) {
124+
const { chain } = useChainContext();
125+
const iconQuery = useQuery({
126+
queryKey: ["_internal_chain_icon_", chain.id] as const,
127+
queryFn: async () => {
128+
if (typeof iconResolver === "string") {
129+
return iconResolver;
130+
}
131+
if (typeof iconResolver === "function") {
132+
return iconResolver();
133+
}
134+
// Check if the chain object already has "icon"
135+
if (chain.icon?.url) {
136+
return chain.icon.url;
137+
}
138+
const possibleUrl = await getChainMetadata(chain).then(
139+
(data) => data.icon?.url,
140+
);
141+
if (!possibleUrl) {
142+
throw new Error("Failed to resolve icon for chain");
143+
}
144+
if (client) {
145+
return resolveScheme({ uri: possibleUrl, client });
146+
}
147+
// If a thirdweb client is not passed, try to manually resolve the uri
148+
if (possibleUrl.startsWith("ipfs://")) {
149+
const cid = findIPFSCidFromUri(possibleUrl);
150+
if (!cid) {
151+
throw new Error(`Failed to resolve IPFS CID from ${possibleUrl}`);
152+
}
153+
return `https://ipfs.io/${cid}`;
154+
}
155+
return possibleUrl;
156+
},
157+
...queryOptions,
158+
});
159+
160+
if (iconQuery.isLoading) {
161+
return loadingComponent || null;
162+
}
163+
164+
if (!iconQuery.data) {
165+
return fallbackComponent || null;
166+
}
167+
168+
return <img src={iconQuery.data} {...restProps} alt={restProps.alt} />;
169+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from "vitest";
2+
import { render, screen, waitFor } from "~test/react-render.js";
3+
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
4+
import { defineChain } from "../../../../../chains/utils.js";
5+
import { ChainName } from "./name.js";
6+
import { ChainProvider } from "./provider.js";
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
9+
it("should return the correct chain name, if the name exists in the chain object", () => {
10+
render(
11+
<ChainProvider chain={ethereum}>
12+
<ChainName />
13+
</ChainProvider>,
14+
);
15+
waitFor(() =>
16+
expect(
17+
screen.getByText("Ethereum", {
18+
exact: true,
19+
selector: "span",
20+
}),
21+
).toBeInTheDocument(),
22+
);
23+
});
24+
25+
it("should return the correct chain name, if the name is loaded from the server", () => {
26+
render(
27+
<ChainProvider chain={defineChain(1)}>
28+
<ChainName />
29+
</ChainProvider>,
30+
);
31+
waitFor(() =>
32+
expect(
33+
screen.getByText("Ethereum Mainnet", {
34+
exact: true,
35+
selector: "span",
36+
}),
37+
).toBeInTheDocument(),
38+
);
39+
});
40+
41+
it("should return the correct FORMATTED chain name", () => {
42+
render(
43+
<ChainProvider chain={ethereum}>
44+
<ChainName formatFn={(str: string) => `${str}-formatted`} />
45+
</ChainProvider>,
46+
);
47+
waitFor(() =>
48+
expect(
49+
screen.getByText("Ethereum-formatted", {
50+
exact: true,
51+
selector: "span",
52+
}),
53+
).toBeInTheDocument(),
54+
);
55+
});
56+
57+
it("should fallback properly when fail to resolve chain name", () => {
58+
render(
59+
<ChainProvider chain={defineChain(-1)}>
60+
<ChainName fallbackComponent={<span>oops</span>} />
61+
</ChainProvider>,
62+
);
63+
64+
waitFor(() =>
65+
expect(
66+
screen.getByText("oops", {
67+
exact: true,
68+
selector: "span",
69+
}),
70+
).toBeInTheDocument(),
71+
);
72+
});
73+
});

0 commit comments

Comments
 (0)