@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/{chunk-FMEEJFAF.js → chunk-5C6ZZ6WX.js} +1 -1
  3. package/dist/{chunk-YEGD7PWE.js → chunk-5O44EPXZ.js} +2 -2
  4. package/dist/chunk-6CTFVBKU.js +1 -0
  5. package/dist/{chunk-KGM2WLSP.js → chunk-7F27CJZW.js} +14 -2
  6. package/dist/{chunk-KGM2WLSP.js.map → chunk-7F27CJZW.js.map} +1 -1
  7. package/dist/{chunk-MAD64DLJ.js → chunk-A7BVFBWB.js} +2 -2
  8. package/dist/{chunk-YBOFRP65.js → chunk-FGM57QUU.js} +2 -2
  9. package/dist/{chunk-HHYNOPPI.js → chunk-KTST7ORH.js} +2 -2
  10. package/dist/{chunk-35WWD5V6.js → chunk-M6NJ73Y5.js} +3 -3
  11. package/dist/chunk-Q3ECVC4F.js +811 -0
  12. package/dist/chunk-Q3ECVC4F.js.map +1 -0
  13. package/dist/{chunk-EODKQL6Y.js → chunk-RVIUUJTP.js} +2 -2
  14. package/dist/{chunk-G3447GIP.js → chunk-SXVUTSMT.js} +24 -9
  15. package/dist/chunk-SXVUTSMT.js.map +1 -0
  16. package/dist/{chunk-I2BYHDFE.js → chunk-UJSF7PSC.js} +159 -106
  17. package/dist/chunk-UJSF7PSC.js.map +1 -0
  18. package/dist/{chunk-YALXP2PW.js → chunk-WH5BZC7W.js} +2 -2
  19. package/dist/{chunk-4XLXOEXQ.js → chunk-Y2HJO2VY.js} +25 -4
  20. package/dist/chunk-Y2HJO2VY.js.map +1 -0
  21. package/dist/{create-config-DwrnzwpM.d.ts → create-config-CAQcvjl6.d.ts} +2 -2
  22. package/dist/{index-DGsVBflk.d.ts → index-MlUK9AQE.d.ts} +2 -2
  23. package/dist/index.css +7 -7
  24. package/dist/index.css.map +1 -1
  25. package/dist/index.d.ts +4 -3
  26. package/dist/index.js +10 -4
  27. package/dist/{lowestListing-BQHIuvNF.d.ts → listTokenMetadata-DO4ChDjn.d.ts} +20 -2
  28. package/dist/{marketplaceConfig-B4Fdsmxu.d.ts → marketplaceConfig-D0MXemEl.d.ts} +1 -1
  29. package/dist/react/_internal/api/index.d.ts +3 -2
  30. package/dist/react/_internal/api/index.js +5 -1
  31. package/dist/react/_internal/databeat/index.js +11 -10
  32. package/dist/react/_internal/index.d.ts +6 -5
  33. package/dist/react/_internal/index.js +5 -1
  34. package/dist/react/_internal/wagmi/index.d.ts +3 -3
  35. package/dist/react/hooks/index.d.ts +11 -8
  36. package/dist/react/hooks/index.js +12 -9
  37. package/dist/react/hooks/options/index.d.ts +3 -3
  38. package/dist/react/hooks/options/index.js +3 -3
  39. package/dist/react/index.css +7 -7
  40. package/dist/react/index.css.map +1 -1
  41. package/dist/react/index.d.ts +11 -10
  42. package/dist/react/index.js +18 -15
  43. package/dist/react/queries/index.d.ts +3 -2
  44. package/dist/react/queries/index.js +6 -4
  45. package/dist/react/ssr/index.d.ts +2 -2
  46. package/dist/react/ssr/index.js +2 -2
  47. package/dist/react/ui/components/collectible-card/index.css +7 -7
  48. package/dist/react/ui/components/collectible-card/index.css.map +1 -1
  49. package/dist/react/ui/components/collectible-card/index.d.ts +7 -5
  50. package/dist/react/ui/components/collectible-card/index.js +18 -17
  51. package/dist/react/ui/components/marketplace-logos/index.js +1 -1
  52. package/dist/react/ui/icons/index.js +7 -6
  53. package/dist/react/ui/index.css +7 -7
  54. package/dist/react/ui/index.css.map +1 -1
  55. package/dist/react/ui/index.d.ts +1 -1
  56. package/dist/react/ui/index.js +16 -15
  57. package/dist/react/ui/modals/_internal/components/actionModal/index.js +11 -10
  58. package/dist/{sdk-config-txlivEKe.d.ts → sdk-config-onSPBxJj.d.ts} +1 -0
  59. package/dist/{services-BI_w8Eq4.d.ts → services-CMSb9ipU.d.ts} +5 -2
  60. package/dist/types/index.d.ts +2 -2
  61. package/dist/types/index.js +1 -1
  62. package/dist/{types-isjvwapz.d.ts → types-B8xzPEKX.d.ts} +2 -2
  63. package/dist/utils/abi/index.d.ts +1 -0
  64. package/dist/utils/abi/index.js +7 -1
  65. package/dist/utils/abi/primary-sale/index.d.ts +1054 -0
  66. package/dist/utils/abi/primary-sale/index.js +9 -0
  67. package/dist/utils/abi/primary-sale/index.js.map +1 -0
  68. package/dist/utils/index.d.ts +1 -0
  69. package/dist/utils/index.js +10 -4
  70. package/package.json +30 -30
  71. package/src/react/_internal/api/services.ts +12 -1
  72. package/src/react/hooks/index.ts +1 -0
  73. package/src/react/hooks/useList1155SaleSupplies.tsx +62 -0
  74. package/src/react/hooks/useListTokenMetadata.ts +19 -0
  75. package/src/react/queries/index.ts +1 -0
  76. package/src/react/queries/listTokenMetadata.ts +38 -0
  77. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +2 -2
  78. package/src/react/ui/components/collectible-card/__tests__/{CollectibleAsset.test.tsx → Media.test.tsx} +7 -13
  79. package/src/react/ui/components/collectible-card/index.ts +1 -1
  80. package/src/react/ui/components/collectible-card/media/Media.tsx +206 -0
  81. package/src/react/ui/components/collectible-card/{collectible-asset/CollectibleAssetSkeleton.tsx → media/MediaSkeleton.tsx} +1 -1
  82. package/src/react/ui/components/collectible-card/media/types.ts +17 -0
  83. package/src/react/ui/components/collectible-card/{collectible-asset → media}/utils.ts +8 -3
  84. package/src/react/ui/index.ts +1 -1
  85. package/src/react/ui/modals/BuyModal/hooks/usePaymentModalParams.ts +28 -3
  86. package/src/react/ui/modals/TransferModal/_views/enterWalletAddress/_components/WalletAddressInput.tsx +1 -1
  87. package/src/types/sdk-config.ts +1 -0
  88. package/src/utils/abi/index.ts +1 -0
  89. package/src/utils/abi/primary-sale/index.ts +2 -0
  90. package/src/utils/abi/primary-sale/sequence-1155-sales-contract.ts +450 -0
  91. package/src/utils/abi/primary-sale/sequence-721-sales-contract.ts +352 -0
  92. package/src/utils/abi/token/sequence-erc1155-items.ts +454 -0
  93. package/src/utils/fetchContentType.ts +5 -1
  94. package/tsconfig.tsbuildinfo +1 -1
  95. package/dist/chunk-4XLXOEXQ.js.map +0 -1
  96. package/dist/chunk-G3447GIP.js.map +0 -1
  97. package/dist/chunk-I2BYHDFE.js.map +0 -1
  98. package/dist/chunk-UISBTKFF.js +0 -1
  99. package/src/react/ui/components/collectible-card/collectible-asset/CollectibleAsset.tsx +0 -174
  100. /package/dist/{chunk-FMEEJFAF.js.map → chunk-5C6ZZ6WX.js.map} +0 -0
  101. /package/dist/{chunk-YEGD7PWE.js.map → chunk-5O44EPXZ.js.map} +0 -0
  102. /package/dist/{chunk-UISBTKFF.js.map → chunk-6CTFVBKU.js.map} +0 -0
  103. /package/dist/{chunk-MAD64DLJ.js.map → chunk-A7BVFBWB.js.map} +0 -0
  104. /package/dist/{chunk-YBOFRP65.js.map → chunk-FGM57QUU.js.map} +0 -0
  105. /package/dist/{chunk-HHYNOPPI.js.map → chunk-KTST7ORH.js.map} +0 -0
  106. /package/dist/{chunk-35WWD5V6.js.map → chunk-M6NJ73Y5.js.map} +0 -0
  107. /package/dist/{chunk-EODKQL6Y.js.map → chunk-RVIUUJTP.js.map} +0 -0
  108. /package/dist/{chunk-YALXP2PW.js.map → chunk-WH5BZC7W.js.map} +0 -0
@@ -0,0 +1,9 @@
1
+ import {
2
+ ERC1155_SALES_CONTRACT_ABI,
3
+ ERC721_SALE_ABI
4
+ } from "../../../chunk-Q3ECVC4F.js";
5
+ export {
6
+ ERC1155_SALES_CONTRACT_ABI,
7
+ ERC721_SALE_ABI
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -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';
@@ -10,9 +10,13 @@ import {
10
10
  networkToWagmiChain,
11
11
  truncateEnd,
12
12
  truncateMiddle
13
- } from "../chunk-YEGD7PWE.js";
14
- import "../chunk-FMEEJFAF.js";
15
- import "../chunk-UISBTKFF.js";
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-KGM2WLSP.js";
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.9",
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.0.15",
45
- "@xstate/store": "^3.4.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.2",
53
- "zod": "^3.24.2"
52
+ "tailwind-merge": "^3.3.0",
53
+ "zod": "^3.24.4"
54
54
  },
55
55
  "peerDependencies": {
56
- "0xsequence": "^2.3.8",
57
- "@0xsequence/api": "^2.3.8",
58
- "@0xsequence/checkout": "^5.2.2",
59
- "@0xsequence/connect": "^5.2.2",
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.8",
62
- "@0xsequence/metadata": "^2.3.8",
63
- "@0xsequence/network": "^2.3.8",
64
- "@tanstack/react-query": "^5.74.3",
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.27.0",
68
- "wagmi": "^2.14.16",
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.0",
74
- "@eslint/js": "^9.23.0",
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.2.0",
77
- "@types/react": "^19.1.2",
78
- "@types/react-dom": "^19.1.2",
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.23.0",
82
+ "eslint": "^9.26.0",
83
83
  "eslint-config-biome": "^1.9.4",
84
- "eslint-plugin-react": "^7.37.4",
84
+ "eslint-plugin-react": "^7.37.5",
85
85
  "eslint-plugin-react-hooks": "^5.2.0",
86
- "globals": "^16.0.0",
87
- "happy-dom": "^17.4.4",
88
- "jsdom": "^26.0.0",
89
- "msw": "^2.7.3",
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.23",
92
- "tailwindcss": "^4.1.4",
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.27.0",
95
+ "typescript-eslint": "^8.32.1",
96
96
  "vite-tsconfig-paths": "^5.1.4",
97
- "vitest": "^3.0.9"
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://api.sequence.app',
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':
@@ -36,3 +36,4 @@ export * from './useListCollections';
36
36
  export * from './useCancelOrder';
37
37
  export * from './useCollectionDetails';
38
38
  export * from './useCollectionDetailsPolling';
39
+ export * from './useListTokenMetadata';
@@ -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
+ };
@@ -5,3 +5,4 @@ export * from './listCollectibles';
5
5
  export * from './highestOffer';
6
6
  export * from './listBalances';
7
7
  export * from './lowestListing';
8
+ export * from './listTokenMetadata';
@@ -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 { CollectibleAsset } from './collectible-asset/CollectibleAsset';
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
- <CollectibleAsset
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 { CollectibleAsset } from '../collectible-asset/CollectibleAsset';
6
- import * as contentTypeUtils from '../collectible-asset/utils';
5
+ import { Media } from '../media/Media';
6
+ import * as contentTypeUtils from '../media/utils';
7
7
 
8
- describe('CollectibleAsset', () => {
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
- <CollectibleAsset
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
- <CollectibleAsset
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
- <CollectibleAsset
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
- <CollectibleAsset
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 './collectible-asset/CollectibleAsset';
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
+ }
@@ -1,6 +1,6 @@
1
1
  import { Skeleton } from '@0xsequence/design-system';
2
2
 
3
- export default function CollectibleAssetSkeleton() {
3
+ export default function MediaSkeleton() {
4
4
  return (
5
5
  <Skeleton
6
6
  data-testid="collectible-asset-skeleton"
@@ -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
- resolve(type);
38
+
39
+ if (type) {
40
+ resolve(type);
41
+ } else {
42
+ reject(new Error('Unsupported file type'));
43
+ }
39
44
  });
40
45
  };
@@ -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 { CollectibleAsset } from './components/collectible-card/collectible-asset/CollectibleAsset';
11
+ export { Media } from './components/collectible-card/media/Media';