@0xsequence/marketplace-sdk 0.8.5 → 0.8.6

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 (29) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{chunk-KCLMSSPS.js → chunk-6SEJI7YS.js} +165 -4
  3. package/dist/chunk-6SEJI7YS.js.map +1 -0
  4. package/dist/{chunk-ZVTG6US2.js → chunk-QMWMJVTX.js} +2 -2
  5. package/dist/{chunk-SFSFIGHM.js → chunk-TGFX3TMV.js} +4 -4
  6. package/dist/{chunk-EZFCQZHU.js → chunk-V3NVAVHV.js} +2 -2
  7. package/dist/index.css +168 -33
  8. package/dist/index.css.map +1 -1
  9. package/dist/react/_internal/databeat/index.js +2 -2
  10. package/dist/react/hooks/index.d.ts +246 -29
  11. package/dist/react/hooks/index.js +5 -1
  12. package/dist/react/index.d.ts +2 -1
  13. package/dist/react/index.js +8 -4
  14. package/dist/react/ui/components/collectible-card/index.js +4 -4
  15. package/dist/react/ui/index.js +4 -4
  16. package/dist/react/ui/modals/_internal/components/actionModal/index.js +2 -2
  17. package/package.json +2 -1
  18. package/src/react/hooks/__tests__/useAutoSelectFeeOption.test.tsx +21 -75
  19. package/src/react/hooks/__tests__/useCurrencyBalance.test.tsx +4 -25
  20. package/src/react/hooks/index.ts +1 -0
  21. package/src/react/hooks/useFilterState.tsx +181 -0
  22. package/src/react/hooks/useFilters.tsx +24 -0
  23. package/src/react/ui/modals/_internal/components/switchChainModal/__tests__/SwitchChainModal.test.tsx +38 -58
  24. package/src/react/ui/modals/_internal/components/switchChainModal/index.tsx +3 -1
  25. package/tsconfig.tsbuildinfo +1 -1
  26. package/dist/chunk-KCLMSSPS.js.map +0 -1
  27. /package/dist/{chunk-ZVTG6US2.js.map → chunk-QMWMJVTX.js.map} +0 -0
  28. /package/dist/{chunk-SFSFIGHM.js.map → chunk-TGFX3TMV.js.map} +0 -0
  29. /package/dist/{chunk-EZFCQZHU.js.map → chunk-V3NVAVHV.js.map} +0 -0
@@ -1,10 +1,8 @@
1
- import { useChain } from '@0xsequence/connect';
2
1
  import { renderHook, server, waitFor } from '@test';
3
2
  import { http, HttpResponse } from 'msw';
4
3
  import { zeroAddress } from 'viem';
5
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { useAccount } from 'wagmi';
7
- import { mainnet } from 'wagmi/chains';
5
+ import { useDisconnect } from 'wagmi';
8
6
  import type { FeeOption } from '../../../types/waas-types';
9
7
  import {
10
8
  mockIndexerEndpoint,
@@ -13,22 +11,7 @@ import {
13
11
  } from '../../_internal/api/__mocks__/indexer.msw';
14
12
  import { useAutoSelectFeeOption } from '../useAutoSelectFeeOption';
15
13
 
16
- // Mock wagmi hooks
17
- vi.mock('wagmi', async () => {
18
- const actual = await vi.importActual('wagmi');
19
- return {
20
- ...actual,
21
- useAccount: vi.fn(),
22
- };
23
- });
24
-
25
- // Mock @0xsequence/connect
26
- vi.mock('@0xsequence/connect', () => ({
27
- useChain: vi.fn(),
28
- }));
29
-
30
14
  describe('useAutoSelectFeeOption', () => {
31
- const mockUserAddress = '0x1234567890123456789012345678901234567890';
32
15
  const mockChainId = 1;
33
16
 
34
17
  const mockFeeOptions: FeeOption[] = [
@@ -73,32 +56,6 @@ describe('useAutoSelectFeeOption', () => {
73
56
  };
74
57
 
75
58
  beforeEach(() => {
76
- // Mock useAccount hook with complete wagmi account type
77
- vi.mocked(useAccount).mockReturnValue({
78
- address: mockUserAddress as `0x${string}`,
79
- addresses: [mockUserAddress as `0x${string}`],
80
- chain: mainnet,
81
- chainId: mockChainId,
82
- connector: undefined,
83
- isConnected: true,
84
- isConnecting: false,
85
- isDisconnected: false,
86
- isReconnecting: true,
87
- status: 'reconnecting',
88
- });
89
-
90
- // Mock useChain hook with required chain properties
91
- vi.mocked(useChain).mockReturnValue({
92
- ...mainnet,
93
- id: mockChainId,
94
- name: 'Ethereum',
95
- nativeCurrency: {
96
- name: 'Ether',
97
- symbol: 'ETH',
98
- decimals: 18,
99
- },
100
- });
101
-
102
59
  // Set up default handler for successful balance check
103
60
  server.use(
104
61
  mockIndexerHandler('GetTokenBalancesDetails', {
@@ -180,37 +137,6 @@ describe('useAutoSelectFeeOption', () => {
180
137
  });
181
138
  });
182
139
 
183
- it('should return UserNotConnected error when wallet is not connected', async () => {
184
- // Mock useAccount to return no address (user not connected)
185
- vi.mocked(useAccount).mockReturnValue({
186
- address: undefined,
187
- addresses: [],
188
- chain: mainnet,
189
- chainId: mockChainId,
190
- connector: undefined,
191
- isConnected: false,
192
- isConnecting: false,
193
- isDisconnected: false,
194
- isReconnecting: true,
195
- status: 'reconnecting',
196
- });
197
-
198
- const { result } = renderHook(() => useAutoSelectFeeOption(defaultArgs));
199
-
200
- // Wait for the hook to complete
201
- await waitFor(async () => {
202
- const response = await result.current;
203
- expect(response.error).toBe('User not connected');
204
- });
205
-
206
- // Verify final state
207
- const finalResponse = await result.current;
208
- expect(finalResponse).toEqual({
209
- selectedOption: null,
210
- error: 'User not connected',
211
- });
212
- });
213
-
214
140
  it('should select second fee option when user has insufficient balance for first but sufficient for second', async () => {
215
141
  // Override handler for mixed balance scenario
216
142
  server.use(
@@ -307,4 +233,24 @@ describe('useAutoSelectFeeOption', () => {
307
233
  error: 'Failed to check balances',
308
234
  });
309
235
  });
236
+
237
+ it('should return UserNotConnected error when wallet is not connected', async () => {
238
+ const { result: disconnect } = renderHook(() => useDisconnect());
239
+ await disconnect.current.disconnectAsync();
240
+
241
+ const { result } = renderHook(() => useAutoSelectFeeOption(defaultArgs));
242
+
243
+ // Wait for the hook to complete
244
+ await waitFor(async () => {
245
+ const response = await result.current;
246
+ expect(response.error).toBe('User not connected');
247
+ });
248
+
249
+ // Verify final state
250
+ const finalResponse = await result.current;
251
+ expect(finalResponse).toEqual({
252
+ selectedOption: null,
253
+ error: 'User not connected',
254
+ });
255
+ });
310
256
  });
@@ -63,38 +63,17 @@ describe('useCurrencyBalance', () => {
63
63
  `);
64
64
  });
65
65
 
66
- it.skip('should return skipToken when required parameters are missing', () => {
66
+ it('should return skipToken when required parameters are missing', () => {
67
67
  const { result } = renderHook(() =>
68
+ // @ts-expect-error - missing params
68
69
  useCurrencyBalance({
69
70
  chainId: undefined,
70
- userAddress: undefined,
71
- currencyAddress: undefined,
72
71
  }),
73
72
  );
74
73
 
75
74
  expect(result.current.data).toBeUndefined();
76
75
  expect(result.current.isLoading).toBe(false);
77
- // expect(commonPublicClientMocks.getBalance).not.toHaveBeenCalled();
78
- // expect(commonPublicClientMocks.readContract).not.toHaveBeenCalled();
79
- });
80
-
81
- it.skip('should handle errors from public client', async () => {
82
- // Mock error response
83
- // const mockError = new Error('Failed to fetch balance');
84
- // const mockPublicClient = createMockPublicClient({
85
- // getBalance: vi.fn().mockRejectedValue(mockError),
86
- // });
87
-
88
- // Override the mock for this test
89
- // vi.mocked(getPublicRpcClient).mockReturnValue(mockPublicClient);
90
-
91
- const { result } = renderHook(() => useCurrencyBalance(defaultArgs));
92
-
93
- await waitFor(() => {
94
- expect(result.current.isError).toBe(true);
95
- });
96
-
97
- expect(result.current.error).toBeDefined();
98
- expect(result.current.data).toBeUndefined();
76
+ expect(result.current.isSuccess).toBe(false);
77
+ expect(result.current.isError).toBe(false);
99
78
  });
100
79
  });
@@ -10,6 +10,7 @@ export * from './useConvertPriceToUSD';
10
10
  export * from './useCurrencies';
11
11
  export * from './useCurrency';
12
12
  export * from './useFilters';
13
+ export * from './useFilterState';
13
14
  export * from './useFloorOrder';
14
15
  export * from './useHighestOffer';
15
16
  export * from './useInventory';
@@ -0,0 +1,181 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import {
4
+ createSerializer,
5
+ parseAsBoolean,
6
+ parseAsJson,
7
+ parseAsString,
8
+ useQueryState,
9
+ } from 'nuqs';
10
+ import { type PropertyFilter, PropertyType } from '../_internal';
11
+
12
+ interface StringFilterValues {
13
+ type: PropertyType.STRING;
14
+ values: string[];
15
+ }
16
+
17
+ interface IntFilterValues {
18
+ type: PropertyType.INT;
19
+ min: number;
20
+ max: number;
21
+ }
22
+
23
+ type FilterValues = StringFilterValues | IntFilterValues;
24
+
25
+ const validateFilters = (value: unknown): PropertyFilter[] => {
26
+ if (!Array.isArray(value)) return [];
27
+ return value.filter(
28
+ (f): f is PropertyFilter =>
29
+ typeof f === 'object' &&
30
+ typeof f.name === 'string' &&
31
+ Object.values(PropertyType).includes(f.type),
32
+ );
33
+ };
34
+
35
+ const filtersParser = parseAsJson(validateFilters).withDefault([]);
36
+ const searchParser = parseAsString.withDefault('');
37
+ const listedOnlyParser = parseAsBoolean.withDefault(false);
38
+
39
+ const serialize = createSerializer(
40
+ {
41
+ filters: filtersParser,
42
+ search: searchParser,
43
+ listedOnly: listedOnlyParser,
44
+ },
45
+ {
46
+ urlKeys: {
47
+ filters: 'f',
48
+ search: 'q',
49
+ listedOnly: 'l',
50
+ },
51
+ },
52
+ );
53
+
54
+ export function useFilterState() {
55
+ const [filterOptions, setFilterOptions] = useQueryState(
56
+ 'filters',
57
+ filtersParser,
58
+ );
59
+ const [searchText, setSearchText] = useQueryState('search', searchParser);
60
+ const [showListedOnly, setShowListedOnly] = useQueryState(
61
+ 'listedOnly',
62
+ listedOnlyParser,
63
+ );
64
+
65
+ const helpers = useMemo(
66
+ () => ({
67
+ getFilter: (name: string): PropertyFilter | undefined => {
68
+ return filterOptions?.find((f) => f.name === name);
69
+ },
70
+
71
+ getFilterValues: (name: string): FilterValues | undefined => {
72
+ const filter = filterOptions?.find((f) => f.name === name);
73
+ if (!filter) return undefined;
74
+
75
+ if (filter.type === PropertyType.INT) {
76
+ return {
77
+ type: PropertyType.INT,
78
+ min: filter.min ?? 0,
79
+ max: filter.max ?? 0,
80
+ };
81
+ }
82
+
83
+ return {
84
+ type: PropertyType.STRING,
85
+ values: (filter.values as string[]) ?? [],
86
+ };
87
+ },
88
+
89
+ isFilterActive: (name: string): boolean => {
90
+ return !!filterOptions?.find((f) => f.name === name);
91
+ },
92
+
93
+ isStringValueSelected: (name: string, value: string): boolean => {
94
+ const filter = filterOptions?.find((f) => f.name === name);
95
+ if (!filter || filter.type !== PropertyType.STRING) return false;
96
+ return (filter.values as string[])?.includes(value) ?? false;
97
+ },
98
+
99
+ isIntFilterActive: (name: string): boolean => {
100
+ const filter = filterOptions?.find((f) => f.name === name);
101
+ return !!filter && filter.type === PropertyType.INT;
102
+ },
103
+
104
+ getIntFilterRange: (name: string): [number, number] | undefined => {
105
+ const filter = filterOptions?.find((f) => f.name === name);
106
+ if (!filter || filter.type !== PropertyType.INT) return undefined;
107
+ return [filter.min ?? 0, filter.max ?? 0];
108
+ },
109
+
110
+ deleteFilter: (name: string) => {
111
+ const otherFilters =
112
+ filterOptions?.filter((f) => !(f.name === name)) ?? [];
113
+ setFilterOptions(otherFilters);
114
+ },
115
+
116
+ toggleStringFilterValue: (name: string, value: string) => {
117
+ const otherFilters =
118
+ filterOptions?.filter((f) => !(f.name === name)) ?? [];
119
+ const filter = filterOptions?.find((f) => f.name === name);
120
+ const existingValues =
121
+ filter?.type === PropertyType.STRING
122
+ ? ((filter.values as string[]) ?? [])
123
+ : [];
124
+
125
+ if (existingValues.includes(value)) {
126
+ const newValues = existingValues.filter((v) => v !== value);
127
+ if (newValues.length === 0) {
128
+ setFilterOptions(otherFilters);
129
+ return;
130
+ }
131
+ setFilterOptions([
132
+ ...otherFilters,
133
+ { name, type: PropertyType.STRING, values: newValues },
134
+ ]);
135
+ } else {
136
+ setFilterOptions([
137
+ ...otherFilters,
138
+ {
139
+ name,
140
+ type: PropertyType.STRING,
141
+ values: [...existingValues, value],
142
+ },
143
+ ]);
144
+ }
145
+ },
146
+
147
+ setIntFilterValue: (name: string, min: number, max: number) => {
148
+ if (min === max && min === 0) {
149
+ const otherFilters =
150
+ filterOptions?.filter((f) => !(f.name === name)) ?? [];
151
+ setFilterOptions(otherFilters);
152
+ return;
153
+ }
154
+ const otherFilters =
155
+ filterOptions?.filter((f) => !(f.name === name)) ?? [];
156
+ setFilterOptions([
157
+ ...otherFilters,
158
+ { name, type: PropertyType.INT, min, max },
159
+ ]);
160
+ },
161
+
162
+ clearAllFilters: () => {
163
+ void setShowListedOnly(false);
164
+ void setFilterOptions([]);
165
+ void setSearchText('');
166
+ },
167
+ }),
168
+ [filterOptions, setFilterOptions, setShowListedOnly, setSearchText],
169
+ );
170
+
171
+ return {
172
+ filterOptions,
173
+ searchText,
174
+ showListedOnly,
175
+ setFilterOptions,
176
+ setSearchText,
177
+ setShowListedOnly,
178
+ ...helpers,
179
+ serialize,
180
+ };
181
+ }
@@ -18,6 +18,7 @@ const UseFiltersSchema = z.object({
18
18
  collectionAddress: AddressSchema,
19
19
  showAllFilters: z.boolean().default(false).optional(),
20
20
  query: QueryArgSchema,
21
+ excludePropertyValues: z.boolean().default(false).optional(),
21
22
  });
22
23
 
23
24
  export type UseFiltersArgs = z.infer<typeof UseFiltersSchema>;
@@ -33,6 +34,7 @@ export const fetchFilters = async (args: UseFiltersArgs, config: SdkConfig) => {
33
34
  chainID: parsedArgs.chainId.toString(),
34
35
  contractAddress: parsedArgs.collectionAddress,
35
36
  excludeProperties: [], // TODO: We can leverage this for some of the exclusion logic
37
+ excludePropertyValues: parsedArgs.excludePropertyValues,
36
38
  })
37
39
  .then((resp) => resp.filters);
38
40
 
@@ -117,3 +119,25 @@ export const useFilters = (args: UseFiltersArgs) => {
117
119
  const config = useConfig();
118
120
  return useQuery(filtersOptions(args, config));
119
121
  };
122
+
123
+ export const useFiltersProgressive = (args: UseFiltersArgs) => {
124
+ const config = useConfig();
125
+
126
+ const namesQuery = useQuery(
127
+ filtersOptions({ ...args, excludePropertyValues: true }, config),
128
+ );
129
+
130
+ const fullQuery = useQuery({
131
+ ...filtersOptions(args, config),
132
+ placeholderData: namesQuery.data,
133
+ });
134
+
135
+ const isLoadingNames = namesQuery.isLoading;
136
+ const isFetchingValues = fullQuery.isPlaceholderData && fullQuery.isFetching;
137
+
138
+ return {
139
+ ...fullQuery,
140
+ isFetchingValues,
141
+ isLoadingNames,
142
+ };
143
+ };
@@ -6,19 +6,32 @@ import {
6
6
  wagmiConfig,
7
7
  waitFor,
8
8
  } from '@test';
9
- import { describe, expect, test, vi } from 'vitest';
10
- import { type Config, useChainId } from 'wagmi';
9
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
10
+ import { useChainId } from 'wagmi';
11
11
  import SwitchChainModal, { useSwitchChainModal } from '../index';
12
12
  import { switchChainModal$ } from '../store';
13
13
 
14
14
  const chainToSwitchTo = wagmiConfig.chains[1];
15
15
 
16
16
  describe('SwitchChainModal', () => {
17
+ beforeEach(() => {
18
+ switchChainModal$.state.isSwitching.set(false);
19
+ switchChainModal$.state.chainIdToSwitchTo.set(undefined);
20
+ switchChainModal$.state.onError.set(undefined);
21
+ switchChainModal$.state.onSuccess.set(undefined);
22
+ switchChainModal$.state.onClose.set(undefined);
23
+ });
24
+
17
25
  test('opens switch chain modal with correct chain', async () => {
26
+ const { result } = renderHook(() => useSwitchChainModal());
27
+
28
+ result.current.show({ chainIdToSwitchTo: chainToSwitchTo.id });
18
29
  render(<SwitchChainModal />);
19
30
 
20
- const { show } = useSwitchChainModal();
21
- show({ chainIdToSwitchTo: chainToSwitchTo.id });
31
+ await waitFor(() => {
32
+ const titleElement = screen.getByText('Wrong network');
33
+ expect(titleElement).toBeInTheDocument();
34
+ });
22
35
 
23
36
  const titleElement = await screen.findByText('Wrong network');
24
37
  expect(titleElement).toBeInTheDocument();
@@ -35,13 +48,12 @@ describe('SwitchChainModal', () => {
35
48
  });
36
49
 
37
50
  test('closes switch chain modal using close callback', async () => {
38
- render(<SwitchChainModal />);
39
-
40
- const { show, close } = useSwitchChainModal();
51
+ const { result } = renderHook(() => useSwitchChainModal());
41
52
 
42
- show({ chainIdToSwitchTo: chainToSwitchTo.id });
53
+ result.current.show({ chainIdToSwitchTo: chainToSwitchTo.id });
54
+ render(<SwitchChainModal />);
43
55
 
44
- close();
56
+ result.current.close();
45
57
 
46
58
  const titleElement = screen.queryByText('Wrong network');
47
59
  await waitFor(() => {
@@ -68,66 +80,34 @@ describe('SwitchChainModal', () => {
68
80
  });
69
81
  });
70
82
 
71
- test.skip('shows spinner while switching chain', async () => {
72
- render(<SwitchChainModal />);
73
- const { show } = useSwitchChainModal();
74
-
75
- show({ chainIdToSwitchTo: chainToSwitchTo.id });
76
-
77
- const switchButton = await screen.findByRole('button', {
78
- name: /switch network/i,
79
- });
80
- expect(switchButton).toBeInTheDocument();
81
-
82
- fireEvent.click(switchButton);
83
+ test('shows spinner while switching chain', async () => {
84
+ switchChainModal$.state.isSwitching.set(true);
85
+ switchChainModal$.state.chainIdToSwitchTo.set(chainToSwitchTo.id);
83
86
 
84
- await waitFor(() => {
85
- const spinner = screen.findByTestId('switch-chain-spinner');
86
- expect(spinner).toBeInTheDocument();
87
- });
87
+ render(<SwitchChainModal />);
88
88
 
89
- const spinner = document.querySelector('.spinner');
89
+ const spinner = screen.getByTestId('switch-chain-spinner');
90
90
  expect(spinner).toBeInTheDocument();
91
-
92
- await waitFor(() => {
93
- expect(switchChainModal$.state.isSwitching.get()).toBe(false);
94
- expect(document.querySelector('.spinner')).not.toBeInTheDocument();
95
- });
96
91
  });
97
92
 
98
- test.skip('calls onError callback when switching chain fails', async () => {
99
- const onError = vi.fn();
100
-
101
- const mockConnector = {
102
- ...wagmiConfig.connectors[0],
103
- features: {
104
- // @ts-expect-error
105
- ...wagmiConfig.connectors[0].features,
106
- switchChainError: true,
107
- },
108
- };
109
- const wagmiConfigWithSwitchChainError = {
110
- ...wagmiConfig,
111
- connectors: [mockConnector],
112
- } as Config;
113
-
114
- render(<SwitchChainModal />, {
115
- wagmiConfig: wagmiConfigWithSwitchChainError,
116
- });
93
+ test('shows error message when chain switch fails', async () => {
94
+ const mockOnError = vi.fn();
95
+ const { result } = renderHook(() => useSwitchChainModal());
117
96
 
118
- const { show } = useSwitchChainModal();
119
- show({ chainIdToSwitchTo: chainToSwitchTo.id, onError });
97
+ result.current.show({
98
+ // @ts-expect-error - invalid chain id to trigger error
99
+ chainIdToSwitchTo: 'invalid-chain-id',
100
+ onError: mockOnError,
101
+ });
102
+ render(<SwitchChainModal />);
120
103
 
121
- const switchButton = await screen.findByRole('button', {
104
+ const button = await screen.findByRole('button', {
122
105
  name: /switch network/i,
123
106
  });
124
- fireEvent.click(switchButton);
107
+ fireEvent.click(button);
125
108
 
126
109
  await waitFor(() => {
127
- // const chainId = renderHook(() => useChainId()).result.current
128
- // expect(chainId).not.toBe(chainToSwitchTo.id)
129
- expect(onError).toHaveBeenCalled();
130
- expect(switchChainModal$.isOpen.get()).toBe(true);
110
+ expect(mockOnError).toHaveBeenCalledOnce();
131
111
  });
132
112
  });
133
113
  });
@@ -101,7 +101,9 @@ const SwitchChainModal = observer(() => {
101
101
  size="sm"
102
102
  label={
103
103
  isSwitching$.get() ? (
104
- <Spinner className="spinner" />
104
+ <div data-testid="switch-chain-spinner">
105
+ <Spinner className="spinner" />
106
+ </div>
105
107
  ) : (
106
108
  'Switch Network'
107
109
  )