Skip to content

Commit f69d1aa

Browse files
committed
[SDK] Update to NFT Components (#5655)
CNCT-2584 - add custom resolver methods - add better test coverage - add caching for the getNFT method - small fix: handling falsy values <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the NFT components in the `thirdweb` library by improving functionality, performance, and test coverage. It introduces custom resolver methods and caching mechanisms, along with fixes for handling falsy values. ### Detailed summary - Added custom resolver methods to `NFTMedia`, `NFTName`, and `NFTDescription`. - Implemented caching for the NFT info getter to improve performance. - Fixed handling of falsy values for NFT media `src`, `name`, and `description`. - Enhanced test coverage by extracting internal logic for testing. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent f680496 commit f69d1aa

File tree

12 files changed

+626
-137
lines changed

12 files changed

+626
-137
lines changed

.changeset/serious-bananas-boil.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Improve NFT Components
6+
- Add custom resolver methods to NFTMedia, NFTName and NFTDescription
7+
- Add caching for the NFT-info-getter method to improve performance
8+
- Small fix to handle falsy values for NFT media src, name and description
9+
- Improve test coverage by extracting internal logics and testing them

packages/thirdweb/src/exports/react.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ export {
189189
export {
190190
NFTMedia,
191191
type NFTMediaProps,
192+
type NFTMediaInfo,
192193
} from "../react/web/ui/prebuilt/NFT/media.js";
193194

194195
export { useConnectionManager } from "../react/core/providers/connection-manager.js";
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
DOODLES_CONTRACT,
4+
DROP1155_CONTRACT,
5+
UNISWAPV3_FACTORY_CONTRACT,
6+
} from "~test/test-contracts.js";
7+
import { fetchNftDescription } from "./description.js";
8+
9+
describe.runIf(process.env.TW_SECRET_KEY)("NFTDescription", () => {
10+
it("fetchNftDescription should work with ERC721", async () => {
11+
const desc = await fetchNftDescription({
12+
contract: DOODLES_CONTRACT,
13+
tokenId: 0n,
14+
});
15+
expect(desc).toBe(
16+
"A community-driven collectibles project featuring art by Burnt Toast. Doodles come in a joyful range of colors, traits and sizes with a collection size of 10,000. Each Doodle allows its owner to vote for experiences and activations paid for by the Doodles Community Treasury. Burnt Toast is the working alias for Scott Martin, a Canadian–based illustrator, designer, animator and muralist.",
17+
);
18+
});
19+
20+
it("fetchNftDescription should work with ERC1155", async () => {
21+
const desc = await fetchNftDescription({
22+
contract: DROP1155_CONTRACT,
23+
tokenId: 0n,
24+
});
25+
expect(desc).toBe("");
26+
});
27+
28+
it("fetchNftDescription should respect descriptionResolver as a string", async () => {
29+
const desc = await fetchNftDescription({
30+
contract: DOODLES_CONTRACT,
31+
tokenId: 0n,
32+
descriptionResolver: "string",
33+
});
34+
expect(desc).toBe("string");
35+
});
36+
37+
it("fetchNftDescription should respect descriptionResolver as a non-async function", async () => {
38+
const desc = await fetchNftDescription({
39+
contract: DOODLES_CONTRACT,
40+
tokenId: 0n,
41+
descriptionResolver: () => "non-async",
42+
});
43+
expect(desc).toBe("non-async");
44+
});
45+
46+
it("fetchNftDescription should respect descriptionResolver as a async function", async () => {
47+
const desc = await fetchNftDescription({
48+
contract: DOODLES_CONTRACT,
49+
tokenId: 0n,
50+
descriptionResolver: async () => "async",
51+
});
52+
expect(desc).toBe("async");
53+
});
54+
55+
it("fetchNftDescription should throw error if failed to resolve nft info", async () => {
56+
await expect(() =>
57+
fetchNftDescription({
58+
contract: UNISWAPV3_FACTORY_CONTRACT,
59+
tokenId: 0n,
60+
}),
61+
).rejects.toThrowError("Failed to resolve NFT info");
62+
});
63+
});

packages/thirdweb/src/react/web/ui/prebuilt/NFT/description.tsx

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"use client";
22

3-
import type { UseQueryOptions } from "@tanstack/react-query";
3+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type { JSX } from "react";
5-
import type { NFT } from "../../../../../utils/nft/parseNft.js";
6-
import { useNftInfo } from "./hooks.js";
5+
import type { ThirdwebContract } from "../../../../../contract/contract.js";
76
import { useNFTContext } from "./provider.js";
7+
import { getNFTInfo } from "./utils.js";
88

99
export interface NFTDescriptionProps
1010
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
@@ -13,7 +13,12 @@ export interface NFTDescriptionProps
1313
/**
1414
* Optional `useQuery` params
1515
*/
16-
queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
16+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
17+
/**
18+
* This prop can be a string or a (async) function that resolves to a string, representing the description of the NFT
19+
* This is particularly useful if you already have a way to fetch the data.
20+
*/
21+
descriptionResolver?: string | (() => string) | (() => Promise<string>);
1722
}
1823

1924
/**
@@ -58,6 +63,21 @@ export interface NFTDescriptionProps
5863
* </NFTProvider>
5964
* ```
6065
*
66+
* ### Override the description with the `descriptionResolver` prop
67+
* If you already have the url, you can skip the network requests and pass it directly to the NFTDescription
68+
* ```tsx
69+
* <NFTDescription descriptionResolver="The desc of the NFT" />
70+
* ```
71+
*
72+
* You can also pass in your own custom (async) function that retrieves the description
73+
* ```tsx
74+
* const getDescription = async () => {
75+
* // ...
76+
* return description;
77+
* };
78+
*
79+
* <NFTDescription descriptionResolver={getDescription} />
80+
* ```
6181
* @component
6282
* @nft
6383
* @beta
@@ -66,22 +86,61 @@ export function NFTDescription({
6686
loadingComponent,
6787
fallbackComponent,
6888
queryOptions,
89+
descriptionResolver,
6990
...restProps
7091
}: NFTDescriptionProps) {
7192
const { contract, tokenId } = useNFTContext();
72-
const nftQuery = useNftInfo({
73-
contract,
74-
tokenId,
75-
queryOptions,
93+
const descQuery = useQuery({
94+
queryKey: [
95+
"_internal_nft_description_",
96+
contract.chain.id,
97+
tokenId.toString(),
98+
{
99+
resolver:
100+
typeof descriptionResolver === "string"
101+
? descriptionResolver
102+
: typeof descriptionResolver === "function"
103+
? descriptionResolver.toString()
104+
: undefined,
105+
},
106+
],
107+
queryFn: async (): Promise<string> =>
108+
fetchNftDescription({ descriptionResolver, contract, tokenId }),
109+
...queryOptions,
76110
});
77111

78-
if (nftQuery.isLoading) {
112+
if (descQuery.isLoading) {
79113
return loadingComponent || null;
80114
}
81115

82-
if (!nftQuery.data?.metadata?.description) {
116+
if (!descQuery.data) {
83117
return fallbackComponent || null;
84118
}
85119

86-
return <span {...restProps}>{nftQuery.data.metadata.description}</span>;
120+
return <span {...restProps}>{descQuery.data}</span>;
121+
}
122+
123+
/**
124+
* @internal Exported for tests
125+
*/
126+
export async function fetchNftDescription(props: {
127+
descriptionResolver?: string | (() => string) | (() => Promise<string>);
128+
contract: ThirdwebContract;
129+
tokenId: bigint;
130+
}): Promise<string> {
131+
const { descriptionResolver, contract, tokenId } = props;
132+
if (typeof descriptionResolver === "string") {
133+
return descriptionResolver;
134+
}
135+
if (typeof descriptionResolver === "function") {
136+
return descriptionResolver();
137+
}
138+
const nft = await getNFTInfo({ contract, tokenId }).catch(() => undefined);
139+
if (!nft) {
140+
throw new Error("Failed to resolve NFT info");
141+
}
142+
if (typeof nft.metadata.description !== "string") {
143+
throw new Error("Failed to resolve NFT description");
144+
}
145+
return nft.metadata.description;
87146
}

packages/thirdweb/src/react/web/ui/prebuilt/NFT/hooks.tsx

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
DOODLES_CONTRACT,
4+
DROP1155_CONTRACT,
5+
UNISWAPV3_FACTORY_CONTRACT,
6+
} from "~test/test-contracts.js";
7+
import { fetchNftMedia } from "./media.js";
8+
9+
describe.runIf(process.env.TW_SECRET_KEY)("NFTMedia", () => {
10+
it("fetchNftMedia should work with ERC721", async () => {
11+
const desc = await fetchNftMedia({
12+
contract: DOODLES_CONTRACT,
13+
tokenId: 0n,
14+
});
15+
expect(desc).toStrictEqual({
16+
src: "ipfs://QmUEfFfwAh4wyB5UfHCVPUxis4j4Q4kJXtm5x5p3g1fVUn",
17+
poster: undefined,
18+
});
19+
});
20+
21+
it("fetchNftMedia should work with ERC1155", async () => {
22+
const desc = await fetchNftMedia({
23+
contract: DROP1155_CONTRACT,
24+
tokenId: 0n,
25+
});
26+
expect(desc).toStrictEqual({
27+
src: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/1.mp4",
28+
poster: "ipfs://QmeGCqV1mSHTZrvuFzW1XZdCRRGXB6AmSotTqHoxA2xfDo/0.png",
29+
});
30+
});
31+
32+
it("fetchNftMedia should respect mediaResolver as a string", async () => {
33+
const desc = await fetchNftMedia({
34+
contract: DOODLES_CONTRACT,
35+
tokenId: 0n,
36+
mediaResolver: {
37+
src: "string",
38+
poster: undefined,
39+
},
40+
});
41+
expect(desc).toStrictEqual({ src: "string", poster: undefined });
42+
});
43+
44+
it("fetchNftMedia should respect mediaResolver as a non-async function", async () => {
45+
const desc = await fetchNftMedia({
46+
contract: DOODLES_CONTRACT,
47+
tokenId: 0n,
48+
mediaResolver: () => ({
49+
src: "non-async",
50+
poster: undefined,
51+
}),
52+
});
53+
expect(desc).toStrictEqual({ src: "non-async", poster: undefined });
54+
});
55+
56+
it("fetchNftMedia should respect mediaResolver as a async function", async () => {
57+
const desc = await fetchNftMedia({
58+
contract: DOODLES_CONTRACT,
59+
tokenId: 0n,
60+
mediaResolver: async () =>
61+
await {
62+
src: "async",
63+
poster: undefined,
64+
},
65+
});
66+
expect(desc).toStrictEqual({ src: "async", poster: undefined });
67+
});
68+
69+
it("fetchNftMedia should throw error if failed to resolve nft info", async () => {
70+
await expect(() =>
71+
fetchNftMedia({
72+
contract: UNISWAPV3_FACTORY_CONTRACT,
73+
tokenId: 0n,
74+
}),
75+
).rejects.toThrowError("Failed to resolve NFT info");
76+
});
77+
});

0 commit comments

Comments
 (0)