@0xsequence/marketplace-sdk 0.4.7 → 0.4.9

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 (114) hide show
  1. package/dist/{chunk-6R4G7J6Q.js → chunk-7HWJ4DUX.js} +29 -5
  2. package/dist/chunk-7HWJ4DUX.js.map +1 -0
  3. package/dist/{chunk-5UCKYAMR.js → chunk-BJLOO4NP.js} +647 -376
  4. package/dist/chunk-BJLOO4NP.js.map +1 -0
  5. package/dist/{chunk-2OJB35FS.js → chunk-BMWCIHCB.js} +5 -5
  6. package/dist/{chunk-2OJB35FS.js.map → chunk-BMWCIHCB.js.map} +1 -1
  7. package/dist/{chunk-R7GVMKMM.js → chunk-CUA4SGWT.js} +540 -18
  8. package/dist/chunk-CUA4SGWT.js.map +1 -0
  9. package/dist/{chunk-ZEH4JI2U.js → chunk-FCF57DZI.js} +7 -2
  10. package/dist/chunk-FCF57DZI.js.map +1 -0
  11. package/dist/{chunk-JEOUQFT3.js → chunk-TFCSNRD5.js} +4 -3
  12. package/dist/chunk-TFCSNRD5.js.map +1 -0
  13. package/dist/{chunk-AQT3BQ67.js → chunk-Y3KINNAU.js} +12 -5
  14. package/dist/chunk-Y3KINNAU.js.map +1 -0
  15. package/dist/{create-config-D5WqfUft.d.ts → create-config-CgYQDyMD.d.ts} +2 -2
  16. package/dist/index.css +31 -17
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +3 -3
  19. package/dist/{marketplace-config-C_fDWzz0.d.ts → marketplace-config-BP5-XnFG.d.ts} +1 -1
  20. package/dist/{marketplace.gen-B8S8fflj.d.ts → marketplace.gen-BzmWLP9L.d.ts} +14 -4
  21. package/dist/react/_internal/api/index.d.ts +2 -2
  22. package/dist/react/_internal/api/index.js +3 -1
  23. package/dist/react/_internal/index.d.ts +5 -5
  24. package/dist/react/_internal/index.js +4 -2
  25. package/dist/react/_internal/wagmi/index.d.ts +3 -3
  26. package/dist/react/_internal/wagmi/index.js +1 -1
  27. package/dist/react/hooks/index.d.ts +46 -5
  28. package/dist/react/hooks/index.js +12 -5
  29. package/dist/react/index.css +37 -17
  30. package/dist/react/index.css.map +1 -1
  31. package/dist/react/index.d.ts +6 -6
  32. package/dist/react/index.js +15 -8
  33. package/dist/react/ssr/index.js +6 -1
  34. package/dist/react/ssr/index.js.map +1 -1
  35. package/dist/react/ui/components/collectible-card/index.css +37 -17
  36. package/dist/react/ui/components/collectible-card/index.css.map +1 -1
  37. package/dist/react/ui/components/collectible-card/index.d.ts +27 -4
  38. package/dist/react/ui/components/collectible-card/index.js +7 -8
  39. package/dist/react/ui/index.css +37 -17
  40. package/dist/react/ui/index.css.map +1 -1
  41. package/dist/react/ui/index.d.ts +3 -3
  42. package/dist/react/ui/index.js +7 -8
  43. package/dist/react/ui/modals/_internal/components/actionModal/index.d.ts +3 -3
  44. package/dist/react/ui/modals/_internal/components/actionModal/index.js +5 -5
  45. package/dist/{sdk-config-BXVH8PS2.d.ts → sdk-config-LbbmA85k.d.ts} +27 -7
  46. package/dist/{services-CdXAIjt1.d.ts → services-DW26ougH.d.ts} +1 -1
  47. package/dist/styles/index.css +31 -17
  48. package/dist/styles/index.css.map +1 -1
  49. package/dist/styles/index.d.ts +2 -2
  50. package/dist/styles/index.js +1 -1
  51. package/dist/types/index.d.ts +3 -3
  52. package/dist/types/index.js +2 -2
  53. package/dist/{types-eX4P9xju.d.ts → types-DhTZWw1U.d.ts} +2 -2
  54. package/dist/utils/index.d.ts +3 -3
  55. package/dist/utils/index.js +3 -3
  56. package/package.json +3 -2
  57. package/src/react/__tests__/provider.test.tsx +75 -0
  58. package/src/react/_internal/api/__mocks__/marketplace.msw.ts +4 -0
  59. package/src/react/_internal/api/marketplace.gen.ts +42 -7
  60. package/src/react/_internal/wagmi/__tests__/create-config.test.ts +196 -0
  61. package/src/react/_internal/wagmi/create-config.ts +9 -1
  62. package/src/react/_internal/wallet/useWallet.ts +30 -46
  63. package/src/react/_internal/wallet/wallet.ts +52 -6
  64. package/src/react/hooks/index.ts +2 -0
  65. package/src/react/hooks/options/__mocks__/marketplaceConfig.msw.ts +10 -1
  66. package/src/react/hooks/useCollectionDetails.tsx +35 -0
  67. package/src/react/hooks/useCollectionDetailsPolling.tsx +60 -0
  68. package/src/react/provider.tsx +5 -0
  69. package/src/react/ui/components/_internals/action-button/ActionButton.tsx +36 -118
  70. package/src/react/ui/components/_internals/action-button/components/ActionButtonBody.tsx +52 -0
  71. package/src/react/ui/components/_internals/action-button/components/NonOwnerActions.tsx +72 -0
  72. package/src/react/ui/components/_internals/action-button/components/OwnerActions.tsx +81 -0
  73. package/src/react/ui/components/_internals/action-button/hooks/useActionButtonLogic.ts +93 -0
  74. package/src/react/ui/components/_internals/action-button/store.ts +47 -0
  75. package/src/react/ui/components/_internals/action-button/styles.css.ts +8 -0
  76. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +34 -14
  77. package/src/react/ui/components/collectible-card/Footer.tsx +9 -9
  78. package/src/react/ui/components/collectible-card/styles.css.ts +44 -31
  79. package/src/react/ui/icons/CartIcon.tsx +46 -0
  80. package/src/react/ui/modals/BuyModal/Modal.tsx +11 -4
  81. package/src/react/ui/modals/BuyModal/__tests__/Modal.test.tsx +253 -0
  82. package/src/react/ui/modals/BuyModal/__tests__/store.test.ts +100 -0
  83. package/src/react/ui/modals/BuyModal/hooks/__tests__/useBuyCollectable.test.tsx +402 -0
  84. package/src/react/ui/modals/BuyModal/hooks/__tests__/useCheckoutOptions.test.tsx +267 -0
  85. package/src/react/ui/modals/BuyModal/hooks/__tests__/useFees.test.tsx +166 -0
  86. package/src/react/ui/modals/BuyModal/hooks/__tests__/useLoadData.test.tsx +209 -0
  87. package/src/react/ui/modals/BuyModal/hooks/useCheckoutOptions.ts +19 -17
  88. package/src/react/ui/modals/BuyModal/hooks/useLoadData.ts +9 -7
  89. package/src/react/ui/modals/BuyModal/modals/CheckoutModal.tsx +7 -0
  90. package/src/react/ui/modals/BuyModal/modals/Modal1155.tsx +36 -18
  91. package/src/react/ui/modals/BuyModal/modals/__tests__/CheckoutModal.test.tsx +162 -0
  92. package/src/react/ui/modals/BuyModal/modals/__tests__/Modal1155.test.tsx +243 -0
  93. package/src/react/ui/modals/BuyModal/store.ts +11 -10
  94. package/src/react/ui/modals/CreateListingModal/Modal.tsx +26 -3
  95. package/src/react/ui/modals/CreateListingModal/__tests__/Modal.test.tsx +141 -29
  96. package/src/react/ui/modals/SuccessfulPurchaseModal/__tests__/Modal.test.tsx +145 -0
  97. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +20 -11
  98. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/__tests__/index.test.tsx +13 -58
  99. package/src/react/ui/modals/_internal/components/priceInput/__tests__/index.test.tsx +2 -0
  100. package/src/react/ui/modals/_internal/components/transactionStatusModal/__tests__/TransactionStatusModal.test.tsx +18 -19
  101. package/src/react/ui/modals/_internal/components/transactionStatusModal/__tests__/utils.test.ts +2 -0
  102. package/src/react/ui/modals/_internal/components/transactionStatusModal/hooks/useTransactionStatus.ts +62 -0
  103. package/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +53 -100
  104. package/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +2 -10
  105. package/src/utils/_internal/error/config.ts +16 -0
  106. package/tsconfig.tsbuildinfo +1 -1
  107. package/dist/chunk-5UCKYAMR.js.map +0 -1
  108. package/dist/chunk-6R4G7J6Q.js.map +0 -1
  109. package/dist/chunk-AQT3BQ67.js.map +0 -1
  110. package/dist/chunk-FWN2MCLI.js +0 -425
  111. package/dist/chunk-FWN2MCLI.js.map +0 -1
  112. package/dist/chunk-JEOUQFT3.js.map +0 -1
  113. package/dist/chunk-R7GVMKMM.js.map +0 -1
  114. package/dist/chunk-ZEH4JI2U.js.map +0 -1
@@ -4,19 +4,21 @@ import { useCollectible, useCollection } from '../../../../hooks';
4
4
 
5
5
  import { useCheckoutOptions } from './useCheckoutOptions';
6
6
 
7
+ export type UseLoadDataProps = {
8
+ chainId: number;
9
+ collectionAddress: Hex;
10
+ collectibleId: string;
11
+ orderId: string;
12
+ marketplace: MarketplaceKind;
13
+ };
14
+
7
15
  export const useLoadData = ({
8
16
  chainId,
9
17
  collectionAddress,
10
18
  collectibleId,
11
19
  orderId,
12
20
  marketplace,
13
- }: {
14
- chainId: number;
15
- collectionAddress: Hex;
16
- collectibleId: string;
17
- orderId: string;
18
- marketplace: MarketplaceKind;
19
- }) => {
21
+ }: UseLoadDataProps) => {
20
22
  const {
21
23
  data: collection,
22
24
  isLoading: collectionLoading,
@@ -5,6 +5,7 @@ import type {
5
5
  MarketplaceKind,
6
6
  TokenMetadata,
7
7
  } from '../../../../_internal/api/marketplace.gen';
8
+ import { buyModal$ } from '../store';
8
9
 
9
10
  export interface BuyInput {
10
11
  orderId: string;
@@ -22,6 +23,12 @@ export interface CheckoutModalProps {
22
23
  export function CheckoutModal({ buy, collectable, order }: CheckoutModalProps) {
23
24
  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
24
25
  useEffect(() => {
26
+ if (
27
+ buyModal$.state.checkoutModalIsLoading.get() ||
28
+ buyModal$.state.checkoutModalLoaded.get()
29
+ )
30
+ return;
31
+
25
32
  const executeBuy = () => {
26
33
  buy({
27
34
  orderId: order.orderId,
@@ -1,9 +1,8 @@
1
1
  import { Box, Text, TokenImage } from '@0xsequence/design-system';
2
2
  import { observer } from '@legendapp/state/react';
3
- import type { Address, Hex } from 'viem';
3
+ import type { Hex } from 'viem';
4
4
  import { formatUnits, parseUnits } from 'viem';
5
- import { useCurrencies } from '../../../../hooks';
6
- import { useCurrencyOptions } from '../../../../hooks/useCurrencyOptions';
5
+ import { useCurrency } from '../../../../hooks';
7
6
  import QuantityInput from '../../_internal/components/quantityInput';
8
7
  import { ActionModal } from '../../_internal/components/actionModal';
9
8
  import { buyModal$ } from '../store';
@@ -17,23 +16,20 @@ interface ERC1155QuantityModalProps extends CheckoutModalProps {
17
16
 
18
17
  export const ERC1155QuantityModal = observer(
19
18
  ({ buy, collectable, order }: ERC1155QuantityModalProps) => {
20
- const currencyOptions = useCurrencyOptions({
21
- collectionAddress: order.collectionContractAddress as Address,
22
- });
23
- const { data: currencies } = useCurrencies({
19
+ const { data: currency, isLoading: isCurrencyLoading } = useCurrency({
24
20
  chainId: order.chainId,
25
- currencyOptions,
21
+ currencyAddress: order.priceCurrencyAddress,
26
22
  });
27
23
 
28
- const currency = currencies?.find(
29
- (currency) => currency.contractAddress === order.priceCurrencyAddress,
30
- );
31
-
32
24
  const quantity = Number(buyModal$.state.quantity.get());
33
25
  const pricePerToken = order.priceAmount;
34
26
  const totalPrice = (BigInt(quantity) * BigInt(pricePerToken)).toString();
35
27
 
36
- if (buyModal$.state.checkoutModalLoaded.get()) {
28
+ if (
29
+ buyModal$.state.checkoutModalLoaded.get() &&
30
+ buyModal$.isOpen.get() &&
31
+ buyModal$.state.checkoutModalIsLoading.get()
32
+ ) {
37
33
  return null;
38
34
  }
39
35
 
@@ -72,14 +68,36 @@ export const ERC1155QuantityModal = observer(
72
68
  maxQuantity={order.quantityRemaining}
73
69
  />
74
70
  <Box display="flex" justifyContent="space-between">
75
- <Text color="text50" fontSize="small">
71
+ <Text color="text50" fontSize="small" fontFamily="body">
76
72
  Total Price
77
73
  </Text>
78
74
  <Box display="flex" alignItems="center" gap="2">
79
- <TokenImage src={currency?.imageUrl} size="xs" />
80
- <Text color="text100" fontSize="small" fontWeight="bold">
81
- {formatUnits(BigInt(totalPrice), currency?.decimals || 0)}
82
- </Text>
75
+ {isCurrencyLoading || !currency ? (
76
+ <Box display="flex" alignItems="center" gap="2">
77
+ <Text color="text50" fontSize="small" fontFamily="body">
78
+ Loading...
79
+ </Text>
80
+ </Box>
81
+ ) : (
82
+ <>
83
+ {currency.imageUrl && (
84
+ <TokenImage src={currency.imageUrl} size="xs" />
85
+ )}
86
+
87
+ <Text
88
+ color="text100"
89
+ fontSize="small"
90
+ fontWeight="bold"
91
+ fontFamily="body"
92
+ >
93
+ {formatUnits(BigInt(totalPrice), currency.decimals || 0)}
94
+ </Text>
95
+
96
+ <Text color="text80" fontSize="small" fontFamily="body">
97
+ {currency?.symbol}
98
+ </Text>
99
+ </>
100
+ )}
83
101
  </Box>
84
102
  </Box>
85
103
  </Box>
@@ -0,0 +1,162 @@
1
+ import { render, waitFor } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { CheckoutModal } from '../CheckoutModal';
4
+ import { parseUnits } from 'viem';
5
+ import type { Order, TokenMetadata } from '../../../../../_internal';
6
+ import {
7
+ MarketplaceKind,
8
+ OrderSide,
9
+ OrderStatus,
10
+ } from '../../../../../_internal';
11
+
12
+ describe('CheckoutModal', () => {
13
+ const mockBuy = vi.fn();
14
+
15
+ const defaultOrder: Order = {
16
+ orderId: '123',
17
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
18
+ priceAmount: '1000000000000000000',
19
+ priceCurrencyAddress: '0x0',
20
+ quantityRemaining: '1',
21
+ priceUSDFormatted: '$1,800.00',
22
+ createdAt: new Date().toISOString(),
23
+ side: OrderSide.listing,
24
+ status: OrderStatus.active,
25
+ chainId: 1,
26
+ originName: 'test',
27
+ collectionContractAddress: '0x1234567890123456789012345678901234567890',
28
+ tokenId: '1',
29
+ createdBy: '0x1234567890123456789012345678901234567890',
30
+ priceAmountFormatted: '1.0',
31
+ priceAmountNet: '950000000000000000',
32
+ priceAmountNetFormatted: '0.95',
33
+ priceDecimals: 18,
34
+ priceUSD: 1800.0,
35
+ quantityInitial: '1',
36
+ quantityInitialFormatted: '1',
37
+ quantityRemainingFormatted: '1',
38
+ quantityAvailable: '1',
39
+ quantityAvailableFormatted: '1',
40
+ quantityDecimals: 0,
41
+ feeBps: 500,
42
+ feeBreakdown: [],
43
+ validFrom: new Date().toISOString(),
44
+ validUntil: new Date(Date.now() + 86400000).toISOString(),
45
+ blockNumber: 1234567,
46
+ updatedAt: new Date().toISOString(),
47
+ };
48
+
49
+ const defaultCollectable: TokenMetadata = {
50
+ tokenId: '1',
51
+ name: 'Test NFT',
52
+ description: 'Test Description',
53
+ image: 'https://test.com/image.png',
54
+ attributes: [],
55
+ decimals: 0,
56
+ };
57
+
58
+ beforeEach(() => {
59
+ vi.clearAllMocks();
60
+ });
61
+
62
+ it('should auto-execute buy function when mounted', async () => {
63
+ render(
64
+ <CheckoutModal
65
+ buy={mockBuy}
66
+ order={defaultOrder}
67
+ collectable={defaultCollectable}
68
+ />,
69
+ );
70
+
71
+ await waitFor(() => {
72
+ expect(mockBuy).toHaveBeenCalledTimes(1);
73
+ });
74
+ });
75
+
76
+ it('should execute buy even when loading (current implementation)', async () => {
77
+ render(
78
+ <CheckoutModal
79
+ buy={mockBuy}
80
+ order={defaultOrder}
81
+ collectable={defaultCollectable}
82
+ isLoading={true}
83
+ />,
84
+ );
85
+
86
+ await waitFor(() => {
87
+ expect(mockBuy).toHaveBeenCalledTimes(1);
88
+ expect(mockBuy).toHaveBeenCalledWith({
89
+ orderId: defaultOrder.orderId,
90
+ collectableDecimals: defaultCollectable.decimals || 0,
91
+ quantity: '1',
92
+ marketplace: defaultOrder.marketplace,
93
+ });
94
+ });
95
+ });
96
+
97
+ it('should call buy with correct parameters', async () => {
98
+ render(
99
+ <CheckoutModal
100
+ buy={mockBuy}
101
+ order={defaultOrder}
102
+ collectable={defaultCollectable}
103
+ />,
104
+ );
105
+
106
+ await waitFor(() => {
107
+ expect(mockBuy).toHaveBeenCalledWith({
108
+ orderId: defaultOrder.orderId,
109
+ collectableDecimals: defaultCollectable.decimals || 0,
110
+ quantity: '1',
111
+ marketplace: defaultOrder.marketplace,
112
+ });
113
+ });
114
+ });
115
+
116
+ it('should handle decimals correctly', async () => {
117
+ const collectableWithDecimals: TokenMetadata = {
118
+ ...defaultCollectable,
119
+ decimals: 18,
120
+ };
121
+
122
+ render(
123
+ <CheckoutModal
124
+ buy={mockBuy}
125
+ order={defaultOrder}
126
+ collectable={collectableWithDecimals}
127
+ />,
128
+ );
129
+
130
+ await waitFor(() => {
131
+ expect(mockBuy).toHaveBeenCalledWith({
132
+ orderId: defaultOrder.orderId,
133
+ collectableDecimals: collectableWithDecimals.decimals,
134
+ quantity: parseUnits('1', 18).toString(),
135
+ marketplace: defaultOrder.marketplace,
136
+ });
137
+ });
138
+
139
+ // Test with different decimal values
140
+ const collectableWithCustomDecimals: TokenMetadata = {
141
+ ...defaultCollectable,
142
+ decimals: 6,
143
+ };
144
+
145
+ render(
146
+ <CheckoutModal
147
+ buy={mockBuy}
148
+ order={defaultOrder}
149
+ collectable={collectableWithCustomDecimals}
150
+ />,
151
+ );
152
+
153
+ await waitFor(() => {
154
+ expect(mockBuy).toHaveBeenCalledWith({
155
+ orderId: defaultOrder.orderId,
156
+ collectableDecimals: collectableWithCustomDecimals.decimals,
157
+ quantity: parseUnits('1', 6).toString(),
158
+ marketplace: defaultOrder.marketplace,
159
+ });
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,243 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import {
3
+ render,
4
+ screen,
5
+ waitFor,
6
+ within,
7
+ fireEvent,
8
+ act,
9
+ cleanup,
10
+ } from '../../../../../_internal/test-utils';
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { ERC1155QuantityModal } from '../Modal1155';
13
+ import { buyModal$ } from '../../store';
14
+ import { MarketplaceKind } from '../../../../../_internal';
15
+ import type { Order, TokenMetadata } from '../../../../../_internal';
16
+
17
+ describe('ERC1155QuantityModal', () => {
18
+ const mockOrder = {
19
+ orderId: '1',
20
+ priceAmount: '1000000000000000000', // 1 ETH in wei
21
+ priceCurrencyAddress: '0x0',
22
+ quantityRemaining: '10',
23
+ quantityDecimals: 0,
24
+ chainId: 1,
25
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
26
+ createdAt: new Date().toISOString(),
27
+ quantityAvailableFormatted: '1',
28
+ } as Order;
29
+
30
+ const mockCollectable: TokenMetadata = {
31
+ decimals: 0,
32
+ tokenId: '1',
33
+ name: 'Test Token',
34
+ description: 'Test Description',
35
+ image: 'https://example.com/image.png',
36
+ attributes: [],
37
+ };
38
+
39
+ const mockBuy = vi.fn();
40
+
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+
44
+ // Reset the modal state
45
+ buyModal$.close();
46
+ buyModal$.state.checkoutModalLoaded.set(false);
47
+ buyModal$.state.checkoutModalIsLoading.set(false);
48
+ buyModal$.state.invalidQuantity.set(false);
49
+ buyModal$.state.quantity.set('1');
50
+ });
51
+
52
+ afterEach(() => {
53
+ buyModal$.close();
54
+ cleanup();
55
+ vi.clearAllMocks();
56
+ });
57
+
58
+ it('should render quantity input correctly', async () => {
59
+ // Open the modal with initial state
60
+ buyModal$.open({
61
+ order: mockOrder,
62
+ callbacks: {},
63
+ chainId: '1',
64
+ collectionAddress: '0x123',
65
+ tokenId: '1',
66
+ });
67
+
68
+ render(
69
+ <ERC1155QuantityModal
70
+ buy={mockBuy}
71
+ collectable={mockCollectable}
72
+ order={mockOrder}
73
+ chainId="1"
74
+ collectionAddress="0x123"
75
+ collectibleId="1"
76
+ />,
77
+ );
78
+
79
+ // Verify modal title
80
+ expect(screen.getByText('Select Quantity')).toBeInTheDocument();
81
+
82
+ // Verify quantity input is present with initial value
83
+ const quantityInput = screen.getByRole('textbox', {
84
+ name: /enter quantity/i,
85
+ });
86
+ expect(quantityInput).toBeInTheDocument();
87
+ expect(quantityInput).toHaveValue('1');
88
+
89
+ // Verify total price calculation (1 ETH)
90
+ const priceLabels = screen.getAllByText('Total Price');
91
+ const priceContainer = priceLabels[0].parentElement;
92
+ if (!priceContainer) throw new Error('Price container not found');
93
+ expect(within(priceContainer).getByText('1')).toBeInTheDocument();
94
+ expect(within(priceContainer).getByText('ETH')).toBeInTheDocument();
95
+
96
+ // Verify buy button
97
+ const buyButton = screen.getByRole('button', { name: /buy now/i });
98
+ expect(buyButton).toBeInTheDocument();
99
+ expect(buyButton).not.toBeDisabled();
100
+
101
+ // Verify token image
102
+ const tokenImage = screen.getByRole('img', { name: '' });
103
+ expect(tokenImage).toBeInTheDocument();
104
+ expect(tokenImage).toHaveAttribute('src', 'https://example.com/eth.png');
105
+
106
+ // Ensure loading state is false and button is enabled before clicking
107
+ buyModal$.state.checkoutModalIsLoading.set(false);
108
+ buyModal$.state.invalidQuantity.set(false);
109
+ });
110
+
111
+ it('should update total price when quantity changes', async () => {
112
+ buyModal$.open({
113
+ order: mockOrder,
114
+ callbacks: {},
115
+ chainId: '1',
116
+ collectionAddress: '0x123',
117
+ tokenId: '1',
118
+ });
119
+
120
+ render(
121
+ <ERC1155QuantityModal
122
+ buy={mockBuy}
123
+ collectable={mockCollectable}
124
+ order={mockOrder}
125
+ chainId="1"
126
+ collectionAddress="0x123"
127
+ collectibleId="1"
128
+ />,
129
+ );
130
+
131
+ const quantityInput = screen.getByRole('textbox', {
132
+ name: /enter quantity/i,
133
+ });
134
+
135
+ // Change quantity to 2
136
+ await act(async () => {
137
+ fireEvent.change(quantityInput, { target: { value: '2' } });
138
+ });
139
+
140
+ // Verify total price updates to 2 ETH
141
+ const priceLabels = screen.getAllByText('Total Price');
142
+ const priceContainer = priceLabels[0].parentElement;
143
+ if (!priceContainer) throw new Error('Price container not found');
144
+
145
+ await waitFor(() => {
146
+ expect(within(priceContainer).getByText('2')).toBeInTheDocument();
147
+ });
148
+ });
149
+
150
+ it('should disable buy button when quantity is invalid', async () => {
151
+ buyModal$.open({
152
+ order: mockOrder,
153
+ callbacks: {},
154
+ chainId: '1',
155
+ collectionAddress: '0x123',
156
+ tokenId: '1',
157
+ });
158
+
159
+ // Set invalid quantity and loading state
160
+ buyModal$.state.invalidQuantity.set(true);
161
+ buyModal$.state.checkoutModalIsLoading.set(true);
162
+
163
+ render(
164
+ <ERC1155QuantityModal
165
+ buy={mockBuy}
166
+ collectable={mockCollectable}
167
+ order={mockOrder}
168
+ chainId="1"
169
+ collectionAddress="0x123"
170
+ collectibleId="1"
171
+ />,
172
+ );
173
+
174
+ const buyButton = screen.getByRole('button', { name: /buy now/i });
175
+
176
+ // Wait for the button to be disabled
177
+ await waitFor(() => {
178
+ expect(buyButton).toBeDisabled();
179
+ });
180
+
181
+ // Click should not trigger buy function
182
+ fireEvent.click(buyButton);
183
+ expect(mockBuy).not.toHaveBeenCalled();
184
+ });
185
+
186
+ it('should calculate total price correctly', async () => {
187
+ // Open the modal with initial state
188
+ buyModal$.open({
189
+ order: mockOrder,
190
+ callbacks: {},
191
+ chainId: '1',
192
+ collectionAddress: '0x123',
193
+ tokenId: '1',
194
+ });
195
+
196
+ render(
197
+ <ERC1155QuantityModal
198
+ buy={mockBuy}
199
+ collectable={mockCollectable}
200
+ order={mockOrder}
201
+ chainId="1"
202
+ collectionAddress="0x123"
203
+ collectibleId="1"
204
+ />,
205
+ );
206
+
207
+ // Wait for modal content to be fully loaded
208
+ await waitFor(() => {
209
+ expect(screen.getByText('Select Quantity')).toBeInTheDocument();
210
+ expect(
211
+ screen.getByRole('textbox', { name: /enter quantity/i }),
212
+ ).toBeInTheDocument();
213
+ expect(screen.getAllByText('Total Price')[0]).toBeInTheDocument();
214
+ });
215
+
216
+ // Verify initial total price (1 ETH)
217
+ const priceLabels = screen.getAllByText('Total Price');
218
+ const priceContainer = priceLabels[0].parentElement;
219
+ if (!priceContainer) throw new Error('Price container not found');
220
+
221
+ await waitFor(() => {
222
+ expect(within(priceContainer).getByText('1')).toBeInTheDocument();
223
+ expect(within(priceContainer).getByText('ETH')).toBeInTheDocument();
224
+ });
225
+
226
+ // Change quantity to 3
227
+ const quantityInput = screen.getByRole('textbox', {
228
+ name: /enter quantity/i,
229
+ });
230
+ await act(async () => {
231
+ fireEvent.change(quantityInput, { target: { value: '3' } });
232
+ });
233
+
234
+ // Verify total price updates to 3 ETH
235
+ await waitFor(() => {
236
+ expect(within(priceContainer).getByText('3')).toBeInTheDocument();
237
+ });
238
+
239
+ // Verify modal is in a valid state
240
+ expect(buyModal$.state.checkoutModalIsLoading.get()).toBe(false);
241
+ expect(buyModal$.state.invalidQuantity.get()).toBe(false);
242
+ });
243
+ });
@@ -3,6 +3,14 @@ import type { ShowBuyModalArgs } from '.';
3
3
  import type { Order } from '../../../_internal';
4
4
  import type { ModalCallbacks } from '../_internal/types';
5
5
 
6
+ const buyState = {
7
+ order: undefined as unknown as Order,
8
+ quantity: '1',
9
+ invalidQuantity: false,
10
+ checkoutModalIsLoading: false,
11
+ checkoutModalLoaded: false,
12
+ } as const;
13
+
6
14
  export interface BuyModalState {
7
15
  isOpen: boolean;
8
16
  open: (
@@ -15,7 +23,6 @@ export interface BuyModalState {
15
23
  state: {
16
24
  order: Order;
17
25
  quantity: string;
18
- modalId: number;
19
26
  invalidQuantity: boolean;
20
27
  checkoutModalIsLoading: boolean;
21
28
  checkoutModalLoaded: boolean;
@@ -38,7 +45,6 @@ export const initialState: BuyModalState = {
38
45
  buyModal$.state.set({
39
46
  quantity: args.order.quantityAvailableFormatted,
40
47
  order: args.order,
41
- modalId: buyModal$.state.modalId.get() + 1,
42
48
  invalidQuantity: false,
43
49
  checkoutModalIsLoading: false,
44
50
  checkoutModalLoaded: false,
@@ -48,15 +54,10 @@ export const initialState: BuyModalState = {
48
54
  },
49
55
  close: () => {
50
56
  buyModal$.isOpen.set(false);
57
+ buyModal$.callbacks.set(undefined);
58
+ buyModal$.state.set(buyState);
51
59
  },
52
- state: {
53
- order: undefined as unknown as Order,
54
- quantity: '1',
55
- modalId: 0,
56
- invalidQuantity: false,
57
- checkoutModalIsLoading: false,
58
- checkoutModalLoaded: false,
59
- },
60
+ state: buyState,
60
61
  setCheckoutModalIsLoading: (isLoading: boolean) => {
61
62
  buyModal$.state.checkoutModalIsLoading.set(isLoading);
62
63
  },
@@ -7,6 +7,8 @@ import {
7
7
  useBalanceOfCollectible,
8
8
  useCollectible,
9
9
  useCollection,
10
+ useCurrencies,
11
+ useCurrencyOptions,
10
12
  } from '../../../hooks';
11
13
  import {
12
14
  ActionModal,
@@ -49,7 +51,16 @@ const Modal = observer(() => {
49
51
  collectionAddress,
50
52
  collectibleId,
51
53
  });
52
-
54
+ const currencyOptions = useCurrencyOptions({ collectionAddress });
55
+ const {
56
+ data: currencies,
57
+ isLoading: currenciesLoading,
58
+ isError: currenciesIsError,
59
+ } = useCurrencies({
60
+ chainId,
61
+ currencyOptions,
62
+ includeNativeCurrency: true,
63
+ });
53
64
  const {
54
65
  data: collection,
55
66
  isLoading: collectionIsLoading,
@@ -91,7 +102,7 @@ const Modal = observer(() => {
91
102
  steps$: steps$,
92
103
  });
93
104
 
94
- if (collectableIsLoading || collectionIsLoading) {
105
+ if (collectableIsLoading || collectionIsLoading || currenciesLoading) {
95
106
  return (
96
107
  <LoadingModal
97
108
  isOpen={createListingModal$.isOpen.get()}
@@ -102,13 +113,25 @@ const Modal = observer(() => {
102
113
  );
103
114
  }
104
115
 
105
- if (collectableIsError || collectionIsError) {
116
+ if (collectableIsError || collectionIsError || currenciesIsError) {
117
+ return (
118
+ <ErrorModal
119
+ isOpen={createListingModal$.isOpen.get()}
120
+ chainId={Number(chainId)}
121
+ onClose={createListingModal$.close}
122
+ title="List item for sale"
123
+ />
124
+ );
125
+ }
126
+
127
+ if (!currencies || currencies.length === 0) {
106
128
  return (
107
129
  <ErrorModal
108
130
  isOpen={createListingModal$.isOpen.get()}
109
131
  chainId={Number(chainId)}
110
132
  onClose={createListingModal$.close}
111
133
  title="List item for sale"
134
+ message="No currencies are configured for the marketplace, contact the marketplace owners"
112
135
  />
113
136
  );
114
137
  }