@0xsequence/marketplace-sdk 0.8.3 → 0.8.4

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 (64) hide show
  1. package/CHANGELOG.md +8 -1
  2. package/dist/{chunk-FH4TZRDV.js → chunk-44YGZVBS.js} +2 -2
  3. package/dist/{chunk-QTV77W42.js → chunk-HRL2TMXU.js} +5 -5
  4. package/dist/chunk-HRL2TMXU.js.map +1 -0
  5. package/dist/{chunk-5ATGT5S4.js → chunk-VBRJ2OPM.js} +14 -6
  6. package/dist/chunk-VBRJ2OPM.js.map +1 -0
  7. package/dist/{chunk-DFI52A2E.js → chunk-VF3LWBQB.js} +365 -243
  8. package/dist/chunk-VF3LWBQB.js.map +1 -0
  9. package/dist/{chunk-FSJKN4YN.js → chunk-ZSCZLHKX.js} +194 -2
  10. package/dist/chunk-ZSCZLHKX.js.map +1 -0
  11. package/dist/index.js +5 -5
  12. package/dist/{lowestListing-DUZ_nYml.d.ts → lowestListing-W7P4EkC3.d.ts} +34 -11
  13. package/dist/react/_internal/databeat/index.js +4 -4
  14. package/dist/react/hooks/index.d.ts +8 -5
  15. package/dist/react/hooks/index.js +5 -3
  16. package/dist/react/index.d.ts +2 -2
  17. package/dist/react/index.js +8 -6
  18. package/dist/react/queries/index.d.ts +1 -1
  19. package/dist/react/queries/index.js +5 -1
  20. package/dist/react/ui/components/collectible-card/index.js +6 -6
  21. package/dist/react/ui/icons/index.js +1 -1
  22. package/dist/react/ui/index.js +6 -6
  23. package/dist/react/ui/modals/_internal/components/actionModal/index.js +4 -4
  24. package/dist/utils/abi/index.js +5 -5
  25. package/dist/utils/index.js +5 -5
  26. package/package.json +1 -1
  27. package/src/react/_internal/api/__mocks__/marketplace.msw.ts +35 -21
  28. package/src/react/hooks/__tests__/useCancelTransactionSteps.test.tsx +4 -9
  29. package/src/react/hooks/__tests__/useGenerateCancelTransaction.test.tsx +5 -4
  30. package/src/react/hooks/__tests__/useGenerateListingTransaction.test.tsx +14 -10
  31. package/src/react/hooks/__tests__/useGenerateOfferTransaction.test.tsx +115 -65
  32. package/src/react/hooks/__tests__/useGenerateSellTransaction.test.tsx +10 -7
  33. package/src/react/hooks/__tests__/useInventory.test.tsx +294 -0
  34. package/src/react/hooks/index.ts +1 -0
  35. package/src/react/hooks/useAutoSelectFeeOption.tsx +10 -3
  36. package/src/react/hooks/useCancelOrder.tsx +1 -0
  37. package/src/react/hooks/useCancelTransactionSteps.tsx +18 -4
  38. package/src/react/hooks/useGenerateOfferTransaction.tsx +11 -1
  39. package/src/react/hooks/useInventory.tsx +15 -0
  40. package/src/react/hooks/util/optimisticCancelUpdates.ts +115 -0
  41. package/src/react/queries/index.ts +1 -0
  42. package/src/react/queries/inventory.ts +303 -0
  43. package/src/react/queries/listBalances.ts +1 -8
  44. package/src/react/queries/listCollectibles.ts +12 -3
  45. package/src/react/ui/modals/CreateListingModal/__tests__/Modal.test.tsx +74 -104
  46. package/src/react/ui/modals/MakeOfferModal/__tests__/Modal.test.tsx +108 -78
  47. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.test.tsx +286 -0
  48. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +16 -4
  49. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/__tests__/index.test.tsx +35 -132
  50. package/src/react/ui/modals/_internal/components/floorPriceText/__tests__/FloorPriceText.test.tsx +199 -0
  51. package/src/react/ui/modals/_internal/components/priceInput/__tests__/PriceInput.test.tsx +55 -0
  52. package/src/react/ui/modals/_internal/components/priceInput/index.tsx +1 -1
  53. package/src/react/ui/modals/_internal/components/switchChainModal/index.tsx +2 -2
  54. package/test/const.ts +24 -0
  55. package/test/test-utils.tsx +74 -26
  56. package/.changeset/flat-parks-clean.md +0 -8
  57. package/.changeset/red-buckets-deny.md +0 -6
  58. package/.changeset/seven-doors-taste.md +0 -5
  59. package/dist/chunk-5ATGT5S4.js.map +0 -1
  60. package/dist/chunk-DFI52A2E.js.map +0 -1
  61. package/dist/chunk-FSJKN4YN.js.map +0 -1
  62. package/dist/chunk-QTV77W42.js.map +0 -1
  63. package/src/react/ui/modals/_internal/components/priceInput/__tests__/index.test.tsx +0 -164
  64. /package/dist/{chunk-FH4TZRDV.js.map → chunk-44YGZVBS.js.map} +0 -0
@@ -0,0 +1,303 @@
1
+ import type {
2
+ ContractInfo,
3
+ Page as IndexerPage,
4
+ TokenBalance,
5
+ } from '@0xsequence/indexer';
6
+ import { infiniteQueryOptions } from '@tanstack/react-query';
7
+ import type { Address } from 'viem';
8
+ import { OrderSide, type Page, type SdkConfig } from '../../types';
9
+ import {
10
+ type CollectibleOrder,
11
+ type ContractType,
12
+ getIndexerClient,
13
+ } from '../_internal';
14
+ import { fetchCollectibles } from './listCollectibles';
15
+
16
+ export interface UseInventoryArgs {
17
+ accountAddress: Address;
18
+ collectionAddress: Address;
19
+ chainId: number;
20
+ isLaos721?: boolean;
21
+ query?: {
22
+ enabled?: boolean;
23
+ };
24
+ }
25
+
26
+ // Maintain collection state across calls
27
+ interface InventoryState {
28
+ seenTokenIds: Set<string>;
29
+ marketplaceFinished: boolean;
30
+ // Track if we've already fetched all indexer tokens
31
+ indexerTokensFetched: boolean;
32
+ // Store the token balances from the indexer
33
+ indexerTokenBalances: Map<string, CollectibleWithBalance>;
34
+ }
35
+
36
+ // Store state per collection
37
+ const stateByCollection = new Map<string, InventoryState>();
38
+
39
+ const getCollectionKey = (args: UseInventoryArgs) =>
40
+ `${args.chainId}-${args.collectionAddress}-${args.accountAddress}`;
41
+
42
+ interface GetInventoryArgs extends Omit<UseInventoryArgs, 'query'> {
43
+ isLaos721: boolean;
44
+ }
45
+
46
+ interface CollectibleWithBalance extends CollectibleOrder {
47
+ balance: string;
48
+ contractInfo?: ContractInfo;
49
+ contractType: ContractType.ERC1155 | ContractType.ERC721;
50
+ }
51
+
52
+ export interface CollectiblesResponse {
53
+ collectibles: CollectibleWithBalance[];
54
+ page: Page;
55
+ }
56
+
57
+ function getOrInitState(collectionKey: string): InventoryState {
58
+ if (!stateByCollection.has(collectionKey)) {
59
+ stateByCollection.set(collectionKey, {
60
+ seenTokenIds: new Set<string>(),
61
+ marketplaceFinished: false,
62
+ indexerTokensFetched: false,
63
+ indexerTokenBalances: new Map(),
64
+ });
65
+ }
66
+
67
+ // biome-ignore lint/style/noNonNullAssertion: guaranteed to exist, by the above init
68
+ return stateByCollection.get(collectionKey)!;
69
+ }
70
+
71
+ function collectibleFromTokenBalance(
72
+ token: TokenBalance,
73
+ ): CollectibleWithBalance {
74
+ return {
75
+ metadata: {
76
+ tokenId: token.tokenID ?? '',
77
+ attributes: token.tokenMetadata?.attributes ?? [],
78
+ image: token.tokenMetadata?.image,
79
+ name: token.tokenMetadata?.name ?? '',
80
+ description: token.tokenMetadata?.description,
81
+ video: token.tokenMetadata?.video,
82
+ audio: token.tokenMetadata?.audio,
83
+ },
84
+ contractInfo: token.contractInfo,
85
+ contractType: token.contractType as
86
+ | ContractType.ERC1155
87
+ | ContractType.ERC721,
88
+ balance: token.balance,
89
+ };
90
+ }
91
+
92
+ async function fetchAllIndexerTokens(
93
+ chainId: number,
94
+ accountAddress: Address,
95
+ collectionAddress: Address,
96
+ config: SdkConfig,
97
+ state: InventoryState,
98
+ ): Promise<void> {
99
+ const indexerClient = getIndexerClient(chainId, config);
100
+
101
+ let page: IndexerPage = {
102
+ pageSize: 50,
103
+ };
104
+
105
+ while (true) {
106
+ const { balances, page: nextPage } = await indexerClient.getTokenBalances({
107
+ accountAddress,
108
+ contractAddress: collectionAddress,
109
+ includeMetadata: true,
110
+ page: page,
111
+ });
112
+
113
+ for (const balance of balances) {
114
+ if (balance.tokenID) {
115
+ state.indexerTokenBalances.set(
116
+ balance.tokenID,
117
+ collectibleFromTokenBalance(balance),
118
+ );
119
+ }
120
+ }
121
+
122
+ if (!nextPage.more) {
123
+ break;
124
+ }
125
+ page = nextPage;
126
+ }
127
+
128
+ state.indexerTokensFetched = true;
129
+ }
130
+
131
+ // Process indexer tokens that we haven't seen yet
132
+ function processRemainingIndexerTokens(
133
+ state: InventoryState,
134
+ page: Page,
135
+ ): CollectiblesResponse {
136
+ const allTokens = Array.from(state.indexerTokenBalances.values());
137
+
138
+ // Filter out tokens that we've already seen
139
+ const newTokens = allTokens.filter(
140
+ (token) => !state.seenTokenIds.has(token.metadata.tokenId),
141
+ );
142
+
143
+ // Calculate pagination
144
+ const startIndex = (page.page - 1) * page.pageSize;
145
+ const endIndex = startIndex + page.pageSize;
146
+ const paginatedTokens = newTokens.slice(startIndex, endIndex);
147
+
148
+ // Add new token IDs to the set
149
+ for (const token of paginatedTokens) {
150
+ state.seenTokenIds.add(token.metadata.tokenId);
151
+ }
152
+
153
+ return {
154
+ collectibles: paginatedTokens,
155
+ page: {
156
+ page: page.page,
157
+ pageSize: page.pageSize,
158
+ more: endIndex < newTokens.length,
159
+ },
160
+ };
161
+ }
162
+
163
+ function processMarketplaceCollectibles(
164
+ collectibles: CollectibleOrder[],
165
+ state: InventoryState,
166
+ page: Page,
167
+ ): {
168
+ enrichedCollectibles: CollectibleWithBalance[];
169
+ missingTokens: CollectibleWithBalance[];
170
+ } {
171
+ // Add new token IDs to the set
172
+ for (const c of collectibles) {
173
+ state.seenTokenIds.add(c.metadata.tokenId);
174
+ }
175
+
176
+ // Enrich marketplace collectibles with balance data from indexer
177
+ const enrichedCollectibles = collectibles.map((c: CollectibleOrder) => {
178
+ const tokenId = c.metadata.tokenId;
179
+ const indexerData = state.indexerTokenBalances.get(tokenId);
180
+
181
+ return {
182
+ ...c,
183
+ balance: indexerData?.balance,
184
+ contractInfo: indexerData?.contractInfo,
185
+ contractType: indexerData?.contractType,
186
+ } as CollectibleWithBalance;
187
+ });
188
+
189
+ // Check for missing tokens in the marketplace data
190
+ const marketplaceTokenIds = new Set(
191
+ enrichedCollectibles.map((c) => c.metadata.tokenId),
192
+ );
193
+
194
+ const missingTokens = Array.from(state.indexerTokenBalances.entries())
195
+ .filter(([tokenId]) => !marketplaceTokenIds.has(tokenId))
196
+ .map(([_, balance]) => balance)
197
+ .slice(0, page.pageSize);
198
+
199
+ return { enrichedCollectibles, missingTokens };
200
+ }
201
+
202
+ export async function fetchInventory(
203
+ args: GetInventoryArgs,
204
+ config: SdkConfig,
205
+ page: Page,
206
+ ): Promise<CollectiblesResponse> {
207
+ const { accountAddress, collectionAddress, chainId, isLaos721 } = args;
208
+ const collectionKey = getCollectionKey(args);
209
+ const state = getOrInitState(collectionKey);
210
+
211
+ // On first run, fetch all pages from the indexer
212
+ if (!state.indexerTokensFetched) {
213
+ await fetchAllIndexerTokens(
214
+ chainId,
215
+ accountAddress,
216
+ collectionAddress,
217
+ config,
218
+ state,
219
+ );
220
+ }
221
+
222
+ // If marketplace API has no more results, use the indexer data
223
+ if (state.marketplaceFinished) {
224
+ return processRemainingIndexerTokens(state, page);
225
+ }
226
+
227
+ // Fetch collectibles from marketplace API
228
+ const collectibles = await fetchCollectibles(
229
+ {
230
+ chainId,
231
+ collectionAddress,
232
+ filter: {
233
+ inAccounts: [accountAddress],
234
+ includeEmpty: true,
235
+ },
236
+ side: OrderSide.listing,
237
+ isLaos721,
238
+ },
239
+ config,
240
+ page,
241
+ );
242
+
243
+ // Process the collectibles and find missing tokens
244
+ const { enrichedCollectibles, missingTokens } =
245
+ processMarketplaceCollectibles(collectibles.collectibles, state, page);
246
+
247
+ // If there are no more results from the marketplace API
248
+ if (!collectibles.page?.more) {
249
+ // Mark marketplace as finished and start using indexer data on next call
250
+ state.marketplaceFinished = true;
251
+ return {
252
+ collectibles: [...enrichedCollectibles, ...missingTokens],
253
+ page: {
254
+ page: collectibles.page?.page ?? page.page,
255
+ pageSize: collectibles.page?.pageSize ?? page.pageSize,
256
+ more: missingTokens.length > 0,
257
+ },
258
+ };
259
+ }
260
+
261
+ return {
262
+ collectibles: enrichedCollectibles,
263
+ page: {
264
+ page: collectibles.page?.page ?? page.page,
265
+ pageSize: collectibles.page?.pageSize ?? page.pageSize,
266
+ more: Boolean(collectibles.page?.more),
267
+ },
268
+ };
269
+ }
270
+
271
+ export function inventoryOptions(args: UseInventoryArgs, config: SdkConfig) {
272
+ const collectionKey = getCollectionKey(args);
273
+ const enabledQuery = args.query?.enabled ?? true;
274
+ const enabled =
275
+ enabledQuery && !!args.accountAddress && !!args.collectionAddress;
276
+
277
+ return infiniteQueryOptions({
278
+ queryKey: [
279
+ 'inventory',
280
+ args.accountAddress,
281
+ args.collectionAddress,
282
+ args.chainId,
283
+ ],
284
+ queryFn: ({ pageParam }) =>
285
+ fetchInventory(
286
+ {
287
+ ...args,
288
+ isLaos721: args.isLaos721 ?? false,
289
+ },
290
+ config,
291
+ pageParam,
292
+ ),
293
+ initialPageParam: { page: 1, pageSize: 30 } as Page,
294
+ getNextPageParam: (lastPage) =>
295
+ lastPage.page?.more ? lastPage.page : undefined,
296
+ enabled,
297
+ meta: {
298
+ onInvalidate: () => {
299
+ stateByCollection.delete(collectionKey);
300
+ },
301
+ },
302
+ });
303
+ }
@@ -24,14 +24,6 @@ export type UseListBalancesArgs = {
24
24
  };
25
25
  };
26
26
 
27
- /**
28
- * Fetches a list of token balances with pagination support
29
- *
30
- * @param args - Arguments for the API call
31
- * @param config - SDK configuration
32
- * @param page - Page parameters for pagination
33
- * @returns The token balances data
34
- */
35
27
  export async function fetchBalances(
36
28
  args: UseListBalancesArgs,
37
29
  config: SdkConfig,
@@ -48,6 +40,7 @@ export async function fetchBalances(
48
40
  body: JSON.stringify({
49
41
  chainId: args.chainId.toString(),
50
42
  accountAddress: args.accountAddress,
43
+ contractAddress: args.contractAddress,
51
44
  includeMetadata: args.includeMetadata ?? true,
52
45
  page: {
53
46
  sort: [
@@ -1,5 +1,5 @@
1
1
  import { infiniteQueryOptions } from '@tanstack/react-query';
2
- import type { Hex } from 'viem';
2
+ import type { Address, Hex } from 'viem';
3
3
  import type { Page, SdkConfig } from '../../types';
4
4
  import type {
5
5
  CollectibleOrder,
@@ -8,7 +8,7 @@ import type {
8
8
  ListCollectiblesReturn,
9
9
  } from '../_internal';
10
10
  import { OrderSide, collectableKeys, getMarketplaceClient } from '../_internal';
11
- import { fetchBalances } from './listBalances';
11
+ import { type UseListBalancesArgs, fetchBalances } from './listBalances';
12
12
  export type UseListCollectiblesArgs = {
13
13
  collectionAddress: Hex;
14
14
  chainId: number;
@@ -43,7 +43,16 @@ export async function fetchCollectibles(
43
43
 
44
44
  if (args.isLaos721 && args.side === OrderSide.listing) {
45
45
  try {
46
- const balances = await fetchBalances(args, config, page);
46
+ const fetchBalancesArgs = {
47
+ chainId: args.chainId,
48
+ accountAddress: args.filter?.inAccounts?.[0] as Address,
49
+ contractAddress: args.collectionAddress,
50
+ page: page,
51
+ includeMetadata: true,
52
+ isLaos721: true,
53
+ } satisfies UseListBalancesArgs;
54
+
55
+ const balances = await fetchBalances(fetchBalancesArgs, config, page);
47
56
  const collectibles: CollectibleOrder[] = balances.balances.map(
48
57
  (balance) => {
49
58
  if (!balance.tokenMetadata)
@@ -1,137 +1,107 @@
1
- import { useWaasFeeOptions } from '@0xsequence/connect';
2
- import { cleanup, fireEvent, render, screen, waitFor } from '@test';
3
- import { zeroAddress } from 'viem';
1
+ import { cleanup, render, renderHook, screen, waitFor } from '@test';
2
+ import { TEST_COLLECTIBLE } from '@test/const';
3
+ import { createMockWallet } from '@test/mocks/wallet';
4
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
- import * as hooks from '../../../../hooks';
5
+ import { useCreateListingModal } from '..';
6
+ import { StepType, WalletKind } from '../../../../_internal';
7
+ import { createMockStep } from '../../../../_internal/api/__mocks__/marketplace.msw';
8
+ import * as walletModule from '../../../../_internal/wallet/useWallet';
6
9
  import { CreateListingModal } from '../Modal';
7
- import { createListingModal$ } from '../store';
8
-
9
- vi.mock(import('../../../../hooks'), async (importOriginal) => {
10
- const actual = await importOriginal();
11
- return {
12
- ...actual,
13
- useCollectible: vi.fn(actual.useCollectible),
14
- useCollection: vi.fn(actual.useCollection),
15
- useCurrencies: vi.fn(actual.useCurrencies),
16
- useMarketplaceConfig: vi.fn(actual.useMarketplaceConfig),
17
- useLowestListing: vi.fn(actual.useLowestListing),
18
- };
19
- });
20
-
21
- vi.mock('@0xsequence/kit', () => ({
22
- useWaasFeeOptions: vi.fn(),
23
- }));
10
+ import * as useGetTokenApprovalDataModule from '../hooks/useGetTokenApproval';
24
11
 
25
12
  const defaultArgs = {
26
- collectionAddress: zeroAddress,
27
- chainId: 1,
28
- collectibleId: '1',
13
+ collectionAddress: TEST_COLLECTIBLE.collectionAddress,
14
+ chainId: TEST_COLLECTIBLE.chainId,
15
+ collectibleId: TEST_COLLECTIBLE.collectibleId,
29
16
  };
30
17
 
31
- describe.skip('CreateListingModal', () => {
18
+ describe('MakeOfferModal', () => {
19
+ const mockWallet = createMockWallet();
20
+
32
21
  beforeEach(() => {
33
22
  cleanup();
34
23
  // Reset all mocks
35
24
  vi.clearAllMocks();
36
25
  vi.resetAllMocks();
37
- vi.mocked(useWaasFeeOptions).mockReturnValue([undefined, vi.fn(), vi.fn()]);
26
+ vi.restoreAllMocks();
38
27
  });
39
28
 
40
- it('should not render when modal is closed', () => {
41
- render(<CreateListingModal />);
42
- expect(screen.queryByText('List item for sale')).toBeNull();
43
- });
44
-
45
- it('should show error modal when there is an error fetching data', async () => {
46
- // @ts-expect-error - TODO: Add a common mock object with the correct shape
47
- vi.mocked(hooks.useCollection).mockReturnValue({
48
- data: undefined,
29
+ it('should show main button if there is no approval step', async () => {
30
+ // Mock sequence wallet
31
+ const sequenceWallet = {
32
+ ...mockWallet,
33
+ walletKind: WalletKind.sequence,
34
+ };
35
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
36
+ wallet: sequenceWallet,
49
37
  isLoading: false,
50
- isError: true,
38
+ isError: false,
51
39
  });
52
-
53
- createListingModal$.open(defaultArgs);
54
-
55
- render(<CreateListingModal />);
56
-
57
- expect(
58
- await screen.findByText('Error loading item details'),
59
- ).toBeInTheDocument();
60
- });
61
-
62
- it('should render main form when data is loaded', async () => {
63
- createListingModal$.open(defaultArgs);
64
- render(<CreateListingModal />);
65
-
66
- // Check for the collection name in the token preview
67
- expect(await screen.findByText('Mock Collection')).toBeInTheDocument();
68
- });
69
-
70
- it('should reset store values when modal is closed and reopened', () => {
71
- // Open modal first time
72
- createListingModal$.open(defaultArgs);
73
-
74
- // Set some values in the store
75
- createListingModal$.listingPrice.amountRaw.set('1000000000000000000');
76
- createListingModal$.quantity.set('5');
77
-
78
- // Close modal
79
- createListingModal$.close();
80
-
81
- // Verify store is reset
82
- expect(createListingModal$.listingPrice.amountRaw.get()).toBe('0');
83
- expect(createListingModal$.quantity.get()).toBe('1');
84
-
85
- // Reopen modal
86
- createListingModal$.open({
87
- collectionAddress: '0x456',
88
- chainId: 1,
89
- collectibleId: '2',
40
+ vi.spyOn(
41
+ useGetTokenApprovalDataModule,
42
+ 'useGetTokenApprovalData',
43
+ ).mockReturnValue({
44
+ data: {
45
+ step: null,
46
+ },
47
+ isLoading: false,
48
+ isSuccess: true,
90
49
  });
91
50
 
92
- // Verify store has default values
93
- expect(createListingModal$.listingPrice.amountRaw.get()).toBe('0');
94
- expect(createListingModal$.quantity.get()).toBe('1');
95
- });
96
-
97
- it('should update state based on price input', async () => {
98
- createListingModal$.open(defaultArgs);
51
+ // Render the modal
52
+ const { result } = renderHook(() => useCreateListingModal());
53
+ result.current.show(defaultArgs);
99
54
 
100
55
  render(<CreateListingModal />);
101
56
 
102
- // Initial price should be 0
103
- expect(createListingModal$.listingPrice.amountRaw.get()).toBe('0');
104
-
105
- // Find and interact with price input using id
106
- const priceInput = await screen.findByRole('textbox', {
107
- name: /enter price/i,
108
- });
109
- expect(priceInput).toBeInTheDocument();
110
-
111
- fireEvent.change(priceInput, { target: { value: '1.5' } });
112
-
113
- // Wait for the state to update and verify it's not 0 anymore
57
+ // Wait for the component to update
114
58
  await waitFor(() => {
115
- expect(createListingModal$.listingPrice.amountRaw.get()).not.toBe('0');
59
+ // The Approve TOKEN button should not exist
60
+ expect(screen.queryByText('Approve TOKEN')).toBeNull();
61
+
62
+ // The List item for sale button should exist
63
+ expect(
64
+ screen.getByRole('button', { name: 'List item for sale' }),
65
+ ).toBeDefined();
116
66
  });
117
67
  });
118
68
 
119
- it('should show no currencies configured modal when currencies array is empty', async () => {
120
- // @ts-expect-error - Improve this mock
121
- vi.mocked(hooks.useCurrencies).mockReturnValue({
122
- data: [],
69
+ it('(non-sequence wallets) should show approve token button if there is an approval step, disable main button', async () => {
70
+ const nonSequenceWallet = {
71
+ ...mockWallet,
72
+ walletKind: 'unknown' as WalletKind,
73
+ };
74
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
75
+ wallet: nonSequenceWallet,
123
76
  isLoading: false,
124
77
  isError: false,
125
78
  });
79
+ vi.spyOn(
80
+ useGetTokenApprovalDataModule,
81
+ 'useGetTokenApprovalData',
82
+ ).mockReturnValue({
83
+ data: {
84
+ step: createMockStep(StepType.tokenApproval),
85
+ },
86
+ isLoading: false,
87
+ isSuccess: true,
88
+ });
126
89
 
127
- createListingModal$.open(defaultArgs);
90
+ // Render the modal
91
+ const { result } = renderHook(() => useCreateListingModal());
92
+ result.current.show(defaultArgs);
128
93
 
129
94
  render(<CreateListingModal />);
130
95
 
131
- expect(
132
- await screen.findByText(
133
- 'No currencies are configured for the marketplace, contact the marketplace owners',
134
- ),
135
- ).toBeInTheDocument();
96
+ await waitFor(() => {
97
+ expect(screen.getByText('Approve TOKEN')).toBeDefined();
98
+
99
+ expect(
100
+ screen.getByRole('button', { name: 'List item for sale' }),
101
+ ).toBeDefined();
102
+ expect(
103
+ screen.getByRole('button', { name: 'List item for sale' }),
104
+ ).toBeDisabled();
105
+ });
136
106
  });
137
107
  });