@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.
- package/CHANGELOG.md +26 -0
- package/dist/chunk-2PSNAIAT.js +1 -0
- package/dist/chunk-2PSNAIAT.js.map +1 -0
- package/dist/{chunk-7FVZD2LL.js → chunk-4XLXOEXQ.js} +2 -2
- package/dist/{chunk-Y6AOCO3Q.js → chunk-7IYKUVC3.js} +312 -145
- package/dist/chunk-7IYKUVC3.js.map +1 -0
- package/dist/{chunk-5HEZNTSU.js → chunk-D7RVSZAQ.js} +131 -83
- package/dist/chunk-D7RVSZAQ.js.map +1 -0
- package/dist/chunk-DWTLVJAW.js +42 -0
- package/dist/chunk-DWTLVJAW.js.map +1 -0
- package/dist/{chunk-YAUZLETY.js → chunk-G3447GIP.js} +37 -18
- package/dist/chunk-G3447GIP.js.map +1 -0
- package/dist/{chunk-O6GWM7C3.js → chunk-HHYNOPPI.js} +2 -2
- package/dist/{chunk-KTT27YUN.js → chunk-KGM2WLSP.js} +12 -156
- package/dist/chunk-KGM2WLSP.js.map +1 -0
- package/dist/chunk-MAD64DLJ.js +81 -0
- package/dist/chunk-MAD64DLJ.js.map +1 -0
- package/dist/chunk-N7BPFK46.js +1 -0
- package/dist/chunk-N7BPFK46.js.map +1 -0
- package/dist/chunk-NX52D7NX.js +135 -0
- package/dist/chunk-NX52D7NX.js.map +1 -0
- package/dist/chunk-O34GCB47.js +32 -0
- package/dist/chunk-O34GCB47.js.map +1 -0
- package/dist/{chunk-Q2DA477S.js → chunk-YALXP2PW.js} +3 -3
- package/dist/{chunk-A6V7XDY4.js → chunk-YBOFRP65.js} +2 -2
- package/dist/{create-config-CdooE7aU.d.ts → create-config-DwrnzwpM.d.ts} +2 -2
- package/dist/{index-ClKHzm0B.d.ts → index-DGsVBflk.d.ts} +3 -12
- package/dist/index.css +7 -5
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -5
- package/dist/{lowestListing-kFyrUGha.d.ts → lowestListing-BQHIuvNF.d.ts} +2 -2
- package/dist/{marketplace.gen-BU6T6f0m.d.ts → marketplace.gen-DQzWciwC.d.ts} +1 -1
- package/dist/marketplaceConfig-B4Fdsmxu.d.ts +17 -0
- package/dist/react/_internal/api/index.d.ts +3 -3
- package/dist/react/_internal/api/index.js +4 -3
- package/dist/react/_internal/databeat/index.d.ts +2 -1
- package/dist/react/_internal/databeat/index.js +11 -7
- package/dist/react/_internal/index.d.ts +6 -6
- package/dist/react/_internal/index.js +13 -10
- package/dist/react/_internal/wagmi/index.d.ts +6 -5
- package/dist/react/_internal/wagmi/index.js +2 -2
- package/dist/react/hooks/index.d.ts +9 -8
- package/dist/react/hooks/index.js +10 -6
- package/dist/react/hooks/options/index.d.ts +4 -4
- package/dist/react/hooks/options/index.js +7 -4
- package/dist/react/index.css +2650 -0
- package/dist/react/index.css.map +1 -1
- package/dist/react/index.d.ts +8 -8
- package/dist/react/index.js +16 -11
- package/dist/react/queries/index.d.ts +3 -3
- package/dist/react/queries/index.js +6 -3
- package/dist/react/ssr/index.d.ts +3 -3
- package/dist/react/ssr/index.js +6 -3
- package/dist/react/ssr/index.js.map +1 -1
- package/dist/react/ui/components/collectible-card/index.css +2650 -0
- package/dist/react/ui/components/collectible-card/index.css.map +1 -1
- package/dist/react/ui/components/collectible-card/index.d.ts +1 -1
- package/dist/react/ui/components/collectible-card/index.js +14 -9
- package/dist/react/ui/icons/index.js +5 -2
- package/dist/react/ui/index.css +2650 -0
- package/dist/react/ui/index.css.map +1 -1
- package/dist/react/ui/index.d.ts +2 -1
- package/dist/react/ui/index.js +14 -9
- package/dist/react/ui/modals/_internal/components/actionModal/index.js +11 -7
- package/dist/sdk-config-txlivEKe.d.ts +133 -0
- package/dist/{services-9ApY0U-o.d.ts → services-BI_w8Eq4.d.ts} +4 -4
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.js +9 -6
- package/dist/{types-DsTwmKG-.d.ts → types-isjvwapz.d.ts} +3 -3
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +5 -2
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/react/{hooks/options/__mocks__/marketplaceConfig.msw.ts → _internal/api/__mocks__/builder.msw.ts} +70 -63
- package/src/react/_internal/api/__mocks__/metadata.msw.ts +30 -10
- package/src/react/_internal/api/builder-api.ts +32 -0
- package/src/react/_internal/api/builder.gen.ts +215 -0
- package/src/react/_internal/api/services.ts +9 -2
- package/src/react/_internal/databeat/types.ts +1 -0
- package/src/react/_internal/wagmi/__tests__/create-config.test.ts +16 -41
- package/src/react/_internal/wagmi/create-config.ts +3 -16
- package/src/react/_internal/wagmi/get-connectors.ts +31 -21
- package/src/react/_internal/wallet/__tests__/wallet.test.ts +30 -0
- package/src/react/_internal/wallet/wallet.ts +25 -2
- package/src/react/hooks/__tests__/__snapshots__/useMarketplaceConfig.test.tsx.snap +96 -0
- package/src/react/hooks/__tests__/useCollection.test.tsx +4 -5
- package/src/react/hooks/__tests__/useCurrencies.test.tsx +1 -1
- package/src/react/hooks/__tests__/useInventory.test.tsx +15 -16
- package/src/react/hooks/__tests__/useListCollections.test.tsx +18 -163
- package/src/react/hooks/__tests__/useMarketplaceConfig.test.tsx +10 -11
- package/src/react/hooks/options/index.ts +1 -1
- package/src/react/hooks/useAutoSelectFeeOption.tsx +1 -0
- package/src/react/hooks/useMarketplaceConfig.tsx +2 -2
- package/src/react/queries/marketplaceConfig.ts +101 -0
- package/src/react/ssr/__tests__/__snapshots__/create-ssr-client.test.ts.snap +1 -0
- package/src/react/ssr/create-ssr-client.ts +1 -1
- package/src/react/ui/components/collectible-card/CollectibleCard.tsx +2 -2
- package/src/react/ui/components/collectible-card/__tests__/CollectibleAsset.test.tsx +72 -42
- package/src/react/ui/components/collectible-card/{CollectibleAsset.tsx → collectible-asset/CollectibleAsset.tsx} +44 -44
- package/src/react/ui/components/collectible-card/collectible-asset/CollectibleAssetSkeleton.tsx +14 -0
- package/src/react/ui/components/collectible-card/collectible-asset/utils.ts +36 -0
- package/src/react/ui/modals/BuyModal/hooks/usePaymentModalParams.ts +6 -1
- package/src/react/ui/modals/BuyModal/store.ts +1 -0
- package/src/react/ui/modals/CreateListingModal/hooks/useTransactionSteps.tsx +20 -2
- package/src/react/ui/modals/MakeOfferModal/hooks/useTransactionSteps.tsx +17 -1
- package/src/react/ui/modals/SellModal/hooks/useTransactionSteps.tsx +4 -0
- package/src/react/ui/modals/_internal/components/actionModal/ActionModal.test.tsx +7 -1
- package/src/types/index.ts +7 -1
- package/src/types/sdk-config.ts +4 -9
- package/src/utils/fetchContentType.ts +101 -0
- package/src/utils/getSequenceMarketRequestId.ts +32 -0
- package/test/mocks/wallet.ts +3 -1
- package/test/test-utils.tsx +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/builder-types-D5HgAUWR.d.ts +0 -82
- package/dist/chunk-5HEZNTSU.js.map +0 -1
- package/dist/chunk-KTT27YUN.js.map +0 -1
- package/dist/chunk-N7E37ENQ.js +0 -58
- package/dist/chunk-N7E37ENQ.js.map +0 -1
- package/dist/chunk-Y6AOCO3Q.js.map +0 -1
- package/dist/chunk-YAUZLETY.js.map +0 -1
- package/dist/sdk-config-DIzJk_tI.d.ts +0 -26
- package/src/react/hooks/options/__tests__/marketplaceConfigOptions.test.tsx +0 -134
- package/src/react/hooks/options/marketplaceConfigOptions.ts +0 -71
- package/src/types/builder-types.ts +0 -88
- /package/dist/{chunk-7FVZD2LL.js.map → chunk-4XLXOEXQ.js.map} +0 -0
- /package/dist/{chunk-O6GWM7C3.js.map → chunk-HHYNOPPI.js.map} +0 -0
- /package/dist/{chunk-Q2DA477S.js.map → chunk-YALXP2PW.js.map} +0 -0
- /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 '
|
|
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).
|
|
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(
|
|
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()
|
|
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(
|
|
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 '
|
|
2
|
+
export * from '../../queries/marketplaceConfig';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
import { marketplaceConfigOptions } from '
|
|
2
|
+
import { marketplaceConfigOptions } from '../queries/marketplaceConfig';
|
|
3
3
|
import { useConfig } from './useConfig';
|
|
4
4
|
|
|
5
|
-
export * from '
|
|
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 '../
|
|
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-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
160
|
+
// In Safari, pointer-events-none should be applied
|
|
161
|
+
expect(videoElement.className).toContain('pointer-events-none');
|
|
146
162
|
|
|
147
|
-
|
|
148
|
-
|
|
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).
|
|
206
|
+
const iframeElement = document.querySelector('iframe') as HTMLIFrameElement;
|
|
207
|
+
expect(iframeElement).toBeInTheDocument();
|
|
179
208
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
209
|
+
// iframe source should be set correctly
|
|
210
|
+
expect(iframeElement.getAttribute('src')).toBe(
|
|
211
|
+
'https://example.com/interactive.html',
|
|
212
|
+
);
|
|
185
213
|
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
// iframe should have appropriate attributes for security
|
|
215
|
+
expect(iframeElement.getAttribute('sandbox')).toBe('allow-scripts');
|
|
188
216
|
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
// iframe should have title for accessibility
|
|
218
|
+
expect(iframeElement.getAttribute('title')).toBe('HTML Collectible');
|
|
191
219
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
// iframe should have proper styling
|
|
221
|
+
expect(iframeElement.className).toContain('aspect-square');
|
|
222
|
+
expect(iframeElement.className).toContain('w-full');
|
|
195
223
|
|
|
196
|
-
|
|
197
|
-
|
|
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 '
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 (
|
|
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 (
|
|
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={
|
|
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)}
|
package/src/react/ui/components/collectible-card/collectible-asset/CollectibleAssetSkeleton.tsx
ADDED
|
@@ -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
|
-
|
|
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
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Observable } from '@legendapp/state';
|
|
2
2
|
import { type Address, type Hex, formatUnits } from 'viem';
|
|
3
|
-
import
|
|
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
|
}
|