thirdweb 5.76.0-nightly-015293eb826b346871696183e96d49da44f31393-20241209000527 → 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.
- package/dist/cjs/exports/react.js.map +1 -1
- package/dist/cjs/react/web/ui/prebuilt/NFT/description.js +57 -9
- package/dist/cjs/react/web/ui/prebuilt/NFT/description.js.map +1 -1
- package/dist/cjs/react/web/ui/prebuilt/NFT/media.js +72 -13
- package/dist/cjs/react/web/ui/prebuilt/NFT/media.js.map +1 -1
- package/dist/cjs/react/web/ui/prebuilt/NFT/name.js +58 -9
- package/dist/cjs/react/web/ui/prebuilt/NFT/name.js.map +1 -1
- package/dist/cjs/react/web/ui/prebuilt/NFT/utils.js +34 -0
- package/dist/cjs/react/web/ui/prebuilt/NFT/utils.js.map +1 -0
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/wallets/coinbase/coinbase-web.js +1 -3
- package/dist/cjs/wallets/coinbase/coinbase-web.js.map +1 -1
- package/dist/cjs/wallets/in-app/core/actions/generate-wallet.enclave.js +1 -1
- package/dist/cjs/wallets/in-app/core/actions/generate-wallet.enclave.js.map +1 -1
- package/dist/cjs/wallets/in-app/core/actions/sign-message.enclave.js +1 -1
- package/dist/cjs/wallets/in-app/core/actions/sign-message.enclave.js.map +1 -1
- package/dist/cjs/wallets/in-app/core/actions/sign-transaction.enclave.js +1 -1
- package/dist/cjs/wallets/in-app/core/actions/sign-transaction.enclave.js.map +1 -1
- package/dist/cjs/wallets/in-app/core/actions/sign-typed-data.enclave.js +1 -1
- package/dist/cjs/wallets/in-app/core/actions/sign-typed-data.enclave.js.map +1 -1
- package/dist/cjs/wallets/injected/index.js +14 -20
- package/dist/cjs/wallets/injected/index.js.map +1 -1
- package/dist/cjs/wallets/smart/lib/userop.js +1 -1
- package/dist/cjs/wallets/smart/lib/userop.js.map +1 -1
- package/dist/esm/exports/react.js.map +1 -1
- package/dist/esm/react/web/ui/prebuilt/NFT/description.js +56 -9
- package/dist/esm/react/web/ui/prebuilt/NFT/description.js.map +1 -1
- package/dist/esm/react/web/ui/prebuilt/NFT/media.js +71 -13
- package/dist/esm/react/web/ui/prebuilt/NFT/media.js.map +1 -1
- package/dist/esm/react/web/ui/prebuilt/NFT/name.js +57 -9
- package/dist/esm/react/web/ui/prebuilt/NFT/name.js.map +1 -1
- package/dist/esm/react/web/ui/prebuilt/NFT/utils.js +32 -0
- package/dist/esm/react/web/ui/prebuilt/NFT/utils.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/wallets/coinbase/coinbase-web.js +1 -3
- package/dist/esm/wallets/coinbase/coinbase-web.js.map +1 -1
- package/dist/esm/wallets/in-app/core/actions/generate-wallet.enclave.js +1 -1
- package/dist/esm/wallets/in-app/core/actions/generate-wallet.enclave.js.map +1 -1
- package/dist/esm/wallets/in-app/core/actions/sign-message.enclave.js +1 -1
- package/dist/esm/wallets/in-app/core/actions/sign-message.enclave.js.map +1 -1
- package/dist/esm/wallets/in-app/core/actions/sign-transaction.enclave.js +1 -1
- package/dist/esm/wallets/in-app/core/actions/sign-transaction.enclave.js.map +1 -1
- package/dist/esm/wallets/in-app/core/actions/sign-typed-data.enclave.js +1 -1
- package/dist/esm/wallets/in-app/core/actions/sign-typed-data.enclave.js.map +1 -1
- package/dist/esm/wallets/injected/index.js +14 -20
- package/dist/esm/wallets/injected/index.js.map +1 -1
- package/dist/esm/wallets/smart/lib/userop.js +1 -1
- package/dist/esm/wallets/smart/lib/userop.js.map +1 -1
- package/dist/types/exports/react.d.ts +1 -1
- package/dist/types/exports/react.d.ts.map +1 -1
- package/dist/types/react/web/ui/prebuilt/NFT/description.d.ts +32 -4
- package/dist/types/react/web/ui/prebuilt/NFT/description.d.ts.map +1 -1
- package/dist/types/react/web/ui/prebuilt/NFT/media.d.ts +52 -4
- package/dist/types/react/web/ui/prebuilt/NFT/media.d.ts.map +1 -1
- package/dist/types/react/web/ui/prebuilt/NFT/name.d.ts +33 -4
- package/dist/types/react/web/ui/prebuilt/NFT/name.d.ts.map +1 -1
- package/dist/types/react/web/ui/prebuilt/NFT/utils.d.ts +7 -0
- package/dist/types/react/web/ui/prebuilt/NFT/utils.d.ts.map +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/wallets/coinbase/coinbase-web.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/actions/generate-wallet.enclave.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/actions/sign-message.enclave.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/actions/sign-transaction.enclave.d.ts.map +1 -1
- package/dist/types/wallets/in-app/core/actions/sign-typed-data.enclave.d.ts.map +1 -1
- package/dist/types/wallets/smart/lib/userop.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/exports/react.ts +1 -0
- package/src/react/web/ui/prebuilt/NFT/description.test.tsx +63 -0
- package/src/react/web/ui/prebuilt/NFT/description.tsx +70 -11
- package/src/react/web/ui/prebuilt/NFT/media.test.tsx +77 -0
- package/src/react/web/ui/prebuilt/NFT/media.tsx +106 -20
- package/src/react/web/ui/prebuilt/NFT/name.test.tsx +61 -0
- package/src/react/web/ui/prebuilt/NFT/name.tsx +71 -11
- package/src/react/web/ui/prebuilt/NFT/{NFT.test.tsx → provider.test.tsx} +34 -42
- package/src/react/web/ui/prebuilt/NFT/utils.test.ts +93 -0
- package/src/react/web/ui/prebuilt/NFT/utils.ts +41 -0
- package/src/utils/encoding/hex.test.ts +5 -0
- package/src/version.ts +1 -1
- package/src/wallets/coinbase/coinbase-web.ts +1 -5
- package/src/wallets/in-app/core/actions/generate-wallet.enclave.ts +3 -1
- package/src/wallets/in-app/core/actions/sign-message.enclave.ts +3 -1
- package/src/wallets/in-app/core/actions/sign-transaction.enclave.ts +3 -1
- package/src/wallets/in-app/core/actions/sign-typed-data.enclave.ts +3 -1
- package/src/wallets/injected/index.ts +14 -19
- package/src/wallets/smart/lib/userop.ts +3 -1
- package/src/wallets/smart/smart-wallet-dev.test.ts +18 -20
- package/dist/cjs/react/web/ui/prebuilt/NFT/hooks.js +0 -45
- package/dist/cjs/react/web/ui/prebuilt/NFT/hooks.js.map +0 -1
- package/dist/esm/react/web/ui/prebuilt/NFT/hooks.js +0 -41
- package/dist/esm/react/web/ui/prebuilt/NFT/hooks.js.map +0 -1
- package/dist/types/react/web/ui/prebuilt/NFT/hooks.d.ts +0 -14
- package/dist/types/react/web/ui/prebuilt/NFT/hooks.d.ts.map +0 -1
- package/src/react/web/ui/prebuilt/NFT/hooks.tsx +0 -53
@@ -1,10 +1,10 @@
|
|
1
1
|
"use client";
|
2
2
|
|
3
|
-
import type
|
3
|
+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
4
4
|
import type { JSX } from "react";
|
5
|
-
import type {
|
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<
|
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
|
73
|
-
|
74
|
-
|
75
|
-
|
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 (
|
112
|
+
if (descQuery.isLoading) {
|
79
113
|
return loadingComponent || null;
|
80
114
|
}
|
81
115
|
|
82
|
-
if (!
|
116
|
+
if (!descQuery.data) {
|
83
117
|
return fallbackComponent || null;
|
84
118
|
}
|
85
119
|
|
86
|
-
return <span {...restProps}>{
|
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
|
1
|
+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
2
2
|
import type { JSX } from "react";
|
3
|
-
import type {
|
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<
|
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
|
90
|
-
|
91
|
-
|
92
|
-
|
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 (
|
150
|
+
if (mediaQuery.isLoading) {
|
96
151
|
return loadingComponent || null;
|
97
152
|
}
|
98
153
|
|
99
|
-
if (!
|
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={
|
115
|
-
poster={
|
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
|
1
|
+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
2
2
|
import type { JSX } from "react";
|
3
|
-
import type {
|
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<
|
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
|
72
|
-
|
73
|
-
|
74
|
-
|
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 (
|
112
|
+
if (nameQuery.isLoading) {
|
78
113
|
return loadingComponent || null;
|
79
114
|
}
|
80
115
|
|
81
|
-
if (!
|
116
|
+
if (!nameQuery.data) {
|
82
117
|
return fallbackComponent || null;
|
83
118
|
}
|
84
|
-
return <span {...restProps}>{
|
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 {
|
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
|
-
<
|
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
|
});
|