Skip to content

Commit

Permalink
[SDK] Chain headless components (#5495)
Browse files Browse the repository at this point in the history
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces headless components related to blockchain chain management in the `thirdweb` library, including `ChainProvider`, `ChainIcon`, and `ChainName`. It also adds tests for these components and updates documentation to reflect their usage.

### Detailed summary
- Added headless components: `ChainProvider`, `ChainIcon`, `ChainName`.
- Introduced `TokenIconProps` interface.
- Updated `react.ts` to export new components and their props.
- Added documentation for `ChainProvider`, `ChainIcon`, and `ChainName` in `page.mdx`.
- Implemented tests for `TokenSymbol`, `ChainProvider`, `TokenProvider`, `ChainName`.
- Enhanced `ChainProvider` and `ChainIcon` components with new props and functionalities.
- Updated `ChainName` component to support custom name resolvers and formatting functions.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
  • Loading branch information
kien-ngo committed Nov 25, 2024
1 parent dfc824d commit d1845f3
Show file tree
Hide file tree
Showing 11 changed files with 640 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-carrots-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Add headless components: ChainProvider, ChainIcon & ChainName
23 changes: 23 additions & 0 deletions apps/portal/src/app/react/v5/components/onchain/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,27 @@ Build your own UI and interact with onchain data using headless components.
description="Component to display the description of an NFT"
/>

### Chains

<ArticleIconCard
title="ChainProvider"
icon={ReactIcon}
href="/references/typescript/v5/ChainProvider"
description="Component to provide the Chain context to your app"
/>

<ArticleIconCard
title="ChainIcon"
icon={ReactIcon}
href="/references/typescript/v5/ChainIcon"
description="Component to display the icon of a chain"
/>

<ArticleIconCard
title="ChainName"
icon={ReactIcon}
href="/references/typescript/v5/ChainName"
description="Component to display the name of a chain"
/>

</Stack>
14 changes: 14 additions & 0 deletions packages/thirdweb/src/exports/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,17 @@ export {
TokenIcon,
type TokenIconProps,
} from "../react/web/ui/prebuilt/Token/icon.js";

// Chain
export {
ChainProvider,
type ChainProviderProps,
} from "../react/web/ui/prebuilt/Chain/provider.js";
export {
ChainName,
type ChainNameProps,
} from "../react/web/ui/prebuilt/Chain/name.js";
export {
ChainIcon,
type ChainIconProps,
} from "../react/web/ui/prebuilt/Chain/icon.js";
154 changes: 154 additions & 0 deletions packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import type { JSX } from "react";
import { getChainMetadata } from "../../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useChainContext } from "./provider.js";

/**
* Props for the ChainIcon component
* @chain
* @component
*/
export interface ChainIconProps
extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> {
/**
* You need a ThirdwebClient to resolve the icon which is hosted on IPFS.
* (since most chain icons are hosted on IPFS, loading them via thirdweb gateway will ensure better performance)
*/
client: ThirdwebClient;
/**
* This prop can be a string or a (async) function that resolves to a string, representing the icon url of the chain
* This is particularly useful if you already have a way to fetch the chain icon.
*/
iconResolver?: string | (() => string) | (() => Promise<string>);
/**
* 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
* <ChainIcon loadingComponent={<Spinner />} />
* ```
*/
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
* <ChainIcon fallbackComponent={<DummyImage />} />
* ```
*/
fallbackComponent?: JSX.Element;

/**
* Optional query options for `useQuery`
*/
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
}

/**
* This component tries to resolve the icon of a given chain, then return an image.
* @returns an <img /> with the src of the chain icon
*
* @example
* ### Basic usage
* ```tsx
* import { ChainProvider, ChainIcon } from "thirdweb/react";
*
* <ChainProvider chain={chain}>
* <ChainIcon />
* </ChainProvider>
* ```
*
* Result: An <img /> component with the src of the icon
* ```html
* <img src="chain-icon.png" />
* ```
*
* ### 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 ChainIcon
* ```tsx
* <ChainIcon iconResolver="/ethereum-icon.png" />
* ```
*
* You can also pass in your own custom (async) function that retrieves the icon url
* ```tsx
* const getIcon = async () => {
* const icon = getIconFromCoinMarketCap(chainId, etc);
* return icon;
* };
*
* <ChainIcon iconResolver={getIcon} />
* ```
*
* ### Show a loading sign while the icon is being loaded
* ```tsx
* <ChainIcon loadingComponent={<Spinner />} />
* ```
*
* ### Fallback to a dummy image if the chain icon fails to resolve
* ```tsx
* <ChainIcon fallbackComponent={<img src="blank-image.png" />} />
* ```
*
* ### Usage with queryOptions
* ChainIcon 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
* <ChainIcon queryOptions={{ enabled: someLogic, retry: 3, }} />
* ```
*
* @component
* @chain
* @beta
*/
export function ChainIcon({
iconResolver,
loadingComponent,
fallbackComponent,
queryOptions,
client,
...restProps
}: ChainIconProps) {
const { chain } = useChainContext();
const iconQuery = useQuery({
queryKey: ["_internal_chain_icon_", chain.id] as const,
queryFn: async () => {
if (typeof iconResolver === "string") {
return iconResolver;
}
if (typeof iconResolver === "function") {
return iconResolver();
}
// Check if the chain object already has "icon"
if (chain.icon?.url) {
return chain.icon.url;
}
const possibleUrl = await getChainMetadata(chain).then(
(data) => data.icon?.url,
);
if (!possibleUrl) {
throw new Error("Failed to resolve icon for chain");
}
return resolveScheme({ uri: possibleUrl, client });
},
...queryOptions,
});

if (iconQuery.isLoading) {
return loadingComponent || null;
}

if (!iconQuery.data) {
return fallbackComponent || null;
}

return <img src={iconQuery.data} {...restProps} alt={restProps.alt} />;
}
73 changes: 73 additions & 0 deletions packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { render, screen, waitFor } from "~test/react-render.js";
import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
import { defineChain } from "../../../../../chains/utils.js";
import { ChainName } from "./name.js";
import { ChainProvider } from "./provider.js";

describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
it("should return the correct chain name, if the name exists in the chain object", () => {
render(
<ChainProvider chain={ethereum}>
<ChainName />
</ChainProvider>,
);
waitFor(() =>
expect(
screen.getByText("Ethereum", {
exact: true,
selector: "span",
}),
).toBeInTheDocument(),
);
});

it("should return the correct chain name, if the name is loaded from the server", () => {
render(
<ChainProvider chain={defineChain(1)}>
<ChainName />
</ChainProvider>,
);
waitFor(() =>
expect(
screen.getByText("Ethereum Mainnet", {
exact: true,
selector: "span",
}),
).toBeInTheDocument(),
);
});

it("should return the correct FORMATTED chain name", () => {
render(
<ChainProvider chain={ethereum}>
<ChainName formatFn={(str: string) => `${str}-formatted`} />
</ChainProvider>,
);
waitFor(() =>
expect(
screen.getByText("Ethereum-formatted", {
exact: true,
selector: "span",
}),
).toBeInTheDocument(),
);
});

it("should fallback properly when fail to resolve chain name", () => {
render(
<ChainProvider chain={defineChain(-1)}>
<ChainName fallbackComponent={<span>oops</span>} />
</ChainProvider>,
);

waitFor(() =>
expect(
screen.getByText("oops", {
exact: true,
selector: "span",
}),
).toBeInTheDocument(),
);
});
});
Loading

0 comments on commit d1845f3

Please sign in to comment.