@0xsequence/marketplace-sdk 0.8.9 → 0.8.10
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/CHANGELOG.md +30 -0
- package/dist/{chunk-FMEEJFAF.js → chunk-5C6ZZ6WX.js} +1 -1
- package/dist/{chunk-YEGD7PWE.js → chunk-5O44EPXZ.js} +2 -2
- package/dist/chunk-6CTFVBKU.js +1 -0
- package/dist/{chunk-KGM2WLSP.js → chunk-7F27CJZW.js} +14 -2
- package/dist/{chunk-KGM2WLSP.js.map → chunk-7F27CJZW.js.map} +1 -1
- package/dist/{chunk-MAD64DLJ.js → chunk-A7BVFBWB.js} +2 -2
- package/dist/{chunk-YBOFRP65.js → chunk-FGM57QUU.js} +2 -2
- package/dist/{chunk-HHYNOPPI.js → chunk-KTST7ORH.js} +2 -2
- package/dist/{chunk-35WWD5V6.js → chunk-M6NJ73Y5.js} +3 -3
- package/dist/chunk-Q3ECVC4F.js +811 -0
- package/dist/chunk-Q3ECVC4F.js.map +1 -0
- package/dist/{chunk-EODKQL6Y.js → chunk-RVIUUJTP.js} +2 -2
- package/dist/{chunk-G3447GIP.js → chunk-SXVUTSMT.js} +24 -9
- package/dist/chunk-SXVUTSMT.js.map +1 -0
- package/dist/{chunk-I2BYHDFE.js → chunk-UJSF7PSC.js} +159 -106
- package/dist/chunk-UJSF7PSC.js.map +1 -0
- package/dist/{chunk-YALXP2PW.js → chunk-WH5BZC7W.js} +2 -2
- package/dist/{chunk-4XLXOEXQ.js → chunk-Y2HJO2VY.js} +25 -4
- package/dist/chunk-Y2HJO2VY.js.map +1 -0
- package/dist/{create-config-DwrnzwpM.d.ts → create-config-CAQcvjl6.d.ts} +2 -2
- package/dist/{index-DGsVBflk.d.ts → index-MlUK9AQE.d.ts} +2 -2
- package/dist/index.css +7 -7
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +10 -4
- package/dist/{lowestListing-BQHIuvNF.d.ts → listTokenMetadata-DO4ChDjn.d.ts} +20 -2
- package/dist/{marketplaceConfig-B4Fdsmxu.d.ts → marketplaceConfig-D0MXemEl.d.ts} +1 -1
- package/dist/react/_internal/api/index.d.ts +3 -2
- package/dist/react/_internal/api/index.js +5 -1
- package/dist/react/_internal/databeat/index.js +11 -10
- package/dist/react/_internal/index.d.ts +6 -5
- package/dist/react/_internal/index.js +5 -1
- package/dist/react/_internal/wagmi/index.d.ts +3 -3
- package/dist/react/hooks/index.d.ts +11 -8
- package/dist/react/hooks/index.js +12 -9
- package/dist/react/hooks/options/index.d.ts +3 -3
- package/dist/react/hooks/options/index.js +3 -3
- package/dist/react/index.css +7 -7
- package/dist/react/index.css.map +1 -1
- package/dist/react/index.d.ts +11 -10
- package/dist/react/index.js +18 -15
- package/dist/react/queries/index.d.ts +3 -2
- package/dist/react/queries/index.js +6 -4
- package/dist/react/ssr/index.d.ts +2 -2
- package/dist/react/ssr/index.js +2 -2
- package/dist/react/ui/components/collectible-card/index.css +7 -7
- package/dist/react/ui/components/collectible-card/index.css.map +1 -1
- package/dist/react/ui/components/collectible-card/index.d.ts +7 -5
- package/dist/react/ui/components/collectible-card/index.js +18 -17
- package/dist/react/ui/components/marketplace-logos/index.js +1 -1
- package/dist/react/ui/icons/index.js +7 -6
- package/dist/react/ui/index.css +7 -7
- package/dist/react/ui/index.css.map +1 -1
- package/dist/react/ui/index.d.ts +1 -1
- package/dist/react/ui/index.js +16 -15
- package/dist/react/ui/modals/_internal/components/actionModal/index.js +11 -10
- package/dist/{sdk-config-txlivEKe.d.ts → sdk-config-onSPBxJj.d.ts} +1 -0
- package/dist/{services-BI_w8Eq4.d.ts → services-CMSb9ipU.d.ts} +5 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +1 -1
- package/dist/{types-isjvwapz.d.ts → types-B8xzPEKX.d.ts} +2 -2
- package/dist/utils/abi/index.d.ts +1 -0
- package/dist/utils/abi/index.js +7 -1
- package/dist/utils/abi/primary-sale/index.d.ts +1054 -0
- package/dist/utils/abi/primary-sale/index.js +9 -0
- package/dist/utils/abi/primary-sale/index.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +10 -4
- package/package.json +30 -30
- package/src/react/_internal/api/services.ts +12 -1
- package/src/react/hooks/index.ts +1 -0
- package/src/react/hooks/useList1155SaleSupplies.tsx +62 -0
- package/src/react/hooks/useListTokenMetadata.ts +19 -0
- package/src/react/queries/index.ts +1 -0
- package/src/react/queries/listTokenMetadata.ts +38 -0
- package/src/react/ui/components/collectible-card/CollectibleCard.tsx +2 -2
- package/src/react/ui/components/collectible-card/__tests__/{CollectibleAsset.test.tsx → Media.test.tsx} +7 -13
- package/src/react/ui/components/collectible-card/index.ts +1 -1
- package/src/react/ui/components/collectible-card/media/Media.tsx +206 -0
- package/src/react/ui/components/collectible-card/{collectible-asset/CollectibleAssetSkeleton.tsx → media/MediaSkeleton.tsx} +1 -1
- package/src/react/ui/components/collectible-card/media/types.ts +17 -0
- package/src/react/ui/components/collectible-card/{collectible-asset → media}/utils.ts +8 -3
- package/src/react/ui/index.ts +1 -1
- package/src/react/ui/modals/BuyModal/hooks/usePaymentModalParams.ts +28 -3
- package/src/react/ui/modals/TransferModal/_views/enterWalletAddress/_components/WalletAddressInput.tsx +1 -1
- package/src/types/sdk-config.ts +1 -0
- package/src/utils/abi/index.ts +1 -0
- package/src/utils/abi/primary-sale/index.ts +2 -0
- package/src/utils/abi/primary-sale/sequence-1155-sales-contract.ts +450 -0
- package/src/utils/abi/primary-sale/sequence-721-sales-contract.ts +352 -0
- package/src/utils/abi/token/sequence-erc1155-items.ts +454 -0
- package/src/utils/fetchContentType.ts +5 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/chunk-4XLXOEXQ.js.map +0 -1
- package/dist/chunk-G3447GIP.js.map +0 -1
- package/dist/chunk-I2BYHDFE.js.map +0 -1
- package/dist/chunk-UISBTKFF.js +0 -1
- package/src/react/ui/components/collectible-card/collectible-asset/CollectibleAsset.tsx +0 -174
- /package/dist/{chunk-FMEEJFAF.js.map → chunk-5C6ZZ6WX.js.map} +0 -0
- /package/dist/{chunk-YEGD7PWE.js.map → chunk-5O44EPXZ.js.map} +0 -0
- /package/dist/{chunk-UISBTKFF.js.map → chunk-6CTFVBKU.js.map} +0 -0
- /package/dist/{chunk-MAD64DLJ.js.map → chunk-A7BVFBWB.js.map} +0 -0
- /package/dist/{chunk-YBOFRP65.js.map → chunk-FGM57QUU.js.map} +0 -0
- /package/dist/{chunk-HHYNOPPI.js.map → chunk-KTST7ORH.js.map} +0 -0
- /package/dist/{chunk-35WWD5V6.js.map → chunk-M6NJ73Y5.js.map} +0 -0
- /package/dist/{chunk-EODKQL6Y.js.map → chunk-RVIUUJTP.js.map} +0 -0
- /package/dist/{chunk-YALXP2PW.js.map → chunk-WH5BZC7W.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { EIP2981_ABI, SequenceMarketplaceV1_ABI, SequenceMarketplaceV2_ABI } from './abi/marketplace/index.js';
|
|
2
2
|
export { ERC1155_ABI, ERC20_ABI, ERC721_ABI } from './abi/token/index.js';
|
|
3
|
+
export { ERC1155_SALES_CONTRACT_ABI, ERC721_SALE_ABI } from './abi/primary-sale/index.js';
|
|
3
4
|
import { Image } from '@0xsequence/design-system';
|
|
4
5
|
import { ComponentType } from 'react';
|
|
5
6
|
import { M as MarketplaceKind } from '../marketplace.gen-DQzWciwC.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -10,9 +10,13 @@ import {
|
|
|
10
10
|
networkToWagmiChain,
|
|
11
11
|
truncateEnd,
|
|
12
12
|
truncateMiddle
|
|
13
|
-
} from "../chunk-
|
|
14
|
-
import "../chunk-
|
|
15
|
-
import "../chunk-
|
|
13
|
+
} from "../chunk-5O44EPXZ.js";
|
|
14
|
+
import "../chunk-5C6ZZ6WX.js";
|
|
15
|
+
import "../chunk-6CTFVBKU.js";
|
|
16
|
+
import {
|
|
17
|
+
ERC1155_SALES_CONTRACT_ABI,
|
|
18
|
+
ERC721_SALE_ABI
|
|
19
|
+
} from "../chunk-Q3ECVC4F.js";
|
|
16
20
|
import {
|
|
17
21
|
ERC1155_ABI,
|
|
18
22
|
ERC20_ABI,
|
|
@@ -25,14 +29,16 @@ import {
|
|
|
25
29
|
} from "../chunk-XX4EVWBF.js";
|
|
26
30
|
import "../chunk-2PSNAIAT.js";
|
|
27
31
|
import "../chunk-DWTLVJAW.js";
|
|
28
|
-
import "../chunk-
|
|
32
|
+
import "../chunk-7F27CJZW.js";
|
|
29
33
|
import "../chunk-D7RVSZAQ.js";
|
|
30
34
|
import "../chunk-NX52D7NX.js";
|
|
31
35
|
export {
|
|
32
36
|
EIP2981_ABI,
|
|
33
37
|
ERC1155_ABI,
|
|
38
|
+
ERC1155_SALES_CONTRACT_ABI,
|
|
34
39
|
ERC20_ABI,
|
|
35
40
|
ERC721_ABI,
|
|
41
|
+
ERC721_SALE_ABI,
|
|
36
42
|
SequenceMarketplaceV1_ABI,
|
|
37
43
|
SequenceMarketplaceV2_ABI,
|
|
38
44
|
calculateEarningsAfterFees,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xsequence/marketplace-sdk",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
7
7
|
"**/*.css"
|
|
@@ -41,60 +41,60 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@databeat/tracker": "^0.9.3",
|
|
43
43
|
"@legendapp/state": "^3.0.0-beta.30",
|
|
44
|
-
"@tailwindcss/postcss": "^4.
|
|
45
|
-
"@xstate/store": "^3.
|
|
44
|
+
"@tailwindcss/postcss": "^4.1.6",
|
|
45
|
+
"@xstate/store": "^3.6.0",
|
|
46
46
|
"class-variance-authority": "^0.7.1",
|
|
47
47
|
"clsx": "^2.1.1",
|
|
48
48
|
"date-fns": "^4.1.0",
|
|
49
49
|
"dnum": "^2.14.0",
|
|
50
50
|
"nuqs": "^2.4.3",
|
|
51
51
|
"react-day-picker": "^9.6.7",
|
|
52
|
-
"tailwind-merge": "^3.0
|
|
53
|
-
"zod": "^3.24.
|
|
52
|
+
"tailwind-merge": "^3.3.0",
|
|
53
|
+
"zod": "^3.24.4"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"0xsequence": "^2.3.
|
|
57
|
-
"@0xsequence/api": "^2.3.
|
|
58
|
-
"@0xsequence/checkout": "^5.
|
|
59
|
-
"@0xsequence/connect": "^5.
|
|
56
|
+
"0xsequence": "^2.3.12",
|
|
57
|
+
"@0xsequence/api": "^2.3.12",
|
|
58
|
+
"@0xsequence/checkout": "^5.3.1",
|
|
59
|
+
"@0xsequence/connect": "^5.3.1",
|
|
60
60
|
"@0xsequence/design-system": "^2.1.12",
|
|
61
|
-
"@0xsequence/indexer": "^2.3.
|
|
62
|
-
"@0xsequence/metadata": "^2.3.
|
|
63
|
-
"@0xsequence/network": "^2.3.
|
|
64
|
-
"@tanstack/react-query": "^5.
|
|
61
|
+
"@0xsequence/indexer": "^2.3.12",
|
|
62
|
+
"@0xsequence/metadata": "^2.3.12",
|
|
63
|
+
"@0xsequence/network": "^2.3.12",
|
|
64
|
+
"@tanstack/react-query": "^5.76.1",
|
|
65
65
|
"react": "^19.1.0",
|
|
66
66
|
"react-dom": "^19.1.0",
|
|
67
|
-
"viem": "^2.
|
|
68
|
-
"wagmi": "^2.
|
|
67
|
+
"viem": "^2.29.2",
|
|
68
|
+
"wagmi": "^2.15.3",
|
|
69
69
|
"@google/model-viewer": "^4.1.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@biomejs/biome": "^1.9.4",
|
|
73
|
-
"@changesets/cli": "^2.29.
|
|
74
|
-
"@eslint/js": "^9.
|
|
73
|
+
"@changesets/cli": "^2.29.4",
|
|
74
|
+
"@eslint/js": "^9.26.0",
|
|
75
75
|
"@testing-library/jest-dom": "6.4.7",
|
|
76
|
-
"@testing-library/react": "^16.
|
|
77
|
-
"@types/react": "^19.1.
|
|
78
|
-
"@types/react-dom": "^19.1.
|
|
76
|
+
"@testing-library/react": "^16.3.0",
|
|
77
|
+
"@types/react": "^19.1.4",
|
|
78
|
+
"@types/react-dom": "^19.1.5",
|
|
79
79
|
"@vitest/coverage-v8": "3.0.9",
|
|
80
80
|
"ctix": "^2.7.1",
|
|
81
81
|
"esbuild-plugin-preserve-directives": "^0.0.11",
|
|
82
|
-
"eslint": "^9.
|
|
82
|
+
"eslint": "^9.26.0",
|
|
83
83
|
"eslint-config-biome": "^1.9.4",
|
|
84
|
-
"eslint-plugin-react": "^7.37.
|
|
84
|
+
"eslint-plugin-react": "^7.37.5",
|
|
85
85
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
86
|
-
"globals": "^16.
|
|
87
|
-
"happy-dom": "^17.4.
|
|
88
|
-
"jsdom": "^26.
|
|
89
|
-
"msw": "^2.
|
|
86
|
+
"globals": "^16.1.0",
|
|
87
|
+
"happy-dom": "^17.4.7",
|
|
88
|
+
"jsdom": "^26.1.0",
|
|
89
|
+
"msw": "^2.8.2",
|
|
90
90
|
"postcss": "^8.5.3",
|
|
91
|
-
"prool": "^0.0.
|
|
92
|
-
"tailwindcss": "^4.1.
|
|
91
|
+
"prool": "^0.0.24",
|
|
92
|
+
"tailwindcss": "^4.1.6",
|
|
93
93
|
"tsup": "^8.4.0",
|
|
94
94
|
"typescript": "^5.8.3",
|
|
95
|
-
"typescript-eslint": "^8.
|
|
95
|
+
"typescript-eslint": "^8.32.1",
|
|
96
96
|
"vite-tsconfig-paths": "^5.1.4",
|
|
97
|
-
"vitest": "^3.
|
|
97
|
+
"vitest": "^3.1.3"
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"build": "tsc -b && tsup",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SequenceAPIClient } from '@0xsequence/api';
|
|
1
2
|
import { SequenceIndexer } from '@0xsequence/indexer';
|
|
2
3
|
import { SequenceMetadata } from '@0xsequence/metadata';
|
|
3
4
|
import { networks, stringTemplate } from '@0xsequence/network';
|
|
@@ -6,7 +7,7 @@ import { MissingConfigError } from '../../../utils/_internal/error/transaction';
|
|
|
6
7
|
import { SequenceMarketplace } from './marketplace-api';
|
|
7
8
|
|
|
8
9
|
const SERVICES = {
|
|
9
|
-
sequenceApi: 'https
|
|
10
|
+
sequenceApi: 'https://${prefix}api.sequence.app',
|
|
10
11
|
metadata: 'https://${prefix}metadata.sequence.app',
|
|
11
12
|
indexer: 'https://${prefix}${network}-indexer.sequence.app',
|
|
12
13
|
marketplaceApi: 'https://${prefix}marketplace-api.sequence.app/${network}',
|
|
@@ -51,6 +52,11 @@ export const builderRpcApi = (env: Env = 'production') => {
|
|
|
51
52
|
return stringTemplate(SERVICES.builderRpcApi, { prefix });
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
export const sequenceApiUrl = (env: Env = 'production') => {
|
|
56
|
+
const prefix = getPrefix(env);
|
|
57
|
+
return stringTemplate(SERVICES.sequenceApi, { prefix });
|
|
58
|
+
};
|
|
59
|
+
|
|
54
60
|
export const builderMarketplaceApi = (
|
|
55
61
|
projectId: string,
|
|
56
62
|
env: Env = 'production',
|
|
@@ -82,6 +88,11 @@ export const getMarketplaceClient = (
|
|
|
82
88
|
projectAccessKey,
|
|
83
89
|
);
|
|
84
90
|
};
|
|
91
|
+
export const getSequenceApiClient = (config: SdkConfig) => {
|
|
92
|
+
const env = config._internal?.sequenceApiEnv || 'production';
|
|
93
|
+
const projectAccessKey = getAccessKey({ env, config });
|
|
94
|
+
return new SequenceAPIClient(sequenceApiUrl(env), projectAccessKey);
|
|
95
|
+
};
|
|
85
96
|
const getAccessKey = ({ env, config }: { env: Env; config: SdkConfig }) => {
|
|
86
97
|
switch (env) {
|
|
87
98
|
case 'development':
|
package/src/react/hooks/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getUnixTime } from 'date-fns';
|
|
2
|
+
import type { Address } from 'viem';
|
|
3
|
+
import { useReadContracts } from 'wagmi';
|
|
4
|
+
import { ERC1155_SALES_CONTRACT_ABI } from '../..';
|
|
5
|
+
|
|
6
|
+
interface UseSaleSupplyDataProps {
|
|
7
|
+
tokenIds: string[];
|
|
8
|
+
salesContractAddress: Address;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useList1155SaleSupplies({
|
|
12
|
+
tokenIds,
|
|
13
|
+
salesContractAddress,
|
|
14
|
+
}: UseSaleSupplyDataProps) {
|
|
15
|
+
const getReadContractsArgs = (tokenIds: string[]) =>
|
|
16
|
+
tokenIds.map((tokenId) => ({
|
|
17
|
+
address: salesContractAddress,
|
|
18
|
+
abi: ERC1155_SALES_CONTRACT_ABI,
|
|
19
|
+
functionName: 'tokenSaleDetails',
|
|
20
|
+
args: [tokenId],
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
data: supplyData,
|
|
25
|
+
isLoading: supplyDataLoading,
|
|
26
|
+
error: supplyDataError,
|
|
27
|
+
} = useReadContracts({
|
|
28
|
+
batchSize: 500_000, // Node gateway limit has a limit of 512kB, setting it to 500kB to be safe
|
|
29
|
+
contracts: getReadContractsArgs(tokenIds),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const extendedSupplyData = (supplyData || [])
|
|
33
|
+
.map((data, index) => ({
|
|
34
|
+
...data,
|
|
35
|
+
tokenId: tokenIds[index],
|
|
36
|
+
}))
|
|
37
|
+
.filter((data) => data.status === 'success')
|
|
38
|
+
.filter((data) => {
|
|
39
|
+
if (typeof data.result !== 'object') return false;
|
|
40
|
+
const now = BigInt(getUnixTime(new Date()));
|
|
41
|
+
return data.result.endTime > now && data.result.startTime < now;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const getSupply = (tokenId: string): number | undefined => {
|
|
45
|
+
const found = extendedSupplyData.find((data) => data.tokenId === tokenId);
|
|
46
|
+
if (!found || typeof found.result !== 'object' || found.result === null)
|
|
47
|
+
return undefined;
|
|
48
|
+
const supply = found.result.supplyCap;
|
|
49
|
+
if (supply === undefined) return undefined;
|
|
50
|
+
// https://github.com/0xsequence/contracts-library/blob/ead1baf34270c76260d01cfc130bb7cc9d57518e/src/tokens/ERC1155/utility/sale/IERC1155Sale.sol#L8
|
|
51
|
+
// 0 means infinite
|
|
52
|
+
if (supply === 0n) return Number.POSITIVE_INFINITY;
|
|
53
|
+
return Number(supply);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
extendedSupplyData,
|
|
58
|
+
getSupply,
|
|
59
|
+
supplyDataLoading,
|
|
60
|
+
supplyDataError,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useConfig } from './useConfig';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type FetchTokenMetadataArgs,
|
|
6
|
+
tokenMetadataOptions,
|
|
7
|
+
} from '../queries/listTokenMetadata';
|
|
8
|
+
|
|
9
|
+
export const useListTokenMetadata = (args: FetchTokenMetadataArgs) => {
|
|
10
|
+
const config = useConfig();
|
|
11
|
+
const { chainId, contractAddress, tokenIds, query } = args;
|
|
12
|
+
|
|
13
|
+
return useQuery({
|
|
14
|
+
...tokenMetadataOptions(
|
|
15
|
+
{ chainId, contractAddress, tokenIds, query },
|
|
16
|
+
config,
|
|
17
|
+
),
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { queryOptions } from '@tanstack/react-query';
|
|
2
|
+
import type { SdkConfig } from '../../types';
|
|
3
|
+
import { getMetadataClient } from '../_internal';
|
|
4
|
+
|
|
5
|
+
export interface FetchTokenMetadataArgs {
|
|
6
|
+
chainId: number;
|
|
7
|
+
contractAddress: string;
|
|
8
|
+
tokenIds: string[];
|
|
9
|
+
query?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const fetchTokenMetadata = async (
|
|
15
|
+
{ chainId, contractAddress, tokenIds }: FetchTokenMetadataArgs,
|
|
16
|
+
config: SdkConfig,
|
|
17
|
+
) => {
|
|
18
|
+
const metadataClient = getMetadataClient(config);
|
|
19
|
+
|
|
20
|
+
const response = await metadataClient.getTokenMetadata({
|
|
21
|
+
chainID: chainId.toString(),
|
|
22
|
+
contractAddress: contractAddress,
|
|
23
|
+
tokenIDs: tokenIds,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return response.tokenMetadata;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const tokenMetadataOptions = (
|
|
30
|
+
args: FetchTokenMetadataArgs,
|
|
31
|
+
config: SdkConfig,
|
|
32
|
+
) => {
|
|
33
|
+
return queryOptions({
|
|
34
|
+
...args.query,
|
|
35
|
+
queryKey: ['listTokenMetadata', args.chainId, args.contractAddress],
|
|
36
|
+
queryFn: () => fetchTokenMetadata(args, config),
|
|
37
|
+
});
|
|
38
|
+
};
|
|
@@ -12,7 +12,7 @@ import { useCurrency } from '../../../hooks';
|
|
|
12
12
|
import { ActionButton } from '../_internals/action-button/ActionButton';
|
|
13
13
|
import { CollectibleCardAction } from '../_internals/action-button/types';
|
|
14
14
|
import { Footer } from './Footer';
|
|
15
|
-
import {
|
|
15
|
+
import { Media } from './media/Media';
|
|
16
16
|
|
|
17
17
|
function CollectibleSkeleton() {
|
|
18
18
|
return (
|
|
@@ -127,7 +127,7 @@ export function CollectibleCard({
|
|
|
127
127
|
>
|
|
128
128
|
<div className="group relative z-10 flex h-full w-full cursor-pointer flex-col items-start overflow-hidden rounded-xl border-none bg-none p-0 focus:outline-none [&:focus]:rounded-[10px] [&:focus]:outline-[3px] [&:focus]:outline-black [&:focus]:outline-offset-[-3px]">
|
|
129
129
|
<article className="w-full rounded-xl">
|
|
130
|
-
<
|
|
130
|
+
<Media
|
|
131
131
|
name={collectibleMetadata?.name || ''}
|
|
132
132
|
assets={[
|
|
133
133
|
collectibleMetadata?.image,
|
|
@@ -2,10 +2,10 @@ import { render, screen, waitFor } from '@test/test-utils';
|
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import * as fetchContentTypeModule from '../../../../../utils/fetchContentType';
|
|
4
4
|
import type { TokenMetadata } from '../../../../_internal';
|
|
5
|
-
import {
|
|
6
|
-
import * as contentTypeUtils from '../
|
|
5
|
+
import { Media } from '../media/Media';
|
|
6
|
+
import * as contentTypeUtils from '../media/utils';
|
|
7
7
|
|
|
8
|
-
describe('
|
|
8
|
+
describe('Media', () => {
|
|
9
9
|
it('renders image content correctly with proper loading states and fallback', async () => {
|
|
10
10
|
const originalImage = window.Image;
|
|
11
11
|
|
|
@@ -30,10 +30,7 @@ describe('CollectibleAsset', () => {
|
|
|
30
30
|
|
|
31
31
|
// Initial render should show the loading skeleton
|
|
32
32
|
const { rerender } = render(
|
|
33
|
-
<
|
|
34
|
-
name="Test Collectible"
|
|
35
|
-
assets={[mockMetadata.image]}
|
|
36
|
-
/>,
|
|
33
|
+
<Media name="Test Collectible" assets={[mockMetadata.image]} />,
|
|
37
34
|
);
|
|
38
35
|
|
|
39
36
|
// check if skeleton is rendered during loading
|
|
@@ -69,7 +66,7 @@ describe('CollectibleAsset', () => {
|
|
|
69
66
|
};
|
|
70
67
|
|
|
71
68
|
rerender(
|
|
72
|
-
<
|
|
69
|
+
<Media
|
|
73
70
|
name="Test Collectible"
|
|
74
71
|
assets={[mockMetadataWithBadImage.image]}
|
|
75
72
|
/>,
|
|
@@ -127,10 +124,7 @@ describe('CollectibleAsset', () => {
|
|
|
127
124
|
};
|
|
128
125
|
|
|
129
126
|
render(
|
|
130
|
-
<
|
|
131
|
-
name="Video Collectible"
|
|
132
|
-
assets={[mockVideoMetadata.video]}
|
|
133
|
-
/>,
|
|
127
|
+
<Media name="Video Collectible" assets={[mockVideoMetadata.video]} />,
|
|
134
128
|
);
|
|
135
129
|
|
|
136
130
|
await waitFor(() => {
|
|
@@ -192,7 +186,7 @@ describe('CollectibleAsset', () => {
|
|
|
192
186
|
};
|
|
193
187
|
|
|
194
188
|
render(
|
|
195
|
-
<
|
|
189
|
+
<Media
|
|
196
190
|
name="HTML Collectible"
|
|
197
191
|
assets={[mockHtmlMetadata.animation_url]}
|
|
198
192
|
/>,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './CollectibleCard';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './media/Media';
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { cn } from '../../../../../utils';
|
|
5
|
+
import { fetchContentType } from '../../../../../utils/fetchContentType';
|
|
6
|
+
import ChessTileImage from '../../../images/chess-tile.png';
|
|
7
|
+
import ModelViewer from '../../ModelViewer';
|
|
8
|
+
import MediaSkeleton from './MediaSkeleton';
|
|
9
|
+
import type { ContentTypeState, MediaProps } from './types';
|
|
10
|
+
import { getContentType } from './utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @description This component is used to display a collectible asset.
|
|
14
|
+
* It will display the first valid asset from the assets array.
|
|
15
|
+
* If no valid asset is found, it will display the placeholder image.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <Media
|
|
19
|
+
* name="Collectible"
|
|
20
|
+
* assets={[undefined, "some-image-url", undefined]} // undefined assets will be ignored, "some-image-url" will be rendered
|
|
21
|
+
* assetSrcPrefixUrl="https://example.com/"
|
|
22
|
+
* className="w-full h-full"
|
|
23
|
+
* />
|
|
24
|
+
*/
|
|
25
|
+
export function Media({
|
|
26
|
+
name,
|
|
27
|
+
assets,
|
|
28
|
+
assetSrcPrefixUrl,
|
|
29
|
+
className,
|
|
30
|
+
supply,
|
|
31
|
+
}: MediaProps) {
|
|
32
|
+
const [assetLoadFailed, setAssetLoadFailed] = useState(false);
|
|
33
|
+
const [assetLoading, setAssetLoading] = useState(true);
|
|
34
|
+
const [contentType, setContentType] = useState<ContentTypeState>({
|
|
35
|
+
type: null,
|
|
36
|
+
loading: true,
|
|
37
|
+
failed: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
41
|
+
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
42
|
+
|
|
43
|
+
const placeholderImage = ChessTileImage;
|
|
44
|
+
const assetUrl = assets.find((asset): asset is string => !!asset);
|
|
45
|
+
const proxiedAssetUrl = assetUrl
|
|
46
|
+
? assetSrcPrefixUrl
|
|
47
|
+
? `${assetSrcPrefixUrl}${assetUrl}`
|
|
48
|
+
: assetUrl
|
|
49
|
+
: '';
|
|
50
|
+
|
|
51
|
+
const classNames = cn(
|
|
52
|
+
'relative aspect-square overflow-hidden bg-background-secondary',
|
|
53
|
+
supply !== undefined && supply === 0 && 'opacity-50',
|
|
54
|
+
className,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!assetUrl) {
|
|
59
|
+
setContentType({ type: null, loading: false, failed: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const determineContentType = async () => {
|
|
64
|
+
try {
|
|
65
|
+
const type = await getContentType(proxiedAssetUrl);
|
|
66
|
+
setContentType({ type, loading: false, failed: false });
|
|
67
|
+
} catch {
|
|
68
|
+
try {
|
|
69
|
+
const type = await fetchContentType(proxiedAssetUrl);
|
|
70
|
+
setContentType({ type, loading: false, failed: false });
|
|
71
|
+
} catch {
|
|
72
|
+
setContentType({ type: null, loading: false, failed: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
determineContentType();
|
|
78
|
+
}, [proxiedAssetUrl, assetUrl]);
|
|
79
|
+
|
|
80
|
+
const handleAssetError = () => {
|
|
81
|
+
setAssetLoadFailed(true);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleAssetLoad = () => {
|
|
85
|
+
setAssetLoading(false);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Display placeholder if asset fails to load or doesn't exist
|
|
89
|
+
if ((contentType.failed && !assetLoadFailed) || !assetUrl) {
|
|
90
|
+
return (
|
|
91
|
+
<div className={cn('h-full w-full', classNames)}>
|
|
92
|
+
<img
|
|
93
|
+
src={placeholderImage}
|
|
94
|
+
alt={name || 'Collectible'}
|
|
95
|
+
className="h-full w-full object-cover"
|
|
96
|
+
onError={(e) => {
|
|
97
|
+
console.error('Failed to load placeholder image');
|
|
98
|
+
e.currentTarget.style.display = 'none';
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Render based on content type
|
|
106
|
+
if (contentType.type === 'html' && !assetLoadFailed) {
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
'flex w-full items-center justify-center rounded-lg',
|
|
111
|
+
classNames,
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{(assetLoading || contentType.loading) && <MediaSkeleton />}
|
|
115
|
+
|
|
116
|
+
<iframe
|
|
117
|
+
title={name || 'Collectible'}
|
|
118
|
+
className="aspect-square w-full"
|
|
119
|
+
src={proxiedAssetUrl}
|
|
120
|
+
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
|
121
|
+
sandbox="allow-scripts"
|
|
122
|
+
style={{ border: '0px' }}
|
|
123
|
+
onError={handleAssetError}
|
|
124
|
+
onLoad={handleAssetLoad}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (contentType.type === '3d-model' && !assetLoadFailed) {
|
|
131
|
+
return (
|
|
132
|
+
<div className={cn('h-full w-full', classNames)}>
|
|
133
|
+
<ModelViewer
|
|
134
|
+
src={proxiedAssetUrl}
|
|
135
|
+
posterSrc={placeholderImage}
|
|
136
|
+
onLoad={handleAssetLoad}
|
|
137
|
+
onError={handleAssetError}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (contentType.type === 'video' && !assetLoadFailed) {
|
|
144
|
+
const videoClassNames = cn(
|
|
145
|
+
'absolute inset-0 h-full w-full object-cover transition-transform duration-200 ease-in-out group-hover:scale-hover',
|
|
146
|
+
assetLoading ? 'invisible' : 'visible',
|
|
147
|
+
// we can't hide the video controls in safari, when user hovers over the video they show up.
|
|
148
|
+
// `pointer-events-none` is the only way to hide them on hover
|
|
149
|
+
isSafari && 'pointer-events-none',
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className={classNames}>
|
|
154
|
+
{(assetLoading || contentType.loading) && <MediaSkeleton />}
|
|
155
|
+
|
|
156
|
+
<video
|
|
157
|
+
ref={videoRef}
|
|
158
|
+
className={videoClassNames}
|
|
159
|
+
autoPlay
|
|
160
|
+
loop
|
|
161
|
+
controls
|
|
162
|
+
playsInline
|
|
163
|
+
muted
|
|
164
|
+
controlsList="nodownload noremoteplayback nofullscreen"
|
|
165
|
+
onError={handleAssetError}
|
|
166
|
+
onLoadedMetadata={handleAssetLoad}
|
|
167
|
+
data-testid="collectible-asset-video"
|
|
168
|
+
>
|
|
169
|
+
<source src={proxiedAssetUrl} />
|
|
170
|
+
</video>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Default to image renderer
|
|
176
|
+
const imgSrc =
|
|
177
|
+
assetLoadFailed || contentType.failed ? placeholderImage : proxiedAssetUrl;
|
|
178
|
+
|
|
179
|
+
const imgClassNames = cn(
|
|
180
|
+
'absolute inset-0 h-full w-full object-cover transition-transform duration-200 ease-in-out group-hover:scale-hover',
|
|
181
|
+
assetLoading || contentType.loading ? 'invisible' : 'visible',
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className={classNames}>
|
|
186
|
+
{(assetLoading || contentType.loading) && <MediaSkeleton />}
|
|
187
|
+
|
|
188
|
+
<img
|
|
189
|
+
src={imgSrc}
|
|
190
|
+
alt={name || 'Collectible'}
|
|
191
|
+
className={imgClassNames}
|
|
192
|
+
onError={(e) => {
|
|
193
|
+
if (contentType.type === 'image') {
|
|
194
|
+
setAssetLoadFailed(true);
|
|
195
|
+
}
|
|
196
|
+
// If this is the placeholder image that failed
|
|
197
|
+
if (e.currentTarget.src === placeholderImage) {
|
|
198
|
+
console.error('Failed to load placeholder image');
|
|
199
|
+
e.currentTarget.style.display = 'none';
|
|
200
|
+
}
|
|
201
|
+
}}
|
|
202
|
+
onLoad={handleAssetLoad}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type ContentType = 'image' | 'video' | 'html' | '3d-model' | null;
|
|
2
|
+
|
|
3
|
+
type ContentTypeState = {
|
|
4
|
+
type: ContentType;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
failed: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type MediaProps = {
|
|
10
|
+
name?: string;
|
|
11
|
+
assets: (string | undefined)[];
|
|
12
|
+
assetSrcPrefixUrl?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
supply?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type { ContentType, ContentTypeState, MediaProps };
|
|
@@ -23,9 +23,9 @@ export const is3dModel = (fileName: string | undefined) => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export const getContentType = (
|
|
26
|
-
url: string,
|
|
26
|
+
url: string | undefined,
|
|
27
27
|
): Promise<'image' | 'video' | 'html' | '3d-model' | null> => {
|
|
28
|
-
return new Promise((resolve) => {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
29
|
const type = isHtml(url)
|
|
30
30
|
? 'html'
|
|
31
31
|
: isVideo(url)
|
|
@@ -35,6 +35,11 @@ export const getContentType = (
|
|
|
35
35
|
: is3dModel(url)
|
|
36
36
|
? '3d-model'
|
|
37
37
|
: null;
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
if (type) {
|
|
40
|
+
resolve(type);
|
|
41
|
+
} else {
|
|
42
|
+
reject(new Error('Unsupported file type'));
|
|
43
|
+
}
|
|
39
44
|
});
|
|
40
45
|
};
|
package/src/react/ui/index.ts
CHANGED
|
@@ -8,4 +8,4 @@ export { useBuyModal } from './modals/BuyModal';
|
|
|
8
8
|
|
|
9
9
|
// components
|
|
10
10
|
export { CollectibleCard } from './components/collectible-card/CollectibleCard';
|
|
11
|
-
export {
|
|
11
|
+
export { Media } from './components/collectible-card/media/Media';
|