thirdweb 5.76.0-nightly-8234dbae8fcf73ca83bbda31d929aa57ca521a53-20241208000407 → 5.76.0-nightly-485dcc6020089a80d994c24882f389c24a0af039-20241210000351

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/cjs/exports/react.js.map +1 -1
  2. package/dist/cjs/react/web/ui/prebuilt/NFT/description.js +57 -9
  3. package/dist/cjs/react/web/ui/prebuilt/NFT/description.js.map +1 -1
  4. package/dist/cjs/react/web/ui/prebuilt/NFT/media.js +72 -13
  5. package/dist/cjs/react/web/ui/prebuilt/NFT/media.js.map +1 -1
  6. package/dist/cjs/react/web/ui/prebuilt/NFT/name.js +58 -9
  7. package/dist/cjs/react/web/ui/prebuilt/NFT/name.js.map +1 -1
  8. package/dist/cjs/react/web/ui/prebuilt/NFT/utils.js +34 -0
  9. package/dist/cjs/react/web/ui/prebuilt/NFT/utils.js.map +1 -0
  10. package/dist/cjs/version.js +1 -1
  11. package/dist/cjs/wallets/coinbase/coinbase-web.js +1 -3
  12. package/dist/cjs/wallets/coinbase/coinbase-web.js.map +1 -1
  13. package/dist/cjs/wallets/in-app/core/actions/generate-wallet.enclave.js +1 -1
  14. package/dist/cjs/wallets/in-app/core/actions/generate-wallet.enclave.js.map +1 -1
  15. package/dist/cjs/wallets/in-app/core/actions/sign-message.enclave.js +1 -1
  16. package/dist/cjs/wallets/in-app/core/actions/sign-message.enclave.js.map +1 -1
  17. package/dist/cjs/wallets/in-app/core/actions/sign-transaction.enclave.js +1 -1
  18. package/dist/cjs/wallets/in-app/core/actions/sign-transaction.enclave.js.map +1 -1
  19. package/dist/cjs/wallets/in-app/core/actions/sign-typed-data.enclave.js +1 -1
  20. package/dist/cjs/wallets/in-app/core/actions/sign-typed-data.enclave.js.map +1 -1
  21. package/dist/cjs/wallets/injected/index.js +14 -20
  22. package/dist/cjs/wallets/injected/index.js.map +1 -1
  23. package/dist/cjs/wallets/smart/lib/userop.js +1 -1
  24. package/dist/cjs/wallets/smart/lib/userop.js.map +1 -1
  25. package/dist/esm/exports/react.js.map +1 -1
  26. package/dist/esm/react/web/ui/prebuilt/NFT/description.js +56 -9
  27. package/dist/esm/react/web/ui/prebuilt/NFT/description.js.map +1 -1
  28. package/dist/esm/react/web/ui/prebuilt/NFT/media.js +71 -13
  29. package/dist/esm/react/web/ui/prebuilt/NFT/media.js.map +1 -1
  30. package/dist/esm/react/web/ui/prebuilt/NFT/name.js +57 -9
  31. package/dist/esm/react/web/ui/prebuilt/NFT/name.js.map +1 -1
  32. package/dist/esm/react/web/ui/prebuilt/NFT/utils.js +32 -0
  33. package/dist/esm/react/web/ui/prebuilt/NFT/utils.js.map +1 -0
  34. package/dist/esm/version.js +1 -1
  35. package/dist/esm/wallets/coinbase/coinbase-web.js +1 -3
  36. package/dist/esm/wallets/coinbase/coinbase-web.js.map +1 -1
  37. package/dist/esm/wallets/in-app/core/actions/generate-wallet.enclave.js +1 -1
  38. package/dist/esm/wallets/in-app/core/actions/generate-wallet.enclave.js.map +1 -1
  39. package/dist/esm/wallets/in-app/core/actions/sign-message.enclave.js +1 -1
  40. package/dist/esm/wallets/in-app/core/actions/sign-message.enclave.js.map +1 -1
  41. package/dist/esm/wallets/in-app/core/actions/sign-transaction.enclave.js +1 -1
  42. package/dist/esm/wallets/in-app/core/actions/sign-transaction.enclave.js.map +1 -1
  43. package/dist/esm/wallets/in-app/core/actions/sign-typed-data.enclave.js +1 -1
  44. package/dist/esm/wallets/in-app/core/actions/sign-typed-data.enclave.js.map +1 -1
  45. package/dist/esm/wallets/injected/index.js +14 -20
  46. package/dist/esm/wallets/injected/index.js.map +1 -1
  47. package/dist/esm/wallets/smart/lib/userop.js +1 -1
  48. package/dist/esm/wallets/smart/lib/userop.js.map +1 -1
  49. package/dist/types/exports/react.d.ts +1 -1
  50. package/dist/types/exports/react.d.ts.map +1 -1
  51. package/dist/types/react/web/ui/prebuilt/NFT/description.d.ts +32 -4
  52. package/dist/types/react/web/ui/prebuilt/NFT/description.d.ts.map +1 -1
  53. package/dist/types/react/web/ui/prebuilt/NFT/media.d.ts +52 -4
  54. package/dist/types/react/web/ui/prebuilt/NFT/media.d.ts.map +1 -1
  55. package/dist/types/react/web/ui/prebuilt/NFT/name.d.ts +33 -4
  56. package/dist/types/react/web/ui/prebuilt/NFT/name.d.ts.map +1 -1
  57. package/dist/types/react/web/ui/prebuilt/NFT/utils.d.ts +7 -0
  58. package/dist/types/react/web/ui/prebuilt/NFT/utils.d.ts.map +1 -0
  59. package/dist/types/version.d.ts +1 -1
  60. package/dist/types/wallets/coinbase/coinbase-web.d.ts.map +1 -1
  61. package/dist/types/wallets/in-app/core/actions/generate-wallet.enclave.d.ts.map +1 -1
  62. package/dist/types/wallets/in-app/core/actions/sign-message.enclave.d.ts.map +1 -1
  63. package/dist/types/wallets/in-app/core/actions/sign-transaction.enclave.d.ts.map +1 -1
  64. package/dist/types/wallets/in-app/core/actions/sign-typed-data.enclave.d.ts.map +1 -1
  65. package/dist/types/wallets/smart/lib/userop.d.ts.map +1 -1
  66. package/package.json +1 -1
  67. package/src/exports/react.ts +1 -0
  68. package/src/react/web/ui/prebuilt/NFT/description.test.tsx +63 -0
  69. package/src/react/web/ui/prebuilt/NFT/description.tsx +70 -11
  70. package/src/react/web/ui/prebuilt/NFT/media.test.tsx +77 -0
  71. package/src/react/web/ui/prebuilt/NFT/media.tsx +106 -20
  72. package/src/react/web/ui/prebuilt/NFT/name.test.tsx +61 -0
  73. package/src/react/web/ui/prebuilt/NFT/name.tsx +71 -11
  74. package/src/react/web/ui/prebuilt/NFT/{NFT.test.tsx → provider.test.tsx} +34 -42
  75. package/src/react/web/ui/prebuilt/NFT/utils.test.ts +93 -0
  76. package/src/react/web/ui/prebuilt/NFT/utils.ts +41 -0
  77. package/src/utils/encoding/hex.test.ts +5 -0
  78. package/src/version.ts +1 -1
  79. package/src/wallets/coinbase/coinbase-web.ts +1 -5
  80. package/src/wallets/in-app/core/actions/generate-wallet.enclave.ts +3 -1
  81. package/src/wallets/in-app/core/actions/sign-message.enclave.ts +3 -1
  82. package/src/wallets/in-app/core/actions/sign-transaction.enclave.ts +3 -1
  83. package/src/wallets/in-app/core/actions/sign-typed-data.enclave.ts +3 -1
  84. package/src/wallets/injected/index.ts +14 -19
  85. package/src/wallets/smart/lib/userop.ts +3 -1
  86. package/src/wallets/smart/smart-wallet-dev.test.ts +18 -20
  87. package/dist/cjs/react/web/ui/prebuilt/NFT/hooks.js +0 -45
  88. package/dist/cjs/react/web/ui/prebuilt/NFT/hooks.js.map +0 -1
  89. package/dist/esm/react/web/ui/prebuilt/NFT/hooks.js +0 -41
  90. package/dist/esm/react/web/ui/prebuilt/NFT/hooks.js.map +0 -1
  91. package/dist/types/react/web/ui/prebuilt/NFT/hooks.d.ts +0 -14
  92. package/dist/types/react/web/ui/prebuilt/NFT/hooks.d.ts.map +0 -1
  93. package/src/react/web/ui/prebuilt/NFT/hooks.tsx +0 -53
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
- import type { UseQueryOptions } from "@tanstack/react-query";
3
+ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
4
4
  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";
7
6
  import { useNFTContext } from "./provider.js";
7
+ import { getNFTInfo } from "./utils.js";
8
8
 
9
9
  export interface NFTDescriptionProps
10
10
  extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
@@ -13,7 +13,12 @@ export interface NFTDescriptionProps
13
13
  /**
14
14
  * Optional `useQuery` params
15
15
  */
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>);
17
22
  }
18
23
 
19
24
  /**
@@ -58,6 +63,21 @@ export interface NFTDescriptionProps
58
63
  * </NFTProvider>
59
64
  * ```
60
65
  *
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
+ * ```
61
81
  * @component
62
82
  * @nft
63
83
  * @beta
@@ -66,22 +86,61 @@ export function NFTDescription({
66
86
  loadingComponent,
67
87
  fallbackComponent,
68
88
  queryOptions,
89
+ descriptionResolver,
69
90
  ...restProps
70
91
  }: NFTDescriptionProps) {
71
92
  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,
76
110
  });
77
111
 
78
- if (nftQuery.isLoading) {
112
+ if (descQuery.isLoading) {
79
113
  return loadingComponent || null;
80
114
  }
81
115
 
82
- if (!nftQuery.data?.metadata?.description) {
116
+ if (!descQuery.data) {
83
117
  return fallbackComponent || null;
84
118
  }
85
119
 
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;
87
146
  }
@@ -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
+ });
@@ -1,13 +1,25 @@
1
- import type { UseQueryOptions } from "@tanstack/react-query";
1
+ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
2
2
  import type { JSX } from "react";
3
- import type { NFT } from "../../../../../utils/nft/parseNft.js";
3
+ import type { ThirdwebContract } from "../../../../../contract/contract.js";
4
4
  import { MediaRenderer } from "../../MediaRenderer/MediaRenderer.js";
5
5
  import type { MediaRendererProps } from "../../MediaRenderer/types.js";
6
- import { useNftInfo } from "./hooks.js";
7
6
  import { useNFTContext } from "./provider.js";
7
+ import { getNFTInfo } from "./utils.js";
8
8
 
9
9
  /**
10
10
  * @component
11
+ * @beta
12
+ * @wallet
13
+ */
14
+ export type NFTMediaInfo = {
15
+ src: string;
16
+ poster: string | undefined;
17
+ };
18
+
19
+ /**
20
+ * @component
21
+ * @beta
22
+ * @wallet
11
23
  * The props for the <NFTMedia /> component
12
24
  * It is similar to the [`MediaRendererProps`](https://portal.thirdweb.com/references/typescript/v5/MediaRendererProps)
13
25
  * (excluding `src`, `poster` and `client`) that you can
@@ -22,7 +34,16 @@ export type NFTMediaProps = Omit<
22
34
  /**
23
35
  * Optional `useQuery` params
24
36
  */
25
- queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
37
+ queryOptions?: Omit<UseQueryOptions<NFTMediaInfo>, "queryFn" | "queryKey">;
38
+ /**
39
+ * This prop can be a string or a (async) function that resolves to a string, representing the media url of the NFT
40
+ * This is particularly useful if you already have a way to fetch the image.
41
+ * In case of function, the function must resolve to an object of type `NFTMediaInfo`
42
+ */
43
+ mediaResolver?:
44
+ | NFTMediaInfo
45
+ | (() => NFTMediaInfo)
46
+ | (() => Promise<NFTMediaInfo>);
26
47
  };
27
48
 
28
49
  /**
@@ -76,6 +97,26 @@ export type NFTMediaProps = Omit<
76
97
  * ```tsx
77
98
  * <NFTMedia style={{ borderRadius: "8px" }} className="mx-auto" />
78
99
  * ```
100
+ *
101
+ * ### Override the media with the `mediaResolver` prop
102
+ * If you already have the url, you can skip the network requests and pass it directly to the NFTMedia
103
+ * ```tsx
104
+ * <NFTMedia mediaResolver={{
105
+ * src: "/cat_video.mp4",
106
+ * // Poster is applicable to medias that are videos and audios
107
+ * poster: "/cat-image.png",
108
+ * }} />
109
+ * ```
110
+ *
111
+ * You can also pass in your own custom (async) function that retrieves the media url
112
+ * ```tsx
113
+ * const getMedia = async () => {
114
+ * const url = getNFTMedia(props);
115
+ * return url;
116
+ * };
117
+ *
118
+ * <NFTMedia mediaResolver={getMedia} />
119
+ * ```
79
120
  * @nft
80
121
  * @beta
81
122
  */
@@ -83,37 +124,82 @@ export function NFTMedia({
83
124
  loadingComponent,
84
125
  fallbackComponent,
85
126
  queryOptions,
127
+ mediaResolver,
86
128
  ...mediaRendererProps
87
129
  }: NFTMediaProps) {
88
130
  const { contract, tokenId } = useNFTContext();
89
- const nftQuery = useNftInfo({
90
- contract,
91
- tokenId,
92
- queryOptions,
131
+ const mediaQuery = useQuery({
132
+ queryKey: [
133
+ "_internal_nft_media_",
134
+ contract.chain.id,
135
+ tokenId.toString(),
136
+ {
137
+ resolver:
138
+ typeof mediaResolver === "object"
139
+ ? mediaResolver
140
+ : typeof mediaResolver === "function"
141
+ ? mediaResolver.toString()
142
+ : undefined,
143
+ },
144
+ ],
145
+ queryFn: async (): Promise<NFTMediaInfo> =>
146
+ fetchNftMedia({ mediaResolver, contract, tokenId }),
147
+ ...queryOptions,
93
148
  });
94
149
 
95
- if (nftQuery.isLoading) {
150
+ if (mediaQuery.isLoading) {
96
151
  return loadingComponent || null;
97
152
  }
98
153
 
99
- if (!nftQuery.data) {
100
- return fallbackComponent || null;
101
- }
102
-
103
- const animation_url = nftQuery.data.metadata.animation_url;
104
- const image =
105
- nftQuery.data.metadata.image || nftQuery.data.metadata.image_url;
106
-
107
- if (!animation_url && !image) {
154
+ if (!mediaQuery.data) {
108
155
  return fallbackComponent || null;
109
156
  }
110
157
 
111
158
  return (
112
159
  <MediaRenderer
113
160
  client={contract.client}
114
- src={animation_url || image}
115
- poster={image}
161
+ src={mediaQuery.data.src}
162
+ poster={mediaQuery.data.poster}
116
163
  {...mediaRendererProps}
117
164
  />
118
165
  );
119
166
  }
167
+
168
+ /**
169
+ * @internal Exported for tests only
170
+ */
171
+ export async function fetchNftMedia(props: {
172
+ mediaResolver?:
173
+ | NFTMediaInfo
174
+ | (() => NFTMediaInfo)
175
+ | (() => Promise<NFTMediaInfo>);
176
+ contract: ThirdwebContract;
177
+ tokenId: bigint;
178
+ }): Promise<{ src: string; poster: string | undefined }> {
179
+ const { mediaResolver, contract, tokenId } = props;
180
+ if (typeof mediaResolver === "object") {
181
+ return mediaResolver;
182
+ }
183
+ if (typeof mediaResolver === "function") {
184
+ return mediaResolver();
185
+ }
186
+ const nft = await getNFTInfo({ contract, tokenId }).catch(() => undefined);
187
+ if (!nft) {
188
+ throw new Error("Failed to resolve NFT info");
189
+ }
190
+ const animation_url = nft.metadata.animation_url;
191
+ const image = nft.metadata.image || nft.metadata.image_url;
192
+ if (animation_url) {
193
+ return {
194
+ src: animation_url,
195
+ poster: image || undefined,
196
+ };
197
+ }
198
+ if (image) {
199
+ return {
200
+ src: image,
201
+ poster: undefined,
202
+ };
203
+ }
204
+ throw new Error("Failed to resolve NFT media");
205
+ }
@@ -0,0 +1,61 @@
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 { fetchNftName } from "./name.js";
8
+
9
+ describe.runIf(process.env.TW_SECRET_KEY)("NFTName", () => {
10
+ it("fetchNftName should work with ERC721", async () => {
11
+ const desc = await fetchNftName({
12
+ contract: DOODLES_CONTRACT,
13
+ tokenId: 0n,
14
+ });
15
+ expect(desc).toBe("Doodle #0");
16
+ });
17
+
18
+ it("fetchNftName should work with ERC1155", async () => {
19
+ const desc = await fetchNftName({
20
+ contract: DROP1155_CONTRACT,
21
+ tokenId: 0n,
22
+ });
23
+ expect(desc).toBe("Aura OG");
24
+ });
25
+
26
+ it("fetchNftName should respect nameResolver as a string", async () => {
27
+ const desc = await fetchNftName({
28
+ contract: DOODLES_CONTRACT,
29
+ tokenId: 0n,
30
+ nameResolver: "string",
31
+ });
32
+ expect(desc).toBe("string");
33
+ });
34
+
35
+ it("fetchNftName should respect nameResolver as a non-async function", async () => {
36
+ const desc = await fetchNftName({
37
+ contract: DOODLES_CONTRACT,
38
+ tokenId: 0n,
39
+ nameResolver: () => "non-async",
40
+ });
41
+ expect(desc).toBe("non-async");
42
+ });
43
+
44
+ it("fetchNftName should respect nameResolver as a async function", async () => {
45
+ const desc = await fetchNftName({
46
+ contract: DOODLES_CONTRACT,
47
+ tokenId: 0n,
48
+ nameResolver: async () => "async",
49
+ });
50
+ expect(desc).toBe("async");
51
+ });
52
+
53
+ it("fetchNftName should throw error if failed to resolve nft info", async () => {
54
+ await expect(() =>
55
+ fetchNftName({
56
+ contract: UNISWAPV3_FACTORY_CONTRACT,
57
+ tokenId: 0n,
58
+ }),
59
+ ).rejects.toThrowError("Failed to resolve NFT info");
60
+ });
61
+ });
@@ -1,8 +1,8 @@
1
- import type { UseQueryOptions } from "@tanstack/react-query";
1
+ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
2
2
  import type { JSX } from "react";
3
- import type { NFT } from "../../../../../utils/nft/parseNft.js";
4
- import { useNftInfo } from "./hooks.js";
3
+ import type { ThirdwebContract } from "../../../../../contract/contract.js";
5
4
  import { useNFTContext } from "./provider.js";
5
+ import { getNFTInfo } from "./utils.js";
6
6
 
7
7
  export interface NFTNameProps
8
8
  extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
@@ -11,7 +11,12 @@ export interface NFTNameProps
11
11
  /**
12
12
  * Optional `useQuery` params
13
13
  */
14
- queryOptions?: Omit<UseQueryOptions<NFT>, "queryFn" | "queryKey">;
14
+ queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
15
+ /**
16
+ * This prop can be a string or a (async) function that resolves to a string, representing the name of the NFT
17
+ * This is particularly useful if you already have a way to fetch the name of the NFT.
18
+ */
19
+ nameResolver?: string | (() => string) | (() => Promise<string>);
15
20
  }
16
21
 
17
22
  /**
@@ -56,6 +61,22 @@ export interface NFTNameProps
56
61
  * </NFTProvider>
57
62
  * ```
58
63
  *
64
+ * ### Override the name with the `nameResolver` prop
65
+ * If you already have the name, you can skip the network requests and pass it directly to the NFTName
66
+ * ```tsx
67
+ * <NFTName nameResolver="Doodles #1" />
68
+ * ```
69
+ *
70
+ * You can also pass in your own custom (async) function that retrieves the name
71
+ * ```tsx
72
+ * const getName = async () => {
73
+ * // ...
74
+ * return name;
75
+ * };
76
+ *
77
+ * <NFTName nameResolver={getName} />
78
+ * ```
79
+ *
59
80
  * @nft
60
81
  * @component
61
82
  * @beta
@@ -64,22 +85,61 @@ export function NFTName({
64
85
  loadingComponent,
65
86
  fallbackComponent,
66
87
  queryOptions,
88
+ nameResolver,
67
89
  ...restProps
68
90
  }: NFTNameProps) {
69
91
  const { contract, tokenId } = useNFTContext();
70
92
 
71
- const nftQuery = useNftInfo({
72
- contract,
73
- tokenId,
74
- queryOptions,
93
+ const nameQuery = useQuery({
94
+ queryKey: [
95
+ "_internal_nft_name_",
96
+ contract.chain.id,
97
+ tokenId.toString(),
98
+ {
99
+ resolver:
100
+ typeof nameResolver === "string"
101
+ ? nameResolver
102
+ : typeof nameResolver === "function"
103
+ ? nameResolver.toString()
104
+ : undefined,
105
+ },
106
+ ],
107
+ queryFn: async (): Promise<string> =>
108
+ fetchNftName({ nameResolver, contract, tokenId }),
109
+ ...queryOptions,
75
110
  });
76
111
 
77
- if (nftQuery.isLoading) {
112
+ if (nameQuery.isLoading) {
78
113
  return loadingComponent || null;
79
114
  }
80
115
 
81
- if (!nftQuery.data?.metadata?.name) {
116
+ if (!nameQuery.data) {
82
117
  return fallbackComponent || null;
83
118
  }
84
- return <span {...restProps}>{nftQuery.data.metadata.name}</span>;
119
+ return <span {...restProps}>{nameQuery.data}</span>;
120
+ }
121
+
122
+ /**
123
+ * @internal Exported for tests
124
+ */
125
+ export async function fetchNftName(props: {
126
+ nameResolver?: string | (() => string) | (() => Promise<string>);
127
+ contract: ThirdwebContract;
128
+ tokenId: bigint;
129
+ }): Promise<string> {
130
+ const { nameResolver, contract, tokenId } = props;
131
+ if (typeof nameResolver === "string") {
132
+ return nameResolver;
133
+ }
134
+ if (typeof nameResolver === "function") {
135
+ return nameResolver();
136
+ }
137
+ const nft = await getNFTInfo({ contract, tokenId }).catch(() => undefined);
138
+ if (!nft) {
139
+ throw new Error("Failed to resolve NFT info");
140
+ }
141
+ if (typeof nft.metadata.name !== "string") {
142
+ throw new Error("Failed to resolve NFT name");
143
+ }
144
+ return nft.metadata.name;
85
145
  }
@@ -1,48 +1,13 @@
1
- import { useContext } from "react";
2
- import { describe, expect, it } from "vitest";
3
- import { render, screen, waitFor } from "~test/react-render.js";
1
+ import { type FC, useContext } from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, renderHook, screen, waitFor } from "~test/react-render.js";
4
4
  import { DOODLES_CONTRACT } from "~test/test-contracts.js";
5
- import { getNFTInfo } from "./hooks.js";
5
+ import { NFTDescription } from "./description.js";
6
6
  import { NFTMedia } from "./media.js";
7
7
  import { NFTName } from "./name.js";
8
- import { NFTProvider, NFTProviderContext } from "./provider.js";
9
-
10
- describe.runIf(process.env.TW_SECRET_KEY)("NFT prebuilt component", () => {
11
- it("should fetch the NFT metadata", async () => {
12
- const nft = await getNFTInfo({
13
- contract: DOODLES_CONTRACT,
14
- tokenId: 1n,
15
- });
16
- expect(nft.metadata).toStrictEqual({
17
- attributes: [
18
- {
19
- trait_type: "face",
20
- value: "holographic beard",
21
- },
22
- {
23
- trait_type: "hair",
24
- value: "white bucket cap",
25
- },
26
- {
27
- trait_type: "body",
28
- value: "purple sweater with satchel",
29
- },
30
- {
31
- trait_type: "background",
32
- value: "grey",
33
- },
34
- {
35
- trait_type: "head",
36
- value: "gradient 2",
37
- },
38
- ],
39
- description:
40
- "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.",
41
- image: "ipfs://QmTDxnzcvj2p3xBrKcGv1wxoyhAn2yzCQnZZ9LmFjReuH9",
42
- name: "Doodle #1",
43
- });
44
- });
8
+ import { NFTProvider, NFTProviderContext, useNFTContext } from "./provider.js";
45
9
 
10
+ describe.runIf(process.env.TW_SECRET_KEY)("NFTProvider", () => {
46
11
  it("should render children correctly", () => {
47
12
  render(
48
13
  <NFTProvider contract={DOODLES_CONTRACT} tokenId={0n}>
@@ -99,7 +64,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("NFT prebuilt component", () => {
99
64
  it("should render the NFT description", () => {
100
65
  render(
101
66
  <NFTProvider contract={DOODLES_CONTRACT} tokenId={1n}>
102
- <NFTName />
67
+ <NFTDescription />
103
68
  </NFTProvider>,
104
69
  );
105
70
 
@@ -111,4 +76,31 @@ describe.runIf(process.env.TW_SECRET_KEY)("NFT prebuilt component", () => {
111
76
  ).toBeInTheDocument(),
112
77
  );
113
78
  });
79
+
80
+ it("useNFTContext should return the context value when used within NFTProvider", () => {
81
+ const wrapper: FC = ({ children }: React.PropsWithChildren) => (
82
+ <NFTProvider contract={DOODLES_CONTRACT} tokenId={0n}>
83
+ {children}
84
+ </NFTProvider>
85
+ );
86
+
87
+ const { result } = renderHook(() => useNFTContext(), { wrapper });
88
+
89
+ expect(result.current.contract).toStrictEqual(DOODLES_CONTRACT);
90
+ expect(result.current.tokenId).toBe(0n);
91
+ });
92
+
93
+ it("useNFTContext should throw an error when used outside of NFTProvider", () => {
94
+ const consoleErrorSpy = vi
95
+ .spyOn(console, "error")
96
+ .mockImplementation(() => {});
97
+
98
+ expect(() => {
99
+ renderHook(() => useNFTContext());
100
+ }).toThrow(
101
+ "NFTProviderContext not found. Make sure you are using NFTMedia, NFTDescription, etc. inside a <NFTProvider /> component",
102
+ );
103
+
104
+ consoleErrorSpy.mockRestore();
105
+ });
114
106
  });