@0xsequence/marketplace-sdk 0.8.7 → 0.8.8

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 (130) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/chunk-2PSNAIAT.js +1 -0
  3. package/dist/chunk-2PSNAIAT.js.map +1 -0
  4. package/dist/{chunk-7FVZD2LL.js → chunk-4XLXOEXQ.js} +2 -2
  5. package/dist/{chunk-Y6AOCO3Q.js → chunk-7IYKUVC3.js} +312 -145
  6. package/dist/chunk-7IYKUVC3.js.map +1 -0
  7. package/dist/{chunk-5HEZNTSU.js → chunk-D7RVSZAQ.js} +131 -83
  8. package/dist/chunk-D7RVSZAQ.js.map +1 -0
  9. package/dist/chunk-DWTLVJAW.js +42 -0
  10. package/dist/chunk-DWTLVJAW.js.map +1 -0
  11. package/dist/{chunk-YAUZLETY.js → chunk-G3447GIP.js} +37 -18
  12. package/dist/chunk-G3447GIP.js.map +1 -0
  13. package/dist/{chunk-O6GWM7C3.js → chunk-HHYNOPPI.js} +2 -2
  14. package/dist/{chunk-KTT27YUN.js → chunk-KGM2WLSP.js} +12 -156
  15. package/dist/chunk-KGM2WLSP.js.map +1 -0
  16. package/dist/chunk-MAD64DLJ.js +81 -0
  17. package/dist/chunk-MAD64DLJ.js.map +1 -0
  18. package/dist/chunk-N7BPFK46.js +1 -0
  19. package/dist/chunk-N7BPFK46.js.map +1 -0
  20. package/dist/chunk-NX52D7NX.js +135 -0
  21. package/dist/chunk-NX52D7NX.js.map +1 -0
  22. package/dist/chunk-O34GCB47.js +32 -0
  23. package/dist/chunk-O34GCB47.js.map +1 -0
  24. package/dist/{chunk-Q2DA477S.js → chunk-YALXP2PW.js} +3 -3
  25. package/dist/{chunk-A6V7XDY4.js → chunk-YBOFRP65.js} +2 -2
  26. package/dist/{create-config-CdooE7aU.d.ts → create-config-DwrnzwpM.d.ts} +2 -2
  27. package/dist/{index-ClKHzm0B.d.ts → index-DGsVBflk.d.ts} +3 -12
  28. package/dist/index.css +7 -5
  29. package/dist/index.css.map +1 -1
  30. package/dist/index.d.ts +13 -4
  31. package/dist/index.js +13 -5
  32. package/dist/{lowestListing-kFyrUGha.d.ts → lowestListing-BQHIuvNF.d.ts} +2 -2
  33. package/dist/{marketplace.gen-BU6T6f0m.d.ts → marketplace.gen-DQzWciwC.d.ts} +1 -1
  34. package/dist/marketplaceConfig-B4Fdsmxu.d.ts +17 -0
  35. package/dist/react/_internal/api/index.d.ts +3 -3
  36. package/dist/react/_internal/api/index.js +4 -3
  37. package/dist/react/_internal/databeat/index.d.ts +2 -1
  38. package/dist/react/_internal/databeat/index.js +11 -7
  39. package/dist/react/_internal/index.d.ts +6 -6
  40. package/dist/react/_internal/index.js +13 -10
  41. package/dist/react/_internal/wagmi/index.d.ts +6 -5
  42. package/dist/react/_internal/wagmi/index.js +2 -2
  43. package/dist/react/hooks/index.d.ts +9 -8
  44. package/dist/react/hooks/index.js +10 -6
  45. package/dist/react/hooks/options/index.d.ts +4 -4
  46. package/dist/react/hooks/options/index.js +7 -4
  47. package/dist/react/index.css +2650 -0
  48. package/dist/react/index.css.map +1 -1
  49. package/dist/react/index.d.ts +8 -8
  50. package/dist/react/index.js +16 -11
  51. package/dist/react/queries/index.d.ts +3 -3
  52. package/dist/react/queries/index.js +6 -3
  53. package/dist/react/ssr/index.d.ts +3 -3
  54. package/dist/react/ssr/index.js +6 -3
  55. package/dist/react/ssr/index.js.map +1 -1
  56. package/dist/react/ui/components/collectible-card/index.css +2650 -0
  57. package/dist/react/ui/components/collectible-card/index.css.map +1 -1
  58. package/dist/react/ui/components/collectible-card/index.d.ts +1 -1
  59. package/dist/react/ui/components/collectible-card/index.js +14 -9
  60. package/dist/react/ui/icons/index.js +5 -2
  61. package/dist/react/ui/index.css +2650 -0
  62. package/dist/react/ui/index.css.map +1 -1
  63. package/dist/react/ui/index.d.ts +2 -1
  64. package/dist/react/ui/index.js +14 -9
  65. package/dist/react/ui/modals/_internal/components/actionModal/index.js +11 -7
  66. package/dist/sdk-config-txlivEKe.d.ts +133 -0
  67. package/dist/{services-9ApY0U-o.d.ts → services-BI_w8Eq4.d.ts} +4 -4
  68. package/dist/types/index.d.ts +5 -4
  69. package/dist/types/index.js +9 -6
  70. package/dist/{types-DsTwmKG-.d.ts → types-isjvwapz.d.ts} +3 -3
  71. package/dist/utils/index.d.ts +1 -1
  72. package/dist/utils/index.js +5 -2
  73. package/package.json +3 -3
  74. package/src/index.ts +1 -0
  75. package/src/react/{hooks/options/__mocks__/marketplaceConfig.msw.ts → _internal/api/__mocks__/builder.msw.ts} +70 -63
  76. package/src/react/_internal/api/__mocks__/metadata.msw.ts +30 -10
  77. package/src/react/_internal/api/builder-api.ts +32 -0
  78. package/src/react/_internal/api/builder.gen.ts +215 -0
  79. package/src/react/_internal/api/services.ts +9 -2
  80. package/src/react/_internal/databeat/types.ts +1 -0
  81. package/src/react/_internal/wagmi/__tests__/create-config.test.ts +16 -41
  82. package/src/react/_internal/wagmi/create-config.ts +3 -16
  83. package/src/react/_internal/wagmi/get-connectors.ts +31 -21
  84. package/src/react/_internal/wallet/__tests__/wallet.test.ts +30 -0
  85. package/src/react/_internal/wallet/wallet.ts +25 -2
  86. package/src/react/hooks/__tests__/__snapshots__/useMarketplaceConfig.test.tsx.snap +96 -0
  87. package/src/react/hooks/__tests__/useCollection.test.tsx +4 -5
  88. package/src/react/hooks/__tests__/useCurrencies.test.tsx +1 -1
  89. package/src/react/hooks/__tests__/useInventory.test.tsx +15 -16
  90. package/src/react/hooks/__tests__/useListCollections.test.tsx +18 -163
  91. package/src/react/hooks/__tests__/useMarketplaceConfig.test.tsx +10 -11
  92. package/src/react/hooks/options/index.ts +1 -1
  93. package/src/react/hooks/useAutoSelectFeeOption.tsx +1 -0
  94. package/src/react/hooks/useMarketplaceConfig.tsx +2 -2
  95. package/src/react/queries/marketplaceConfig.ts +101 -0
  96. package/src/react/ssr/__tests__/__snapshots__/create-ssr-client.test.ts.snap +1 -0
  97. package/src/react/ssr/create-ssr-client.ts +1 -1
  98. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +2 -2
  99. package/src/react/ui/components/collectible-card/__tests__/CollectibleAsset.test.tsx +72 -42
  100. package/src/react/ui/components/collectible-card/{CollectibleAsset.tsx → collectible-asset/CollectibleAsset.tsx} +44 -44
  101. package/src/react/ui/components/collectible-card/collectible-asset/CollectibleAssetSkeleton.tsx +14 -0
  102. package/src/react/ui/components/collectible-card/collectible-asset/utils.ts +36 -0
  103. package/src/react/ui/modals/BuyModal/hooks/usePaymentModalParams.ts +6 -1
  104. package/src/react/ui/modals/BuyModal/store.ts +1 -0
  105. package/src/react/ui/modals/CreateListingModal/hooks/useTransactionSteps.tsx +20 -2
  106. package/src/react/ui/modals/MakeOfferModal/hooks/useTransactionSteps.tsx +17 -1
  107. package/src/react/ui/modals/SellModal/hooks/useTransactionSteps.tsx +4 -0
  108. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.test.tsx +7 -1
  109. package/src/types/index.ts +7 -1
  110. package/src/types/sdk-config.ts +4 -9
  111. package/src/utils/fetchContentType.ts +101 -0
  112. package/src/utils/getSequenceMarketRequestId.ts +32 -0
  113. package/test/mocks/wallet.ts +3 -1
  114. package/test/test-utils.tsx +1 -1
  115. package/tsconfig.tsbuildinfo +1 -1
  116. package/dist/builder-types-D5HgAUWR.d.ts +0 -82
  117. package/dist/chunk-5HEZNTSU.js.map +0 -1
  118. package/dist/chunk-KTT27YUN.js.map +0 -1
  119. package/dist/chunk-N7E37ENQ.js +0 -58
  120. package/dist/chunk-N7E37ENQ.js.map +0 -1
  121. package/dist/chunk-Y6AOCO3Q.js.map +0 -1
  122. package/dist/chunk-YAUZLETY.js.map +0 -1
  123. package/dist/sdk-config-DIzJk_tI.d.ts +0 -26
  124. package/src/react/hooks/options/__tests__/marketplaceConfigOptions.test.tsx +0 -134
  125. package/src/react/hooks/options/marketplaceConfigOptions.ts +0 -71
  126. package/src/types/builder-types.ts +0 -88
  127. /package/dist/{chunk-7FVZD2LL.js.map → chunk-4XLXOEXQ.js.map} +0 -0
  128. /package/dist/{chunk-O6GWM7C3.js.map → chunk-HHYNOPPI.js.map} +0 -0
  129. /package/dist/{chunk-Q2DA477S.js.map → chunk-YALXP2PW.js.map} +0 -0
  130. /package/dist/{chunk-A6V7XDY4.js.map → chunk-YBOFRP65.js.map} +0 -0
@@ -1,12 +1,12 @@
1
1
  import { renderHook, server, waitFor } from '@test';
2
2
  import { describe, expect, it } from 'vitest';
3
+ import { createLookupMarketplaceConfigErrorHandler } from '../../_internal/api/__mocks__/builder.msw';
4
+
3
5
  import {
4
- createConfigHandler,
5
- createErrorHandler,
6
6
  createStylesErrorHandler,
7
7
  mockConfig,
8
8
  mockStyles,
9
- } from '../options/__mocks__/marketplaceConfig.msw';
9
+ } from '../../_internal/api/__mocks__/builder.msw';
10
10
  import { useMarketplaceConfig } from '../useMarketplaceConfig';
11
11
 
12
12
  describe('useMarketplaceConfig', () => {
@@ -24,17 +24,13 @@ describe('useMarketplaceConfig', () => {
24
24
 
25
25
  // Verify the data matches our mock
26
26
  expect(result.current.data).toBeDefined();
27
- expect(result.current.data).toEqual({
28
- ...mockConfig,
29
- cssString: mockStyles.replaceAll(/['"]/g, ''),
30
- manifestUrl: expect.stringContaining('/manifest.json'),
31
- });
27
+ expect(result.current.data).toMatchSnapshot();
32
28
  expect(result.current.error).toBeNull();
33
29
  });
34
30
 
35
31
  it('should handle config fetch error', async () => {
36
32
  // Override the handler for this test to return an error
37
- server.use(createErrorHandler());
33
+ server.use(createLookupMarketplaceConfigErrorHandler());
38
34
 
39
35
  const { result } = renderHook(() => useMarketplaceConfig());
40
36
 
@@ -48,7 +44,7 @@ describe('useMarketplaceConfig', () => {
48
44
 
49
45
  it('should handle styles fetch error', async () => {
50
46
  // Override the handler for this test to return an error
51
- server.use(createStylesErrorHandler(), createConfigHandler());
47
+ server.use(createStylesErrorHandler());
52
48
 
53
49
  const { result } = renderHook(() => useMarketplaceConfig());
54
50
 
@@ -63,7 +59,10 @@ describe('useMarketplaceConfig', () => {
63
59
 
64
60
  it('should handle both config and styles fetch errors', async () => {
65
61
  // Override both handlers to return errors
66
- server.use(createErrorHandler(), createStylesErrorHandler());
62
+ server.use(
63
+ createLookupMarketplaceConfigErrorHandler(),
64
+ createStylesErrorHandler(),
65
+ );
67
66
 
68
67
  const { result } = renderHook(() => useMarketplaceConfig());
69
68
 
@@ -1,2 +1,2 @@
1
1
  export { fetchCollection, collectionOptions } from './collectionOptions';
2
- export * from './marketplaceConfigOptions';
2
+ export * from '../../queries/marketplaceConfig';
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { type Address, zeroAddress } from 'viem';
2
3
 
3
4
  import { useChain } from '@0xsequence/connect';
@@ -1,8 +1,8 @@
1
1
  import { useQuery } from '@tanstack/react-query';
2
- import { marketplaceConfigOptions } from './options/marketplaceConfigOptions';
2
+ import { marketplaceConfigOptions } from '../queries/marketplaceConfig';
3
3
  import { useConfig } from './useConfig';
4
4
 
5
- export * from './options/marketplaceConfigOptions';
5
+ export * from '../queries/marketplaceConfig';
6
6
 
7
7
  export const useMarketplaceConfig = () => {
8
8
  const config = useConfig();
@@ -0,0 +1,101 @@
1
+ import { queryOptions } from '@tanstack/react-query';
2
+ import type { Env, SdkConfig } from '../../types';
3
+ import { builderMarketplaceApi, builderRpcApi, configKeys } from '../_internal';
4
+ import { BuilderAPI } from '../_internal/api/builder-api';
5
+ import type { MarketplaceSettings } from '../_internal/api/builder.gen';
6
+
7
+ export type MarketplaceConfig = MarketplaceSettings & {
8
+ cssString: string;
9
+ manifestUrl: string;
10
+ };
11
+
12
+ const fetchBuilderConfig = async ({
13
+ projectId,
14
+ projectAccessKey,
15
+ env,
16
+ prefetchedMarketplaceSettings,
17
+ }: {
18
+ projectId: string;
19
+ projectAccessKey: string;
20
+ env: Env;
21
+ prefetchedMarketplaceSettings?: MarketplaceSettings;
22
+ }) => {
23
+ if (prefetchedMarketplaceSettings) {
24
+ return prefetchedMarketplaceSettings;
25
+ }
26
+ const baseUrl = builderRpcApi(env);
27
+ const builderApi = new BuilderAPI(baseUrl, projectAccessKey);
28
+ const response = await builderApi.lookupMarketplaceConfig({
29
+ projectId: Number(projectId),
30
+ });
31
+ return response.settings;
32
+ };
33
+
34
+ const fetchStyles = async (projectId: string, env: Env) => {
35
+ const response = await fetch(
36
+ `${builderMarketplaceApi(projectId, env)}/styles.css`,
37
+ );
38
+ const styles = await response.text();
39
+ // React sanitizes this string, so we need to remove all quotes, they are not needed anyway
40
+ return styles.replaceAll(/['"]/g, '');
41
+ };
42
+
43
+ const fetchMarketplaceConfig = async ({
44
+ env,
45
+ projectId,
46
+ projectAccessKey,
47
+ prefetchedMarketplaceSettings,
48
+ }: {
49
+ env: Env;
50
+ projectId: string;
51
+ projectAccessKey: string;
52
+ prefetchedMarketplaceSettings?: MarketplaceSettings;
53
+ }): Promise<MarketplaceConfig> => {
54
+ const [marketplaceConfig, cssString] = await Promise.all([
55
+ fetchBuilderConfig({
56
+ projectId,
57
+ projectAccessKey,
58
+ env,
59
+ prefetchedMarketplaceSettings,
60
+ }),
61
+ fetchStyles(projectId, env),
62
+ ]);
63
+
64
+ return {
65
+ ...marketplaceConfig,
66
+ cssString,
67
+ manifestUrl: `${builderMarketplaceApi(projectId, env)}/manifest.json`,
68
+ };
69
+ };
70
+
71
+ export const marketplaceConfigOptions = (
72
+ config: Pick<SdkConfig, 'projectId' | 'projectAccessKey'> | SdkConfig,
73
+ ) => {
74
+ let env: Env = 'production';
75
+ if ('_internal' in config && config._internal !== undefined) {
76
+ env = config._internal.builderEnv ?? env;
77
+ }
78
+
79
+ let prefetchedMarketplaceSettings: MarketplaceSettings | undefined;
80
+ if (
81
+ '_internal' in config &&
82
+ config._internal?.prefetchedMarketplaceSettings
83
+ ) {
84
+ prefetchedMarketplaceSettings =
85
+ config._internal.prefetchedMarketplaceSettings;
86
+ }
87
+
88
+ const projectId = config.projectId;
89
+ const projectAccessKey = config.projectAccessKey;
90
+
91
+ return queryOptions({
92
+ queryKey: [...configKeys.marketplace, env, projectId],
93
+ queryFn: () =>
94
+ fetchMarketplaceConfig({
95
+ env,
96
+ projectId,
97
+ projectAccessKey,
98
+ prefetchedMarketplaceSettings,
99
+ }),
100
+ });
101
+ };
@@ -71,6 +71,7 @@ exports[`createSSRClient > should fetch marketplace config successfully 1`] = `
71
71
  "landingPageLayout": "default",
72
72
  "logoUrl": "https://example.com/logo.png",
73
73
  "manifestUrl": "https://api.sequence.build/marketplace/test-project/manifest.json",
74
+ "projectId": 1,
74
75
  "publisherId": "test-publisher",
75
76
  "shortDescription": "A test marketplace",
76
77
  "socials": {
@@ -2,7 +2,7 @@ import type { QueryClient } from '@tanstack/react-query';
2
2
  import { type State, cookieToInitialState } from 'wagmi';
3
3
  import type { SdkConfig } from '../../types/sdk-config';
4
4
  import { createWagmiConfig } from '../_internal/wagmi/create-config';
5
- import { marketplaceConfigOptions } from '../hooks/options/marketplaceConfigOptions';
5
+ import { marketplaceConfigOptions } from '../queries/marketplaceConfig';
6
6
 
7
7
  type InitSSRClientArgs = {
8
8
  cookie: string;
@@ -11,8 +11,8 @@ import type {
11
11
  import { useCurrency } from '../../../hooks';
12
12
  import { ActionButton } from '../_internals/action-button/ActionButton';
13
13
  import { CollectibleCardAction } from '../_internals/action-button/types';
14
- import { CollectibleAsset } from './CollectibleAsset';
15
14
  import { Footer } from './Footer';
15
+ import { CollectibleAsset } from './collectible-asset/CollectibleAsset';
16
16
 
17
17
  function CollectibleSkeleton() {
18
18
  return (
@@ -145,7 +145,7 @@ export function CollectibleCard({
145
145
  />
146
146
 
147
147
  {(highestOffer || collectible) && !balanceIsLoading && (
148
- <div className="-bottom-action-offset absolute flex w-full items-center justify-center bg-overlay-light p-2 backdrop-blur transition-transform duration-200 ease-in-out group-hover:translate-y-[-44px]">
148
+ <div className="-bottom-16 absolute flex w-full origin-bottom items-center justify-center bg-overlay-light p-2 backdrop-blur transition-transform duration-200 ease-in-out group-hover:translate-y-[-64px]">
149
149
  <ActionButton
150
150
  chainId={chainId}
151
151
  collectionAddress={collectionAddress}
@@ -1,7 +1,9 @@
1
- import { render, screen } from '@test/test-utils';
1
+ import { render, screen, waitFor } from '@test/test-utils';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
+ import * as fetchContentTypeModule from '../../../../../utils/fetchContentType';
3
4
  import type { TokenMetadata } from '../../../../_internal';
4
- import { CollectibleAsset } from '../CollectibleAsset';
5
+ import { CollectibleAsset } from '../collectible-asset/CollectibleAsset';
6
+ import * as contentTypeUtils from '../collectible-asset/utils';
5
7
 
6
8
  describe('CollectibleAsset', () => {
7
9
  it('renders image content correctly with proper loading states and fallback', async () => {
@@ -86,7 +88,16 @@ describe('CollectibleAsset', () => {
86
88
  window.Image = originalImage;
87
89
  });
88
90
 
89
- it('handles video content with appropriate controls and loading states', () => {
91
+ it('handles video content with appropriate controls and loading states', async () => {
92
+ const getContentTypeSpy = vi.spyOn(contentTypeUtils, 'getContentType');
93
+ getContentTypeSpy.mockResolvedValue('video');
94
+
95
+ const fetchContentTypeSpy = vi.spyOn(
96
+ fetchContentTypeModule,
97
+ 'fetchContentType',
98
+ );
99
+ fetchContentTypeSpy.mockResolvedValue('video');
100
+
90
101
  // Create a mock for the HTMLVideoElement addEventListener
91
102
  const originalAddEventListener =
92
103
  HTMLVideoElement.prototype.addEventListener;
@@ -122,34 +133,39 @@ describe('CollectibleAsset', () => {
122
133
  />,
123
134
  );
124
135
 
125
- // Check that video element is present with correct attributes
126
- const videoElement = document.querySelector('video');
136
+ await waitFor(() => {
137
+ expect(screen.getByTestId('collectible-asset-video')).toBeInTheDocument();
138
+ });
139
+
140
+ const videoElement = screen.getByTestId(
141
+ 'collectible-asset-video',
142
+ ) as HTMLVideoElement;
143
+
127
144
  expect(videoElement).not.toBeNull();
128
145
 
129
- if (videoElement) {
130
- // Video source should be set correctly
131
- const sourceElement = videoElement.querySelector('source');
132
- expect(sourceElement).not.toBeNull();
133
- expect(sourceElement?.getAttribute('src')).toBe(
134
- 'https://example.com/video.mp4',
135
- );
146
+ // Video source should be set correctly
147
+ const sourceElement = videoElement.querySelector('source');
148
+ expect(sourceElement).not.toBeNull();
149
+ expect(sourceElement?.getAttribute('src')).toBe(
150
+ 'https://example.com/video.mp4',
151
+ );
136
152
 
137
- // Video should have correct attributes for NFT display
138
- expect(videoElement.autoplay).toBe(true);
139
- expect(videoElement.loop).toBe(true);
140
- expect(videoElement.controls).toBe(true);
141
- expect(videoElement.playsInline).toBe(true);
142
- expect(videoElement.muted).toBe(true);
153
+ // Video should have correct attributes for NFT display
154
+ expect(videoElement.autoplay).toBe(true);
155
+ expect(videoElement.loop).toBe(true);
156
+ expect(videoElement.controls).toBe(true);
157
+ expect(videoElement.playsInline).toBe(true);
158
+ expect(videoElement.muted).toBe(true);
143
159
 
144
- // In Safari, pointer-events-none should be applied
145
- expect(videoElement.className).toContain('pointer-events-none');
160
+ // In Safari, pointer-events-none should be applied
161
+ expect(videoElement.className).toContain('pointer-events-none');
146
162
 
147
- // After metadata loaded, video should be visible
148
- expect(videoElement.className).toContain('visible');
149
- expect(videoElement.className).not.toContain('invisible');
150
- }
163
+ // After metadata loaded, video should be visible
164
+ expect(videoElement.className).toContain('visible');
151
165
 
152
166
  // Clean up mocks
167
+ getContentTypeSpy.mockRestore();
168
+ fetchContentTypeSpy.mockRestore();
153
169
  HTMLVideoElement.prototype.addEventListener = originalAddEventListener;
154
170
  Object.defineProperty(navigator, 'userAgent', {
155
171
  value: originalUserAgent,
@@ -157,7 +173,16 @@ describe('CollectibleAsset', () => {
157
173
  });
158
174
  });
159
175
 
160
- it('handles HTML content in iframes with proper sandboxing', () => {
176
+ it('handles HTML content in iframes with proper sandboxing', async () => {
177
+ const getContentTypeSpy = vi.spyOn(contentTypeUtils, 'getContentType');
178
+ getContentTypeSpy.mockResolvedValue('html');
179
+
180
+ const fetchContentTypeSpy = vi.spyOn(
181
+ fetchContentTypeModule,
182
+ 'fetchContentType',
183
+ );
184
+ fetchContentTypeSpy.mockResolvedValue('html');
185
+
161
186
  // Mock HTML content metadata
162
187
  const mockHtmlMetadata: Partial<TokenMetadata> = {
163
188
  tokenId: '1',
@@ -173,28 +198,33 @@ describe('CollectibleAsset', () => {
173
198
  />,
174
199
  );
175
200
 
201
+ await waitFor(() => {
202
+ expect(document.querySelector('iframe')).toBeInTheDocument();
203
+ });
204
+
176
205
  // Check that iframe element is present with correct attributes
177
- const iframeElement = document.querySelector('iframe');
178
- expect(iframeElement).not.toBeNull();
206
+ const iframeElement = document.querySelector('iframe') as HTMLIFrameElement;
207
+ expect(iframeElement).toBeInTheDocument();
179
208
 
180
- if (iframeElement) {
181
- // iframe source should be set correctly
182
- expect(iframeElement.getAttribute('src')).toBe(
183
- 'https://example.com/interactive.html',
184
- );
209
+ // iframe source should be set correctly
210
+ expect(iframeElement.getAttribute('src')).toBe(
211
+ 'https://example.com/interactive.html',
212
+ );
185
213
 
186
- // iframe should have appropriate attributes for security
187
- expect(iframeElement.getAttribute('sandbox')).toBe('allow-scripts');
214
+ // iframe should have appropriate attributes for security
215
+ expect(iframeElement.getAttribute('sandbox')).toBe('allow-scripts');
188
216
 
189
- // iframe should have title for accessibility
190
- expect(iframeElement.getAttribute('title')).toBe('HTML Collectible');
217
+ // iframe should have title for accessibility
218
+ expect(iframeElement.getAttribute('title')).toBe('HTML Collectible');
191
219
 
192
- // iframe should have proper styling
193
- expect(iframeElement.className).toContain('aspect-square');
194
- expect(iframeElement.className).toContain('w-full');
220
+ // iframe should have proper styling
221
+ expect(iframeElement.className).toContain('aspect-square');
222
+ expect(iframeElement.className).toContain('w-full');
195
223
 
196
- // Verify border styling
197
- expect(iframeElement.style.border).toBe('0px');
198
- }
224
+ // Verify border styling
225
+ expect(iframeElement.style.border).toBe('0px');
226
+
227
+ getContentTypeSpy.mockRestore();
228
+ fetchContentTypeSpy.mockRestore();
199
229
  });
200
230
  });
@@ -1,38 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { Skeleton } from '@0xsequence/design-system';
4
3
  import { useEffect, useRef, useState } from 'react';
5
- import { cn } from '../../../../utils';
6
- import type { TokenMetadata } from '../../../_internal';
7
- import ChessTileImage from '../../images/chess-tile.png';
8
-
9
- export function CollectibleAssetSkeleton() {
10
- return (
11
- <Skeleton
12
- data-testid="collectible-asset-skeleton"
13
- size="lg"
14
- className="absolute inset-0 h-full w-full animate-shimmer"
15
- style={{
16
- borderRadius: 0,
17
- }}
18
- />
19
- );
20
- }
21
-
22
- export const isHtml = (fileName: string | undefined) => {
23
- const isHtml = /.*\.(html\?.+|html)$/.test(fileName?.toLowerCase() || '');
24
- return isHtml;
25
- };
26
-
27
- export const isVideo = (fileName: string | undefined) => {
28
- const isVideo = /.*\.(mp4|ogg|webm)$/.test(fileName?.toLowerCase() || '');
29
- return isVideo;
30
- };
31
-
32
- export const is3dModel = (fileName: string | undefined) => {
33
- const isGltf = /.*\.gltf$/.test(fileName?.toLowerCase() || '');
34
- return isGltf;
35
- };
4
+ import { cn } from '../../../../../utils';
5
+ import { fetchContentType } from '../../../../../utils/fetchContentType';
6
+ import type { TokenMetadata } from '../../../../_internal';
7
+ import ChessTileImage from '../../../images/chess-tile.png';
8
+ import CollectibleAssetSkeleton from './CollectibleAssetSkeleton';
9
+ import { getContentType } from './utils';
36
10
 
37
11
  type CollectibleImageProps = {
38
12
  name?: string;
@@ -47,6 +21,11 @@ export function CollectibleAsset({
47
21
  }: CollectibleImageProps) {
48
22
  const [assetLoadFailed, setAssetLoadFailed] = useState(false);
49
23
  const [assetLoading, setAssetLoading] = useState(true);
24
+ const [contentType, setContentType] = useState<{
25
+ type: 'image' | 'video' | 'html' | null;
26
+ loading: boolean;
27
+ failed: boolean;
28
+ }>({ type: null, loading: true, failed: false });
50
29
  const videoRef = useRef<HTMLVideoElement>(null);
51
30
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
52
31
 
@@ -62,17 +41,29 @@ export function CollectibleAsset({
62
41
  : assetUrl;
63
42
 
64
43
  useEffect(() => {
65
- if (videoRef.current) {
66
- videoRef.current.addEventListener('loadedmetadata', () => {
67
- setAssetLoading(false);
44
+ getContentType(proxiedAssetUrl)
45
+ .then((contentType) => {
46
+ setContentType({ type: contentType, loading: false, failed: false });
47
+ })
48
+ .catch(() => {
49
+ fetchContentType(proxiedAssetUrl)
50
+ .then((contentType) => {
51
+ setContentType({
52
+ type: contentType,
53
+ loading: false,
54
+ failed: false,
55
+ });
56
+ })
57
+ .catch(() => {
58
+ setContentType({ type: null, loading: false, failed: true });
59
+ });
68
60
  });
69
- }
70
- }, []);
61
+ }, [proxiedAssetUrl]);
71
62
 
72
- if (isHtml(assetUrl)) {
63
+ if (contentType.type === 'html' && !assetLoadFailed) {
73
64
  return (
74
65
  <div className="flex aspect-square w-full items-center justify-center overflow-hidden rounded-lg bg-background-secondary">
75
- {assetLoading && <CollectibleAssetSkeleton />}
66
+ {(assetLoading || contentType.loading) && <CollectibleAssetSkeleton />}
76
67
 
77
68
  <iframe
78
69
  title={name || 'Collectible'}
@@ -92,10 +83,11 @@ export function CollectibleAsset({
92
83
 
93
84
  // TODO: Add 3d model support
94
85
 
95
- if (isVideo(assetUrl)) {
86
+ if (contentType.type === 'video' && !assetLoadFailed) {
96
87
  return (
97
88
  <div className="relative flex aspect-square w-full items-center justify-center overflow-hidden rounded-lg bg-background-secondary">
98
- {assetLoading && <CollectibleAssetSkeleton />}
89
+ {(assetLoading || contentType.loading) && <CollectibleAssetSkeleton />}
90
+
99
91
  <video
100
92
  ref={videoRef}
101
93
  className={cn(
@@ -114,6 +106,10 @@ export function CollectibleAsset({
114
106
  onError={() => {
115
107
  setAssetLoadFailed(true);
116
108
  }}
109
+ onLoadedMetadata={() => {
110
+ setAssetLoading(false);
111
+ }}
112
+ data-testid="collectible-asset-video"
117
113
  >
118
114
  <source src={proxiedAssetUrl} />
119
115
  </video>
@@ -123,13 +119,17 @@ export function CollectibleAsset({
123
119
 
124
120
  return (
125
121
  <div className="relative aspect-square overflow-hidden bg-background-secondary">
126
- {assetLoading && <CollectibleAssetSkeleton />}
122
+ {(assetLoading || contentType.loading) && <CollectibleAssetSkeleton />}
127
123
 
128
124
  <img
129
- src={assetLoadFailed ? placeholderImage : proxiedAssetUrl}
125
+ src={
126
+ assetLoadFailed || contentType.failed
127
+ ? placeholderImage
128
+ : proxiedAssetUrl
129
+ }
130
130
  alt={name || 'Collectible'}
131
131
  className={`absolute inset-0 h-full w-full object-cover transition-transform duration-200 ease-in-out group-hover:scale-hover ${
132
- assetLoading ? 'invisible' : 'visible'
132
+ assetLoading || contentType.loading ? 'invisible' : 'visible'
133
133
  }`}
134
134
  onError={() => setAssetLoadFailed(true)}
135
135
  onLoad={() => setAssetLoading(false)}
@@ -0,0 +1,14 @@
1
+ import { Skeleton } from '@0xsequence/design-system';
2
+
3
+ export default function CollectibleAssetSkeleton() {
4
+ return (
5
+ <Skeleton
6
+ data-testid="collectible-asset-skeleton"
7
+ size="lg"
8
+ className="absolute inset-0 h-full w-full animate-shimmer"
9
+ style={{
10
+ borderRadius: 0,
11
+ }}
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,36 @@
1
+ export const isImage = (fileName: string | undefined) => {
2
+ const isImage = /.*\.(png|jpg|jpeg|gif|svg|webp)$/.test(
3
+ fileName?.toLowerCase() || '',
4
+ );
5
+ return isImage;
6
+ };
7
+
8
+ export const isHtml = (fileName: string | undefined) => {
9
+ const isHtml = /.*\.(html\?.+|html)$/.test(fileName?.toLowerCase() || '');
10
+ return isHtml;
11
+ };
12
+
13
+ export const isVideo = (fileName: string | undefined) => {
14
+ const isVideo = /.*\.(mp4|ogg|webm)$/.test(fileName?.toLowerCase() || '');
15
+ return isVideo;
16
+ };
17
+
18
+ export const is3dModel = (fileName: string | undefined) => {
19
+ const isGltf = /.*\.gltf$/.test(fileName?.toLowerCase() || '');
20
+ return isGltf;
21
+ };
22
+
23
+ export const getContentType = (
24
+ url: string,
25
+ ): Promise<'image' | 'video' | 'html' | null> => {
26
+ return new Promise((resolve) => {
27
+ const type = isHtml(url)
28
+ ? 'html'
29
+ : isVideo(url)
30
+ ? 'video'
31
+ : isImage(url)
32
+ ? 'image'
33
+ : null;
34
+ resolve(type);
35
+ });
36
+ };
@@ -40,6 +40,7 @@ interface GetBuyCollectableParams {
40
40
  priceCurrencyAddress: string;
41
41
  customCreditCardProviderCallback: ((buyStep: Step) => void) | undefined;
42
42
  skipNativeBalanceCheck: boolean | undefined;
43
+ nativeTokenAddress: string | undefined;
43
44
  }
44
45
 
45
46
  export const getBuyCollectableParams = async ({
@@ -58,6 +59,7 @@ export const getBuyCollectableParams = async ({
58
59
  checkoutOptions,
59
60
  fee,
60
61
  skipNativeBalanceCheck,
62
+ nativeTokenAddress,
61
63
  }: GetBuyCollectableParams) => {
62
64
  const marketplaceClient = getMarketplaceClient(chainId, config);
63
65
  const { steps } = await marketplaceClient.generateBuyTransaction({
@@ -111,7 +113,7 @@ export const getBuyCollectableParams = async ({
111
113
  callbacks?.onSuccess?.({ hash: hash as Hash });
112
114
  },
113
115
  supplementaryAnalyticsInfo: {
114
- orderId: orderId,
116
+ requestId: orderId,
115
117
  marketplaceKind: marketplace,
116
118
  },
117
119
  onError: callbacks?.onError,
@@ -121,6 +123,7 @@ export const getBuyCollectableParams = async ({
121
123
  buyModalStore.send({ type: 'close' });
122
124
  },
123
125
  skipNativeBalanceCheck,
126
+ nativeTokenAddress,
124
127
  ...(customCreditCardProviderCallback && {
125
128
  customProviderCallback: () => {
126
129
  customCreditCardProviderCallback(buyStep);
@@ -157,6 +160,7 @@ export const usePaymentModalParams = (args: usePaymentModalParams) => {
157
160
  orderId,
158
161
  customCreditCardProviderCallback,
159
162
  skipNativeBalanceCheck,
163
+ nativeTokenAddress,
160
164
  } = buyModalProps;
161
165
 
162
166
  const config = useConfig();
@@ -198,6 +202,7 @@ export const usePaymentModalParams = (args: usePaymentModalParams) => {
198
202
  },
199
203
  customCreditCardProviderCallback,
200
204
  skipNativeBalanceCheck,
205
+ nativeTokenAddress,
201
206
  })
202
207
  : skipToken,
203
208
  });
@@ -11,6 +11,7 @@ export type BuyModalProps = {
11
11
  marketplace: MarketplaceKind;
12
12
  customCreditCardProviderCallback?: (buyStep: Step) => void;
13
13
  skipNativeBalanceCheck?: boolean;
14
+ nativeTokenAddress?: Address;
14
15
  };
15
16
 
16
17
  export type onSuccessCallback = ({
@@ -1,6 +1,7 @@
1
1
  import type { Observable } from '@legendapp/state';
2
2
  import { type Address, type Hex, formatUnits } from 'viem';
3
- import type { OrderbookKind, Price } from '../../../../../types';
3
+ import { OrderbookKind, type Price } from '../../../../../types';
4
+ import { getSequenceMarketplaceRequestId } from '../../../../../utils/getSequenceMarketRequestId';
4
5
  import {
5
6
  type Step,
6
7
  StepType,
@@ -196,12 +197,28 @@ export const useTransactionSteps = ({
196
197
  formatUnits(BigInt(currencyValueRaw), currencyDecimal),
197
198
  );
198
199
 
200
+ let requestId = orderId;
201
+
202
+ if (
203
+ hash &&
204
+ (orderbookKind === OrderbookKind.sequence_marketplace_v1 ||
205
+ orderbookKind === OrderbookKind.sequence_marketplace_v2)
206
+ ) {
207
+ requestId = await getSequenceMarketplaceRequestId(
208
+ hash,
209
+ wallet.publicClient,
210
+ await wallet.address(),
211
+ );
212
+ }
213
+
199
214
  analytics.trackCreateListing({
200
215
  props: {
201
216
  orderbookKind,
202
217
  collectionAddress,
203
218
  currencyAddress: listingInput.listing.currencyAddress,
204
- currencySymbol: '',
219
+ currencySymbol: currency?.symbol || '',
220
+ tokenId: listingInput.listing.tokenId,
221
+ requestId: requestId || '',
205
222
  chainId: chainId.toString(),
206
223
  txnHash: hash || '',
207
224
  },
@@ -214,6 +231,7 @@ export const useTransactionSteps = ({
214
231
  } catch (error) {
215
232
  steps$.transaction.isExecuting.set(false);
216
233
  steps$.transaction.exist.set(false);
234
+
217
235
  if (callbacks?.onError && typeof callbacks.onError === 'function') {
218
236
  callbacks.onError(error as Error);
219
237
  }