@0xsequence/marketplace-sdk 0.8.3 → 0.8.5

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 (97) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/{chunk-25CAMYCG.js → chunk-BB2PTJHI.js} +22 -20
  3. package/dist/chunk-BB2PTJHI.js.map +1 -0
  4. package/dist/{chunk-5ATGT5S4.js → chunk-EZFCQZHU.js} +14 -6
  5. package/dist/chunk-EZFCQZHU.js.map +1 -0
  6. package/dist/{chunk-DFI52A2E.js → chunk-KCLMSSPS.js} +364 -242
  7. package/dist/chunk-KCLMSSPS.js.map +1 -0
  8. package/dist/{chunk-XUNDLCEH.js → chunk-LDZZUYG7.js} +2 -2
  9. package/dist/{chunk-QTV77W42.js → chunk-SFSFIGHM.js} +45 -35
  10. package/dist/chunk-SFSFIGHM.js.map +1 -0
  11. package/dist/{chunk-FSJKN4YN.js → chunk-ZSCZLHKX.js} +194 -2
  12. package/dist/chunk-ZSCZLHKX.js.map +1 -0
  13. package/dist/{chunk-FH4TZRDV.js → chunk-ZVTG6US2.js} +2 -2
  14. package/dist/index.css +4 -4
  15. package/dist/index.css.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/{lowestListing-DUZ_nYml.d.ts → lowestListing-W7P4EkC3.d.ts} +34 -11
  18. package/dist/react/_internal/databeat/index.js +5 -5
  19. package/dist/react/_internal/index.d.ts +1 -1
  20. package/dist/react/_internal/index.js +3 -1
  21. package/dist/react/_internal/wagmi/index.d.ts +3 -2
  22. package/dist/react/_internal/wagmi/index.js +3 -1
  23. package/dist/react/hooks/index.d.ts +8 -5
  24. package/dist/react/hooks/index.js +6 -4
  25. package/dist/react/hooks/options/index.js +2 -2
  26. package/dist/react/index.d.ts +2 -2
  27. package/dist/react/index.js +9 -7
  28. package/dist/react/queries/index.d.ts +1 -1
  29. package/dist/react/queries/index.js +6 -2
  30. package/dist/react/ssr/index.js +1 -1
  31. package/dist/react/ui/components/collectible-card/index.d.ts +3 -2
  32. package/dist/react/ui/components/collectible-card/index.js +7 -7
  33. package/dist/react/ui/icons/index.js +1 -1
  34. package/dist/react/ui/index.js +7 -7
  35. package/dist/react/ui/modals/_internal/components/actionModal/index.js +5 -5
  36. package/dist/types/index.js +1 -1
  37. package/dist/utils/index.js +1 -1
  38. package/package.json +19 -19
  39. package/src/react/_internal/api/__mocks__/marketplace.msw.ts +35 -21
  40. package/src/react/_internal/wagmi/__tests__/create-config.test.ts +1 -11
  41. package/src/react/_internal/wagmi/get-connectors.ts +27 -24
  42. package/src/react/hooks/__tests__/useCancelTransactionSteps.test.tsx +4 -9
  43. package/src/react/hooks/__tests__/useGenerateCancelTransaction.test.tsx +5 -4
  44. package/src/react/hooks/__tests__/useGenerateListingTransaction.test.tsx +14 -10
  45. package/src/react/hooks/__tests__/useGenerateOfferTransaction.test.tsx +115 -65
  46. package/src/react/hooks/__tests__/useGenerateSellTransaction.test.tsx +10 -7
  47. package/src/react/hooks/__tests__/useInventory.test.tsx +294 -0
  48. package/src/react/hooks/index.ts +1 -0
  49. package/src/react/hooks/useAutoSelectFeeOption.tsx +10 -3
  50. package/src/react/hooks/useCancelOrder.tsx +1 -0
  51. package/src/react/hooks/useCancelTransactionSteps.tsx +18 -4
  52. package/src/react/hooks/useGenerateOfferTransaction.tsx +11 -1
  53. package/src/react/hooks/useInventory.tsx +15 -0
  54. package/src/react/hooks/util/optimisticCancelUpdates.ts +115 -0
  55. package/src/react/queries/index.ts +1 -0
  56. package/src/react/queries/inventory.ts +303 -0
  57. package/src/react/queries/listBalances.ts +1 -8
  58. package/src/react/queries/listCollectibles.ts +12 -3
  59. package/src/react/ui/components/_internals/action-button/__tests__/ActionButtonBody.test.tsx +27 -94
  60. package/src/react/ui/components/_internals/action-button/__tests__/NonOwnerActions.test.tsx +59 -0
  61. package/src/react/ui/components/_internals/action-button/__tests__/OwnerActions.test.tsx +73 -0
  62. package/src/react/ui/components/_internals/action-button/__tests__/useActionButtonLogic.test.tsx +77 -0
  63. package/src/react/ui/components/_internals/action-button/components/ActionButtonBody.tsx +3 -2
  64. package/src/react/ui/components/_internals/action-button/hooks/useActionButtonLogic.ts +4 -3
  65. package/src/react/ui/components/collectible-card/CollectibleAsset.tsx +1 -0
  66. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +18 -12
  67. package/src/react/ui/components/collectible-card/__tests__/CollectibleAsset.test.tsx +200 -0
  68. package/src/react/ui/components/collectible-card/__tests__/CollectibleCard.test.tsx +92 -123
  69. package/src/react/ui/components/collectible-card/__tests__/Footer.test.tsx +136 -0
  70. package/src/react/ui/modals/BuyModal/__tests__/Modal.test.tsx +2 -8
  71. package/src/react/ui/modals/CreateListingModal/__tests__/Modal.test.tsx +74 -104
  72. package/src/react/ui/modals/MakeOfferModal/__tests__/Modal.test.tsx +108 -78
  73. package/src/react/ui/modals/SellModal/__tests__/Modal.test.tsx +72 -135
  74. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.test.tsx +286 -0
  75. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +16 -4
  76. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/__tests__/index.test.tsx +35 -132
  77. package/src/react/ui/modals/_internal/components/floorPriceText/__tests__/FloorPriceText.test.tsx +199 -0
  78. package/src/react/ui/modals/_internal/components/priceInput/__tests__/PriceInput.test.tsx +55 -0
  79. package/src/react/ui/modals/_internal/components/priceInput/index.tsx +1 -1
  80. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/ActionButtons.test.tsx +72 -0
  81. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/BalanceIndicator.test.tsx +50 -0
  82. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/SelectWaasFeeOptions.test.tsx +193 -0
  83. package/src/react/ui/modals/_internal/components/switchChainModal/index.tsx +2 -2
  84. package/test/const.ts +24 -0
  85. package/test/test-utils.tsx +85 -47
  86. package/.changeset/flat-parks-clean.md +0 -8
  87. package/.changeset/red-buckets-deny.md +0 -6
  88. package/.changeset/seven-doors-taste.md +0 -5
  89. package/dist/chunk-25CAMYCG.js.map +0 -1
  90. package/dist/chunk-5ATGT5S4.js.map +0 -1
  91. package/dist/chunk-DFI52A2E.js.map +0 -1
  92. package/dist/chunk-FSJKN4YN.js.map +0 -1
  93. package/dist/chunk-QTV77W42.js.map +0 -1
  94. package/src/react/ui/components/_internals/action-button/__tests__/ActionButton.test.tsx +0 -107
  95. package/src/react/ui/modals/_internal/components/priceInput/__tests__/index.test.tsx +0 -164
  96. /package/dist/{chunk-XUNDLCEH.js.map → chunk-LDZZUYG7.js.map} +0 -0
  97. /package/dist/{chunk-FH4TZRDV.js.map → chunk-ZVTG6US2.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,133 +1,66 @@
1
1
  'use client';
2
2
 
3
- import * as kit from '@0xsequence/connect';
4
3
  import { fireEvent, render, screen } from '@test';
4
+ import { createMockWallet } from '@test/mocks/wallet';
5
5
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
- import * as wagmi from 'wagmi';
6
+ import * as walletModule from '../../../../../_internal/wallet/useWallet';
7
7
  import { ActionButtonBody } from '../components/ActionButtonBody';
8
8
  import { setPendingAction } from '../store';
9
9
  import { CollectibleCardAction } from '../types';
10
10
 
11
- // Mock the hooks
12
- vi.mock('wagmi', async () => {
13
- const actual = await vi.importActual('wagmi');
14
- return {
15
- ...actual,
16
- useAccount: vi.fn(),
17
- };
18
- });
19
-
20
- vi.mock('@0xsequence/connect', () => ({
21
- useOpenConnectModal: vi.fn(),
22
- }));
23
-
24
- // Mock the store
25
11
  vi.mock('../store', () => ({
26
12
  setPendingAction: vi.fn(),
27
13
  }));
28
14
 
29
- describe.skip('ActionButtonBody', () => {
15
+ describe('ActionButtonBody', () => {
16
+ const mockOnClick = vi.fn();
30
17
  const defaultProps = {
31
18
  label: 'Buy now' as const,
32
- tokenId: '1',
33
- onClick: vi.fn(),
19
+ tokenId: '123',
20
+ onClick: mockOnClick,
34
21
  action: CollectibleCardAction.BUY as CollectibleCardAction.BUY,
35
- disabled: false,
36
- loading: false,
37
22
  };
38
23
 
39
24
  beforeEach(() => {
40
25
  vi.clearAllMocks();
41
- // @ts-expect-error
42
- vi.mocked(wagmi.useAccount).mockReturnValue({ address: undefined });
43
- vi.mocked(kit.useOpenConnectModal).mockReturnValue({
44
- setOpenConnectModal: vi.fn(),
45
- openConnectModalState: false,
26
+
27
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
28
+ wallet: createMockWallet(),
29
+ isLoading: false,
30
+ isError: false,
46
31
  });
47
32
  });
48
33
 
49
- it('renders with correct label', () => {
34
+ it('executes onClick directly when user is connected', () => {
50
35
  render(<ActionButtonBody {...defaultProps} />);
51
- expect(screen.getByText('Buy now')).toBeInTheDocument();
52
- });
53
36
 
54
- it('calls onClick directly when user is connected', () => {
55
- // @ts-expect-error
56
- vi.mocked(wagmi.useAccount).mockReturnValue({ address: '0x123' });
57
- render(<ActionButtonBody {...defaultProps} />);
37
+ const button = screen.getByRole('button', { name: defaultProps.label });
38
+ fireEvent.click(button);
58
39
 
59
- fireEvent.click(screen.getByText('Buy now'));
60
- expect(defaultProps.onClick).toHaveBeenCalled();
40
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
61
41
  expect(setPendingAction).not.toHaveBeenCalled();
62
42
  });
63
43
 
64
44
  it('sets pending action and opens connect modal when user is not connected', () => {
65
- const setOpenConnectModal = vi.fn();
66
- // @ts-expect-error
67
- vi.mocked(kit.useOpenConnectModal).mockReturnValue({
68
- setOpenConnectModal,
45
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
46
+ wallet: {
47
+ ...createMockWallet(),
48
+ // @ts-expect-error - address is undefined for testing
49
+ address: undefined,
50
+ },
51
+ isLoading: false,
52
+ isError: false,
69
53
  });
70
-
71
54
  render(<ActionButtonBody {...defaultProps} />);
72
55
 
73
- fireEvent.click(screen.getByText('Buy now'));
56
+ const button = screen.getByRole('button', { name: defaultProps.label });
57
+ fireEvent.click(button);
74
58
 
59
+ expect(mockOnClick).not.toHaveBeenCalled();
75
60
  expect(setPendingAction).toHaveBeenCalledWith(
76
- CollectibleCardAction.BUY,
61
+ defaultProps.action,
77
62
  defaultProps.onClick,
78
63
  defaultProps.tokenId,
79
64
  );
80
- expect(setOpenConnectModal).toHaveBeenCalledWith(true);
81
- expect(defaultProps.onClick).not.toHaveBeenCalled();
82
- });
83
-
84
- it('prevents event propagation on click', () => {
85
- const preventDefault = vi.fn();
86
- const stopPropagation = vi.fn();
87
-
88
- render(<ActionButtonBody {...defaultProps} />);
89
-
90
- const button = screen.getByRole('button');
91
- const mockEvent = new MouseEvent('click', {
92
- bubbles: true,
93
- cancelable: true,
94
- });
95
- Object.defineProperties(mockEvent, {
96
- preventDefault: { value: preventDefault },
97
- stopPropagation: { value: stopPropagation },
98
- });
99
-
100
- fireEvent(button, mockEvent);
101
-
102
- expect(preventDefault).toHaveBeenCalled();
103
- expect(stopPropagation).toHaveBeenCalled();
104
- });
105
-
106
- it('renders with custom icon when provided', () => {
107
- const TestIcon = () => <span data-testid="test-icon">Icon</span>;
108
- render(<ActionButtonBody {...defaultProps} icon={TestIcon} />);
109
- expect(screen.getByTestId('test-icon')).toBeInTheDocument();
110
- });
111
-
112
- it('renders with different labels', () => {
113
- const labels = [
114
- 'Sell',
115
- 'Make an offer',
116
- 'Create listing',
117
- 'Transfer',
118
- ] as const;
119
-
120
- for (const label of labels) {
121
- render(<ActionButtonBody {...defaultProps} label={label} />);
122
- expect(screen.getByText(label)).toBeInTheDocument();
123
- }
124
- });
125
-
126
- it('calls onClick when clicked and not disabled', () => {
127
- // @ts-expect-error
128
- vi.mocked(wagmi.useAccount).mockReturnValue({ address: '0x123' });
129
- render(<ActionButtonBody {...defaultProps} />);
130
- fireEvent.click(screen.getByRole('button'));
131
- expect(defaultProps.onClick).toHaveBeenCalled();
132
65
  });
133
66
  });
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { render, screen } from '@test';
4
+ import { createMockWallet } from '@test/mocks/wallet';
5
+ import { zeroAddress } from 'viem';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { OrderSide } from '../../../../../_internal';
8
+ import { mockOrder } from '../../../../../_internal/api/__mocks__/marketplace.msw';
9
+ import * as walletModule from '../../../../../_internal/wallet/useWallet';
10
+ import { NonOwnerActions } from '../components/NonOwnerActions';
11
+ import { CollectibleCardAction } from '../types';
12
+
13
+ describe('NonOwnerActions', () => {
14
+ const defaultProps = {
15
+ action: CollectibleCardAction.BUY,
16
+ tokenId: '1',
17
+ collectionAddress: zeroAddress,
18
+ chainId: 1,
19
+ lowestListing: { ...mockOrder, side: OrderSide.listing },
20
+ };
21
+
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ vi.resetAllMocks();
25
+ vi.restoreAllMocks();
26
+
27
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
28
+ wallet: createMockWallet(),
29
+ isLoading: false,
30
+ isError: false,
31
+ });
32
+ });
33
+
34
+ it('renders Buy now button for BUY action', () => {
35
+ render(<NonOwnerActions {...defaultProps} />);
36
+ });
37
+
38
+ it('renders Make an offer button for OFFER action', () => {
39
+ render(
40
+ <NonOwnerActions
41
+ {...defaultProps}
42
+ action={CollectibleCardAction.OFFER}
43
+ />,
44
+ );
45
+ expect(screen.getByText('Make an offer')).toBeInTheDocument();
46
+ });
47
+
48
+ it('returns null for unsupported actions', () => {
49
+ const props = {
50
+ ...defaultProps,
51
+ action: CollectibleCardAction.LIST,
52
+ };
53
+
54
+ const { container } = render(
55
+ <NonOwnerActions {...(props as Parameters<typeof NonOwnerActions>[0])} />,
56
+ );
57
+ expect(container).toBeEmptyDOMElement();
58
+ });
59
+ });
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import { render, screen } from '@test';
4
+ import { createMockWallet } from '@test/mocks/wallet';
5
+ import { zeroAddress } from 'viem';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { OrderSide } from '../../../../../_internal';
8
+ import { mockOrder } from '../../../../../_internal/api/__mocks__/marketplace.msw';
9
+ import * as walletModule from '../../../../../_internal/wallet/useWallet';
10
+ import { OwnerActions } from '../components/OwnerActions';
11
+ import { CollectibleCardAction } from '../types';
12
+
13
+ describe('OwnerActions', () => {
14
+ const defaultProps = {
15
+ action: CollectibleCardAction.BUY,
16
+ tokenId: '1',
17
+ collectionAddress: zeroAddress,
18
+ chainId: 1,
19
+ lowestListing: { ...mockOrder, side: OrderSide.listing },
20
+ highestOffer: { ...mockOrder, side: OrderSide.offer },
21
+ };
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ vi.resetAllMocks();
26
+ vi.restoreAllMocks();
27
+
28
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
29
+ wallet: createMockWallet(),
30
+ isLoading: false,
31
+ isError: false,
32
+ });
33
+ });
34
+
35
+ it('renders Create listing button for LIST action', () => {
36
+ render(
37
+ <OwnerActions {...defaultProps} action={CollectibleCardAction.LIST} />,
38
+ );
39
+
40
+ expect(screen.getByText('Create listing')).toBeInTheDocument();
41
+ });
42
+
43
+ it('renders Transfer button for TRANSFER action', () => {
44
+ render(
45
+ <OwnerActions
46
+ {...defaultProps}
47
+ action={CollectibleCardAction.TRANSFER}
48
+ />,
49
+ );
50
+
51
+ expect(screen.getByText('Transfer')).toBeInTheDocument();
52
+ });
53
+
54
+ it('renders Sell button for SELL action', () => {
55
+ render(
56
+ <OwnerActions {...defaultProps} action={CollectibleCardAction.SELL} />,
57
+ );
58
+
59
+ expect(screen.getByText('Sell')).toBeInTheDocument();
60
+ });
61
+
62
+ it('returns null for unsupported actions', () => {
63
+ const props = {
64
+ ...defaultProps,
65
+ action: CollectibleCardAction.BUY,
66
+ };
67
+
68
+ const { container } = render(
69
+ <OwnerActions {...(props as Parameters<typeof OwnerActions>[0])} />,
70
+ );
71
+ expect(container).toBeEmptyDOMElement();
72
+ });
73
+ });