@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,136 @@
1
+ import { TEST_CURRENCY } from '@test/const';
2
+ import { render, screen } from '@test/test-utils';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { ContractType } from '../../../../_internal';
5
+ import { mockOrder } from '../../../../_internal/api/__mocks__/marketplace.msw';
6
+ import { Footer } from '../Footer';
7
+
8
+ const defaultProps = {
9
+ name: 'Test',
10
+ type: ContractType.ERC721,
11
+ decimals: 18,
12
+ onOfferClick: vi.fn(),
13
+ highestOffer: mockOrder,
14
+ lowestOffer: mockOrder,
15
+ lowestListingPriceAmount: '100',
16
+ lowestListingCurrency: TEST_CURRENCY,
17
+ balance: '100',
18
+ };
19
+
20
+ describe('Footer', () => {
21
+ it('Renders with basic props (name, type) correctly', () => {
22
+ render(<Footer {...defaultProps} />);
23
+
24
+ expect(screen.getByText('Test')).toBeInTheDocument();
25
+ expect(screen.getByText('0.0001 TEST')).toBeInTheDocument();
26
+
27
+ const notificationBell = screen.getByRole('button', {
28
+ name: 'Notification Bell',
29
+ });
30
+ expect(notificationBell).toBeInTheDocument();
31
+ });
32
+
33
+ it('Truncates long names appropriately based on presence of offers', () => {
34
+ const longName =
35
+ 'This is a very long collectible name that needs truncation';
36
+
37
+ // Test truncation with offer present (truncates to 13 chars + "...")
38
+ render(
39
+ <Footer {...defaultProps} name={longName} highestOffer={mockOrder} />,
40
+ );
41
+ expect(screen.getByText('This is a ver...')).toBeInTheDocument();
42
+
43
+ // Test truncation without offer present (truncates to 17 chars + "...")
44
+ render(
45
+ <Footer {...defaultProps} name={longName} highestOffer={undefined} />,
46
+ );
47
+ expect(screen.getByText('This is a very lo...')).toBeInTheDocument();
48
+
49
+ // Test short name with offer (no truncation needed)
50
+ render(
51
+ <Footer {...defaultProps} name="Short Name" highestOffer={mockOrder} />,
52
+ );
53
+ expect(screen.getByText('Short Name')).toBeInTheDocument();
54
+ });
55
+
56
+ it('Formats prices correctly for different scenarios (normal, overflow, underflow)', () => {
57
+ // Test normal price formatting
58
+ render(
59
+ <Footer
60
+ {...defaultProps}
61
+ lowestListingPriceAmount="1000000000000000000" // 1 TOKEN in wei
62
+ />,
63
+ );
64
+ expect(screen.getByText('1 TEST')).toBeInTheDocument();
65
+
66
+ // Test small number formatting (shows more decimals)
67
+ render(
68
+ <Footer
69
+ {...defaultProps}
70
+ lowestListingPriceAmount="5000000000000000" // 0.005 TOKEN in wei
71
+ />,
72
+ );
73
+ expect(screen.getByText('0.005 TEST')).toBeInTheDocument();
74
+
75
+ // Test underflow price (< 0.0001)
76
+ render(
77
+ <Footer
78
+ {...defaultProps}
79
+ lowestListingPriceAmount="10000000000000" // 0.00001 TOKEN in wei
80
+ />,
81
+ );
82
+ // Should display minimum price with chevron icon
83
+ expect(screen.getByText('0.0001 TEST')).toBeInTheDocument();
84
+
85
+ // Test overflow price (> 100,000,000)
86
+ render(
87
+ <Footer
88
+ {...defaultProps}
89
+ lowestListingPriceAmount="100000000000000000000000000" // 100M+ TOKEN in wei
90
+ />,
91
+ );
92
+ // Should display maximum price with chevron icon
93
+ expect(screen.getByText('100,000,000 TEST')).toBeInTheDocument();
94
+ });
95
+
96
+ it("Displays 'Not listed yet' when no listing price is provided", () => {
97
+ // Create props without listing price information
98
+ const propsWithoutListing = {
99
+ ...defaultProps,
100
+ lowestListingPriceAmount: undefined,
101
+ lowestListingCurrency: undefined,
102
+ };
103
+
104
+ render(<Footer {...propsWithoutListing} />);
105
+
106
+ // Verify "Not listed yet" text is displayed
107
+ expect(screen.getByText('Not listed yet')).toBeInTheDocument();
108
+ });
109
+
110
+ it('Shows proper token balance information for ERC721 vs ERC1155 tokens', () => {
111
+ // Test ERC721 token display
112
+ render(<Footer {...defaultProps} type={ContractType.ERC721} />);
113
+ expect(screen.getByText('ERC-721')).toBeInTheDocument();
114
+
115
+ // Test ERC1155 token display without balance
116
+ render(
117
+ <Footer
118
+ {...defaultProps}
119
+ type={ContractType.ERC1155}
120
+ balance={undefined}
121
+ />,
122
+ );
123
+ expect(screen.getByText('ERC-1155')).toBeInTheDocument();
124
+
125
+ // Test ERC1155 token display with balance
126
+ render(
127
+ <Footer
128
+ {...defaultProps}
129
+ type={ContractType.ERC1155}
130
+ balance="1000000000000000000"
131
+ decimals={18}
132
+ />,
133
+ );
134
+ expect(screen.getByText('Owned: 1')).toBeInTheDocument();
135
+ });
136
+ });
@@ -1,4 +1,4 @@
1
- import { WebSdkWrapper, cleanup, render, screen } from '@test';
1
+ import { cleanup, render, screen } from '@test';
2
2
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
4
  import { server } from '@test';
@@ -46,9 +46,7 @@ describe('BuyModal', () => {
46
46
  // },
47
47
  // });
48
48
  // render(
49
- // <WebSdkWrapper>
50
49
  // <BuyModal />
51
- // </WebSdkWrapper>
52
50
  // );
53
51
  // // Should show error modal
54
52
  // await waitFor(() => {
@@ -98,11 +96,7 @@ describe('BuyModal', () => {
98
96
  },
99
97
  });
100
98
 
101
- render(
102
- <WebSdkWrapper>
103
- <BuyModal />
104
- </WebSdkWrapper>,
105
- );
99
+ render(<BuyModal />);
106
100
 
107
101
  // Should show loading modal
108
102
  expect(screen.getByText('Loading Sequence Pay')).toBeInTheDocument();
@@ -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
  });
@@ -1,119 +1,149 @@
1
- import { cleanup, fireEvent, render, screen, waitFor } from '@test';
2
- import { zeroAddress } from 'viem';
1
+ import { cleanup, render, renderHook, waitFor } from '@test';
2
+ import { TEST_COLLECTIBLE } from '@test/const';
3
+ import { createMockWallet } from '@test/mocks/wallet';
3
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { useMakeOfferModal } from '..';
6
+ import { CurrencyStatus, WalletKind } from '../../../../_internal';
7
+ import * as walletModule from '../../../../_internal/wallet/useWallet';
4
8
  import { MakeOfferModal } from '../Modal';
5
9
  import { makeOfferModal$ } from '../store';
6
10
 
7
- // TODO: This should be moved to a shared test file
8
- vi.mock(import('../../../../hooks'), async (importOriginal) => {
9
- const actual = await importOriginal();
10
- return {
11
- ...actual,
12
- useCollectible: vi.fn(actual.useCollectible),
13
- useCollection: vi.fn(actual.useCollection),
14
- useCurrencies: vi.fn(actual.useCurrencies),
15
- useMarketplaceConfig: vi.fn(actual.useMarketplaceConfig),
16
- useLowestListing: vi.fn(actual.useLowestListing),
17
- };
18
- });
19
-
20
- vi.mock('@0xsequence/kit', () => ({
21
- useWaasFeeOptions: vi.fn().mockReturnValue([]),
22
- }));
23
-
24
11
  const defaultArgs = {
25
- collectionAddress: zeroAddress,
12
+ collectionAddress: TEST_COLLECTIBLE.collectionAddress,
13
+ chainId: TEST_COLLECTIBLE.chainId,
14
+ collectibleId: TEST_COLLECTIBLE.collectibleId,
15
+ };
16
+
17
+ // Mock currency object with all required properties
18
+ const mockCurrency = {
26
19
  chainId: 1,
27
- collectibleId: '1',
20
+ contractAddress: '0x123',
21
+ status: CurrencyStatus.active,
22
+ name: 'Test Token',
23
+ symbol: 'TEST',
24
+ decimals: 18,
25
+ imageUrl: 'https://example.com/test.png',
26
+ exchangeRate: 1,
27
+ defaultChainCurrency: false,
28
+ nativeCurrency: false,
29
+ createdAt: new Date().toISOString(),
30
+ updatedAt: new Date().toISOString(),
28
31
  };
29
32
 
30
- describe.skip('MakeOfferModal', () => {
33
+ describe('MakeOfferModal', () => {
34
+ const mockWallet = createMockWallet();
35
+
31
36
  beforeEach(() => {
32
37
  cleanup();
33
38
  // Reset all mocks
34
39
  vi.clearAllMocks();
35
40
  vi.resetAllMocks();
41
+ vi.restoreAllMocks();
36
42
  });
37
43
 
38
- it('should not render when modal is closed', () => {
39
- render(<MakeOfferModal />);
40
- expect(screen.queryByText('Make an offer')).toBeNull();
41
- });
42
-
43
- it('should render loading state', () => {
44
- makeOfferModal$.open(defaultArgs);
45
-
46
- render(<MakeOfferModal />);
47
- const loadingModal = screen.getByTestId('loading-modal');
48
- expect(loadingModal).toBeVisible();
49
- });
50
-
51
- it.skip('should render error state', async () => {
52
- // @ts-expect-error - TODO: Add a common mock object with the correct shape
53
- vi.mocked(hooks.useCollection).mockReturnValue({
54
- data: undefined,
44
+ it('should show main button if there is no approval step', async () => {
45
+ // Mock sequence wallet
46
+ const sequenceWallet = {
47
+ ...mockWallet,
48
+ walletKind: WalletKind.sequence,
49
+ };
50
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
51
+ wallet: sequenceWallet,
55
52
  isLoading: false,
56
- isError: true,
53
+ isError: false,
57
54
  });
58
55
 
59
- makeOfferModal$.open(defaultArgs);
56
+ // Render the modal
57
+ const { result } = renderHook(() => useMakeOfferModal());
58
+ result.current.show(defaultArgs);
60
59
 
61
- render(<MakeOfferModal />);
62
- const errorModal = await screen.findByTestId('error-modal');
63
- expect(errorModal).toBeVisible();
64
- });
60
+ const { queryByText } = render(<MakeOfferModal />);
65
61
 
66
- it.skip('should render main form when data is loaded', async () => {
67
- makeOfferModal$.open(defaultArgs);
68
-
69
- render(<MakeOfferModal />);
62
+ // Wait for the component to update
63
+ await waitFor(() => {
64
+ // The Approve TOKEN button should not exist
65
+ const approveButton = queryByText('Approve TOKEN');
66
+ expect(approveButton).toBeNull();
70
67
 
71
- expect(await screen.findByText('Enter price')).toBeInTheDocument();
68
+ const makeOfferButton = queryByText('Make offer');
69
+ expect(makeOfferButton).toBeDefined();
70
+ });
72
71
  });
73
72
 
74
- it.skip('should reset store values when modal is closed and reopened', () => {
75
- // Open modal first time
76
- makeOfferModal$.open(defaultArgs);
77
-
78
- // Set some values in the store
79
- makeOfferModal$.offerPrice.amountRaw.set('1000000000000000000');
80
- makeOfferModal$.expiry.set(new Date());
73
+ it('(non-sequence wallets) should show approve token button if there is an approval step, disable main button', async () => {
74
+ const nonSequenceWallet = {
75
+ ...mockWallet,
76
+ walletKind: 'unknown' as WalletKind,
77
+ };
78
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
79
+ wallet: nonSequenceWallet,
80
+ isLoading: false,
81
+ isError: false,
82
+ });
81
83
 
82
- // Close modal
83
- makeOfferModal$.close();
84
+ // Render the modal
85
+ const { result } = renderHook(() => useMakeOfferModal());
86
+ result.current.show(defaultArgs);
84
87
 
85
- // Verify store is reset
86
- expect(makeOfferModal$.offerPrice.amountRaw.get()).toBe('0');
87
- expect(makeOfferModal$.expiry.get()).toBeDefined();
88
+ const { getByText } = render(<MakeOfferModal />);
88
89
 
89
- // Reopen modal
90
- makeOfferModal$.open(defaultArgs);
90
+ await waitFor(() => {
91
+ // find the Approve TOKEN button
92
+ const approveButton = getByText('Approve TOKEN');
93
+ expect(approveButton).toBeDefined();
91
94
 
92
- // Verify store has default values
93
- expect(makeOfferModal$.offerPrice.amountRaw.get()).toBe('0');
94
- expect(makeOfferModal$.expiry.get()).toBeDefined();
95
+ // main button is disabled when approval step exists
96
+ const makeOfferButton = getByText('Make offer');
97
+ expect(makeOfferButton.closest('button')).toHaveAttribute('disabled');
98
+ });
95
99
  });
96
100
 
97
- it.skip('should update state based on price input', async () => {
101
+ it('should show/hide CTAs based on approval step existence in store', async () => {
102
+ // test case 1: approval.exist = true
98
103
  makeOfferModal$.open(defaultArgs);
104
+ makeOfferModal$.offerPrice.set({
105
+ amountRaw: '1000000000000000000',
106
+ currency: mockCurrency,
107
+ });
108
+ makeOfferModal$.offerPriceChanged.set(true);
109
+ makeOfferModal$.steps.approval.exist.set(true);
99
110
 
100
- render(<MakeOfferModal />);
111
+ const { getByText, unmount } = render(<MakeOfferModal />);
101
112
 
102
- // Initial price should be 0
103
- expect(makeOfferModal$.offerPrice.amountRaw.get()).toBe('0');
113
+ await waitFor(() => {
114
+ // Approve TOKEN button should be visible
115
+ const approveButton = getByText('Approve TOKEN');
116
+ expect(approveButton).toBeDefined();
117
+
118
+ // Make offer button should be disabled
119
+ const makeOfferButton = getByText('Make offer');
120
+ expect(makeOfferButton.closest('button')).toHaveAttribute('disabled');
121
+ });
104
122
 
105
- // Find and interact with price input
123
+ unmount();
124
+ cleanup();
106
125
 
107
- const priceInput = await screen.findByRole('textbox', {
108
- name: 'Enter price',
126
+ // test case 2: approval.exist = false
127
+ makeOfferModal$.open(defaultArgs);
128
+ makeOfferModal$.offerPrice.set({
129
+ amountRaw: '1000000000000000000',
130
+ currency: mockCurrency,
109
131
  });
110
- expect(priceInput).toBeInTheDocument();
132
+ makeOfferModal$.offerPriceChanged.set(true);
133
+ makeOfferModal$.steps.approval.exist.set(false);
111
134
 
112
- fireEvent.change(priceInput, { target: { value: '1.5' } });
135
+ const { queryByText, getByText: getByText2 } = render(<MakeOfferModal />);
113
136
 
114
- // Wait for the state to update and verify it's not 0 anymore
115
137
  await waitFor(() => {
116
- expect(makeOfferModal$.offerPrice.amountRaw.get()).not.toBe('0');
138
+ // Approve TOKEN button should not exist or be hidden
139
+ const approveButton = queryByText('Approve TOKEN');
140
+ expect(approveButton).toBeNull();
141
+
142
+ // Make offer button should be enabled
143
+ const makeOfferButton = getByText2('Make offer');
144
+ expect(makeOfferButton.closest('button')).not.toHaveAttribute('disabled');
117
145
  });
146
+
147
+ makeOfferModal$.close();
118
148
  });
119
149
  });