@0xsequence/marketplace-sdk 0.4.8 → 0.5.0

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 (116) hide show
  1. package/dist/{chunk-2OJB35FS.js → chunk-5NORRVPM.js} +5 -5
  2. package/dist/{chunk-2OJB35FS.js.map → chunk-5NORRVPM.js.map} +1 -1
  3. package/dist/{chunk-ATDCYXXV.js → chunk-6YHHCGGY.js} +2 -2
  4. package/dist/{chunk-WRMJ5FZM.js → chunk-HV2X2VZN.js} +874 -194
  5. package/dist/chunk-HV2X2VZN.js.map +1 -0
  6. package/dist/{chunk-AQT3BQ67.js → chunk-J2XJZ6SJ.js} +12 -5
  7. package/dist/chunk-J2XJZ6SJ.js.map +1 -0
  8. package/dist/{chunk-JEOUQFT3.js → chunk-LJAB3S6U.js} +4 -3
  9. package/dist/chunk-LJAB3S6U.js.map +1 -0
  10. package/dist/{chunk-7WCZP6FN.js → chunk-OUVFTA63.js} +649 -386
  11. package/dist/chunk-OUVFTA63.js.map +1 -0
  12. package/dist/{chunk-XXML5K3X.js → chunk-QTJF5GDQ.js} +2 -2
  13. package/dist/{chunk-LF44FCG5.js → chunk-TQWM4ER6.js} +2 -2
  14. package/dist/{chunk-LF44FCG5.js.map → chunk-TQWM4ER6.js.map} +1 -1
  15. package/dist/{chunk-6R4G7J6Q.js → chunk-WSCUPAGR.js} +33 -5
  16. package/dist/chunk-WSCUPAGR.js.map +1 -0
  17. package/dist/{create-config-D5WqfUft.d.ts → create-config-BXvwUh55.d.ts} +2 -2
  18. package/dist/index.css +31 -17
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +1 -1
  21. package/dist/{marketplace-config-C_fDWzz0.d.ts → marketplace-config-znEu4L0K.d.ts} +1 -1
  22. package/dist/{marketplace.gen-B8S8fflj.d.ts → marketplace.gen-CCJ-URn2.d.ts} +16 -4
  23. package/dist/react/_internal/api/index.d.ts +3 -2
  24. package/dist/react/_internal/api/index.js +3 -1
  25. package/dist/react/_internal/index.d.ts +5 -5
  26. package/dist/react/_internal/index.js +3 -1
  27. package/dist/react/_internal/wagmi/index.d.ts +3 -3
  28. package/dist/react/hooks/index.d.ts +249 -74
  29. package/dist/react/hooks/index.js +18 -5
  30. package/dist/react/index.css +37 -17
  31. package/dist/react/index.css.map +1 -1
  32. package/dist/react/index.d.ts +6 -6
  33. package/dist/react/index.js +22 -9
  34. package/dist/react/ssr/index.js +4 -0
  35. package/dist/react/ssr/index.js.map +1 -1
  36. package/dist/react/ui/components/collectible-card/index.css +37 -17
  37. package/dist/react/ui/components/collectible-card/index.css.map +1 -1
  38. package/dist/react/ui/components/collectible-card/index.d.ts +27 -4
  39. package/dist/react/ui/components/collectible-card/index.js +8 -9
  40. package/dist/react/ui/icons/index.js +3 -3
  41. package/dist/react/ui/index.css +37 -17
  42. package/dist/react/ui/index.css.map +1 -1
  43. package/dist/react/ui/index.d.ts +3 -3
  44. package/dist/react/ui/index.js +8 -9
  45. package/dist/react/ui/modals/_internal/components/actionModal/index.d.ts +3 -3
  46. package/dist/react/ui/modals/_internal/components/actionModal/index.js +5 -5
  47. package/dist/{sdk-config-BXVH8PS2.d.ts → sdk-config-B32_2bG3.d.ts} +29 -7
  48. package/dist/{services-CdXAIjt1.d.ts → services-BRBVE0mm.d.ts} +1 -1
  49. package/dist/styles/index.css +31 -17
  50. package/dist/styles/index.css.map +1 -1
  51. package/dist/styles/index.d.ts +2 -2
  52. package/dist/styles/index.js +2 -2
  53. package/dist/types/index.d.ts +3 -3
  54. package/dist/types/index.js +1 -1
  55. package/dist/{types-eX4P9xju.d.ts → types-Yto6KrTN.d.ts} +2 -2
  56. package/dist/utils/index.d.ts +3 -3
  57. package/dist/utils/index.js +1 -1
  58. package/package.json +16 -16
  59. package/src/react/_internal/api/__mocks__/marketplace.msw.ts +4 -0
  60. package/src/react/_internal/api/marketplace.gen.ts +45 -7
  61. package/src/react/_internal/api/query-keys.ts +4 -0
  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 +4 -0
  65. package/src/react/hooks/options/__mocks__/marketplaceConfig.msw.ts +10 -1
  66. package/src/react/hooks/useAutoSelectFeeOption.tsx +104 -0
  67. package/src/react/hooks/useCancelOrder.tsx +57 -1
  68. package/src/react/hooks/useCollectionBalanceDetails.tsx +87 -0
  69. package/src/react/hooks/useCollectionDetails.tsx +35 -0
  70. package/src/react/hooks/useCollectionDetailsPolling.tsx +60 -0
  71. package/src/react/ui/components/_internals/action-button/ActionButton.tsx +36 -118
  72. package/src/react/ui/components/_internals/action-button/components/ActionButtonBody.tsx +52 -0
  73. package/src/react/ui/components/_internals/action-button/components/NonOwnerActions.tsx +72 -0
  74. package/src/react/ui/components/_internals/action-button/components/OwnerActions.tsx +81 -0
  75. package/src/react/ui/components/_internals/action-button/hooks/useActionButtonLogic.ts +93 -0
  76. package/src/react/ui/components/_internals/action-button/store.ts +47 -0
  77. package/src/react/ui/components/_internals/action-button/styles.css.ts +8 -0
  78. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +35 -18
  79. package/src/react/ui/components/collectible-card/Footer.tsx +5 -8
  80. package/src/react/ui/components/collectible-card/styles.css.ts +44 -31
  81. package/src/react/ui/icons/CartIcon.tsx +46 -0
  82. package/src/react/ui/modals/BuyModal/Modal.tsx +0 -2
  83. package/src/react/ui/modals/BuyModal/__tests__/Modal.test.tsx +253 -0
  84. package/src/react/ui/modals/BuyModal/__tests__/store.test.ts +100 -0
  85. package/src/react/ui/modals/BuyModal/hooks/__tests__/useBuyCollectable.test.tsx +402 -0
  86. package/src/react/ui/modals/BuyModal/hooks/__tests__/useCheckoutOptions.test.tsx +267 -0
  87. package/src/react/ui/modals/BuyModal/hooks/__tests__/useFees.test.tsx +166 -0
  88. package/src/react/ui/modals/BuyModal/hooks/__tests__/useLoadData.test.tsx +209 -0
  89. package/src/react/ui/modals/BuyModal/hooks/useBuyCollectable.ts +7 -4
  90. package/src/react/ui/modals/BuyModal/hooks/useCheckoutOptions.ts +19 -17
  91. package/src/react/ui/modals/BuyModal/hooks/useLoadData.ts +9 -7
  92. package/src/react/ui/modals/BuyModal/modals/Modal1155.tsx +36 -18
  93. package/src/react/ui/modals/BuyModal/modals/__tests__/CheckoutModal.test.tsx +162 -0
  94. package/src/react/ui/modals/BuyModal/modals/__tests__/Modal1155.test.tsx +243 -0
  95. package/src/react/ui/modals/BuyModal/store.ts +11 -10
  96. package/src/react/ui/modals/CreateListingModal/Modal.tsx +26 -3
  97. package/src/react/ui/modals/CreateListingModal/__tests__/Modal.test.tsx +141 -29
  98. package/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx +5 -1
  99. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +20 -11
  100. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/__tests__/index.test.tsx +13 -58
  101. package/src/react/ui/modals/_internal/components/priceInput/__tests__/index.test.tsx +2 -0
  102. package/src/react/ui/modals/_internal/components/transactionStatusModal/__tests__/TransactionStatusModal.test.tsx +18 -19
  103. package/src/react/ui/modals/_internal/components/transactionStatusModal/__tests__/utils.test.ts +2 -0
  104. package/src/react/ui/modals/_internal/components/transactionStatusModal/hooks/useTransactionStatus.ts +62 -0
  105. package/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +53 -100
  106. package/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +2 -10
  107. package/tsconfig.tsbuildinfo +1 -1
  108. package/dist/chunk-6R4G7J6Q.js.map +0 -1
  109. package/dist/chunk-7WCZP6FN.js.map +0 -1
  110. package/dist/chunk-AQT3BQ67.js.map +0 -1
  111. package/dist/chunk-FWN2MCLI.js +0 -425
  112. package/dist/chunk-FWN2MCLI.js.map +0 -1
  113. package/dist/chunk-JEOUQFT3.js.map +0 -1
  114. package/dist/chunk-WRMJ5FZM.js.map +0 -1
  115. /package/dist/{chunk-ATDCYXXV.js.map → chunk-6YHHCGGY.js.map} +0 -0
  116. /package/dist/{chunk-XXML5K3X.js.map → chunk-QTJF5GDQ.js.map} +0 -0
@@ -2,7 +2,6 @@ import { useState } from 'react';
2
2
 
3
3
  import { Box, IconButton, Skeleton } from '@0xsequence/design-system';
4
4
  import type { Hex } from 'viem';
5
- import { useAccount } from 'wagmi';
6
5
  import type {
7
6
  ChainId,
8
7
  CollectibleOrder,
@@ -10,7 +9,7 @@ import type {
10
9
  Order,
11
10
  OrderbookKind,
12
11
  } from '../../../_internal';
13
- import { useCurrency, useHighestOffer } from '../../../hooks';
12
+ import { useCurrency } from '../../../hooks';
14
13
  import SvgDiamondEyeIcon from '../../icons/DiamondEye';
15
14
  import ChessTileImage from '../../images/chess-tile.png';
16
15
  import { ActionButton } from '../_internals/action-button/ActionButton';
@@ -63,6 +62,23 @@ type CollectibleCardProps = {
63
62
  onOfferClick?: ({ order }: { order?: Order }) => void;
64
63
  balance?: string;
65
64
  cardLoading?: boolean;
65
+ /**
66
+ * Callback function that is called when the user attempts to perform an action
67
+ * (such as buying or making an offer) that they are not permitted to do.
68
+ *
69
+ * This function is invoked in the following scenario:
70
+ *
71
+ * - When a disconnected user clicks on "Buy Now" and is prompted to connect
72
+ * their wallet. After connecting, if it is determined that the user is
73
+ * already the owner of the collectible, this callback is triggered to inform
74
+ * them that they cannot perform the action (e.g., buying their own collectible).
75
+ *
76
+ * @param action - The action that the user cannot perform, which can be either
77
+ * CollectibleCardAction.BUY or CollectibleCardAction.OFFER.
78
+ */
79
+ onCannotPerformAction?: (
80
+ action: CollectibleCardAction.BUY | CollectibleCardAction.OFFER,
81
+ ) => void;
66
82
  };
67
83
 
68
84
  export function CollectibleCard({
@@ -76,16 +92,11 @@ export function CollectibleCard({
76
92
  onOfferClick,
77
93
  balance,
78
94
  cardLoading,
95
+ onCannotPerformAction,
79
96
  }: CollectibleCardProps) {
80
- const { address: accountAddress } = useAccount();
81
97
  const collectibleMetadata = lowestListing?.metadata;
98
+ const highestOffer = lowestListing?.offer
82
99
  const [imageLoadingError, setImageLoadingError] = useState(false);
83
- const { data: highestOffer, isLoading: highestOfferLoading } =
84
- useHighestOffer({
85
- chainId: String(chainId),
86
- collectionAddress,
87
- tokenId: collectibleId,
88
- });
89
100
 
90
101
  const { data: lowestListingCurrency } = useCurrency({
91
102
  chainId,
@@ -95,13 +106,13 @@ export function CollectibleCard({
95
106
  enabled: !!lowestListing?.order?.priceCurrencyAddress,
96
107
  },
97
108
  });
98
- if (highestOfferLoading || cardLoading) {
109
+ if (cardLoading) {
99
110
  return <CollectibleSkeleton />;
100
111
  }
101
112
 
102
113
  const action = (
103
114
  balance
104
- ? (highestOffer?.order && CollectibleCardAction.SELL) ||
115
+ ? (highestOffer && CollectibleCardAction.SELL) ||
105
116
  (!lowestListing?.order && CollectibleCardAction.LIST) ||
106
117
  CollectibleCardAction.TRANSFER
107
118
  : (lowestListing?.order && CollectibleCardAction.BUY) ||
@@ -118,6 +129,7 @@ export function CollectibleCard({
118
129
  borderRadius="md"
119
130
  overflow="hidden"
120
131
  background="backgroundPrimary"
132
+ tabIndex={0}
121
133
  >
122
134
  <Box
123
135
  display="flex"
@@ -134,11 +146,15 @@ export function CollectibleCard({
134
146
  padding="0"
135
147
  className={collectibleTileWrapper}
136
148
  >
137
- <article style={{ width: '100%' }}>
149
+ <article
150
+ style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
151
+ >
138
152
  {externalUrl && (
139
153
  <IconButton
140
154
  as="a"
141
155
  href={externalUrl}
156
+ target="_blank"
157
+ rel="noopener noreferrer"
142
158
  size="sm"
143
159
  backdropFilter="blur"
144
160
  variant="glass"
@@ -146,6 +162,7 @@ export function CollectibleCard({
146
162
  e.stopPropagation();
147
163
  }}
148
164
  position="absolute"
165
+ zIndex="20"
149
166
  top="2"
150
167
  left="2"
151
168
  icon={SvgDiamondEyeIcon}
@@ -162,15 +179,14 @@ export function CollectibleCard({
162
179
  <Footer
163
180
  name={name || ''}
164
181
  type={collectionType}
165
- onOfferClick={() => onOfferClick?.({ order: highestOffer?.order })}
166
- highestOffer={highestOffer?.order}
182
+ onOfferClick={() => onOfferClick?.({ order: highestOffer })}
183
+ highestOffer={highestOffer}
167
184
  lowestListingPriceAmount={lowestListing?.order?.priceAmount}
168
185
  lowestListingCurrency={lowestListingCurrency}
169
186
  balance={balance}
170
- isAnimated={!!action}
171
187
  />
172
188
 
173
- {accountAddress && (highestOffer || lowestListing) && (
189
+ {(highestOffer || lowestListing) && (
174
190
  <Box
175
191
  display="flex"
176
192
  alignItems="center"
@@ -184,9 +200,10 @@ export function CollectibleCard({
184
200
  tokenId={collectibleId}
185
201
  orderbookKind={orderbookKind}
186
202
  action={action}
187
- highestOffer={highestOffer?.order}
203
+ highestOffer={highestOffer}
188
204
  lowestListing={lowestListing?.order}
189
- isOwned={!!balance}
205
+ owned={!!balance}
206
+ onCannotPerformAction={onCannotPerformAction}
190
207
  />
191
208
  </Box>
192
209
  )}
@@ -1,6 +1,5 @@
1
1
  import { Box, IconButton, Image, Text } from '@0xsequence/design-system';
2
2
  import { formatUnits } from 'viem';
3
- import { useAccount } from 'wagmi';
4
3
  import { ContractType, type Currency, type Order } from '../../../_internal';
5
4
  import SvgBellIcon from '../../icons/Bell';
6
5
  import { footer, offerBellButton } from './styles.css';
@@ -13,7 +12,6 @@ type FooterProps = {
13
12
  lowestListingPriceAmount?: string;
14
13
  lowestListingCurrency?: Currency;
15
14
  balance?: string;
16
- isAnimated?: boolean;
17
15
  };
18
16
 
19
17
  export const Footer = ({
@@ -24,9 +22,7 @@ export const Footer = ({
24
22
  lowestListingPriceAmount,
25
23
  lowestListingCurrency,
26
24
  balance,
27
- isAnimated,
28
25
  }: FooterProps) => {
29
- const { address } = useAccount();
30
26
  const listed = !!lowestListingPriceAmount && !!lowestListingCurrency;
31
27
 
32
28
  if (name.length > 15 && highestOffer) {
@@ -45,7 +41,7 @@ export const Footer = ({
45
41
  padding="4"
46
42
  whiteSpace="nowrap"
47
43
  position="relative"
48
- className={!!address && isAnimated ? footer.animated : footer.static}
44
+ className={footer}
49
45
  >
50
46
  <Box
51
47
  display="flex"
@@ -60,8 +56,9 @@ export const Footer = ({
60
56
  fontWeight="bold"
61
57
  textAlign="left"
62
58
  fontFamily="body"
59
+ visibility={name ? 'visible' : 'hidden'}
63
60
  >
64
- {name}
61
+ {name || 'Untitled'}
65
62
  </Text>
66
63
 
67
64
  {highestOffer && onOfferClick && (
@@ -82,8 +79,8 @@ export const Footer = ({
82
79
  </Box>
83
80
 
84
81
  <Box display="flex" alignItems="center" gap="1">
85
- {listed && (
86
- <Image src={lowestListingCurrency?.imageUrl} width="3" height="3" />
82
+ {listed && lowestListingCurrency.imageUrl && (
83
+ <Image src={lowestListingCurrency.imageUrl} width="3" height="3" />
87
84
  )}
88
85
 
89
86
  <Text
@@ -1,19 +1,23 @@
1
1
  import { atoms } from '@0xsequence/design-system';
2
- import { style, styleVariants } from '@vanilla-extract/css';
2
+ import { style } from '@vanilla-extract/css';
3
3
 
4
4
  export const collectibleCard = style([
5
5
  {
6
6
  width: '175px',
7
7
  border: '1px solid hsla(0, 0%, 31%, 1)',
8
+ ':active': {
9
+ border: '1px solid hsla(247, 100%, 75%, 1)',
10
+ boxShadow: '0px 0px 0px 1px hsla(247, 100%, 75%, 1)',
11
+ },
12
+ ':focus-visible': {
13
+ border: '1px solid hsla(247, 100%, 75%, 1)',
14
+ boxShadow: '0px 0px 0px 2px hsla(247, 100%, 75%, 1)',
15
+ outline: '4px solid hsla(254, 100%, 57%, 1)',
16
+ outlineOffset: '2px',
17
+ },
8
18
  },
9
19
  ]);
10
20
 
11
- export const collectibleImage = style({
12
- width: '175px',
13
- height: '175px',
14
- objectFit: 'cover',
15
- });
16
-
17
21
  export const collectibleTileWrapper = style({
18
22
  padding: 0,
19
23
  border: 'none',
@@ -24,6 +28,24 @@ export const collectibleTileWrapper = style({
24
28
  '&:focus': {
25
29
  outline: 'none',
26
30
  },
31
+
32
+ [`${collectibleCard}:focus &`]: {
33
+ outline: '3px solid black',
34
+ outlineOffset: '-3px',
35
+ borderRadius: 10,
36
+ },
37
+ },
38
+ });
39
+
40
+ export const collectibleImage = style({
41
+ width: '175px',
42
+ height: '175px',
43
+ objectFit: 'cover',
44
+ transition: 'transform 0.2s ease-in-out',
45
+ selectors: {
46
+ [`${collectibleTileWrapper}:hover &`]: {
47
+ transform: 'scale(1.165)',
48
+ },
27
49
  },
28
50
  });
29
51
 
@@ -32,31 +54,22 @@ export const offerBellButton = style({
32
54
  height: '22px',
33
55
  });
34
56
 
35
- const footerBase = style([atoms({ background: 'backgroundPrimary' })]);
57
+ export const footer = style([atoms({ background: 'backgroundPrimary' })]);
36
58
 
37
- export const footer = styleVariants({
38
- animated: [
39
- footerBase,
40
- {
41
- transition: 'transform 0.2s ease-in-out',
42
- selectors: {
43
- [`${collectibleTileWrapper}:hover &`]: {
44
- transform: 'translateY(-30px)',
45
- },
59
+ export const actionWrapper = style([
60
+ atoms({
61
+ backdropFilter: 'blur',
62
+ }),
63
+ {
64
+ background: 'hsla(0, 0%, 100%, 0.1)',
65
+ transition: 'transform 0.2s ease-in-out',
66
+ position: 'absolute',
67
+ width: '100%',
68
+ bottom: -44,
69
+ selectors: {
70
+ [`${collectibleTileWrapper}:hover &`]: {
71
+ transform: 'translateY(-44px)',
46
72
  },
47
73
  },
48
- ],
49
- static: [footerBase],
50
- });
51
-
52
- export const actionWrapper = style({
53
- transition: 'transform 0.2s ease-in-out',
54
- position: 'absolute',
55
- width: '100%',
56
- bottom: -36,
57
- selectors: {
58
- [`${collectibleTileWrapper}:hover &`]: {
59
- transform: 'translateY(-36px)',
60
- },
61
74
  },
62
- });
75
+ ]);
@@ -0,0 +1,46 @@
1
+ import { Box, type IconProps } from '@0xsequence/design-system';
2
+ import { iconVariants } from './styles.css';
3
+
4
+ const Svg = () => (
5
+ <svg
6
+ width="20"
7
+ height="20"
8
+ viewBox="0 0 20 20"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ role="img"
12
+ aria-labelledby="cart-title"
13
+ >
14
+ <title id="cart-title">Cart Icon</title>
15
+ <path
16
+ fillRule="evenodd"
17
+ clipRule="evenodd"
18
+ d="M2.5 3.46836C2.5 3.20969 2.72366 3 2.99955 3H5.16925C5.88938 3 6.5077 3.48022 6.64172 4.14359L8.33188 12.5093C8.37655 12.7304 8.58266 12.8905 8.8227 12.8905H16.987C17.2629 12.8905 17.4866 13.1002 17.4866 13.3589C17.4866 13.6175 17.2629 13.8272 16.987 13.8272H8.8227C8.10257 13.8272 7.48425 13.347 7.35023 12.6836L5.66007 4.31793C5.6154 4.0968 5.40929 3.93673 5.16925 3.93673H2.99955C2.72366 3.93673 2.5 3.72704 2.5 3.46836Z"
19
+ fill="white"
20
+ />
21
+ <path
22
+ d="M18.0003 5.34182H6.40946L7.49564 10.8234H17.0736C17.3133 10.8234 17.5193 10.6637 17.5643 10.443L18.491 5.89813C18.5498 5.60942 18.3138 5.34182 18.0003 5.34182Z"
23
+ fill="white"
24
+ />
25
+ <path
26
+ d="M10.0889 15.8559C10.0889 16.4878 9.54259 17 8.86866 17C8.19473 17 7.64841 16.4878 7.64841 15.8559C7.64841 15.2241 8.19473 14.7119 8.86866 14.7119C9.54259 14.7119 10.0889 15.2241 10.0889 15.8559Z"
27
+ fill="white"
28
+ />
29
+ <path
30
+ d="M16.6268 15.8559C16.6268 16.4878 16.0804 17 15.4065 17C14.7326 17 14.1863 16.4878 14.1863 15.8559C14.1863 15.2241 14.7326 14.7119 15.4065 14.7119C16.0804 14.7119 16.6268 15.2241 16.6268 15.8559Z"
31
+ fill="white"
32
+ />
33
+ </svg>
34
+ );
35
+
36
+ const SvgCartIcon = ({ size = 'sm', ...props }: IconProps) => (
37
+ <Box
38
+ as={Svg}
39
+ className={iconVariants({
40
+ size,
41
+ })}
42
+ {...props}
43
+ />
44
+ );
45
+
46
+ export default SvgCartIcon;
@@ -24,7 +24,6 @@ const BuyModalContent = () => {
24
24
  buyModal$.state.order.collectionContractAddress,
25
25
  ) as Hex;
26
26
  const collectibleId = use$(buyModal$.state.order.tokenId);
27
- const modalId = use$(buyModal$.state.modalId);
28
27
  const callbacks = use$(buyModal$.callbacks);
29
28
  const order = use$(buyModal$.state.order);
30
29
  const isOpen = use$(buyModal$.isOpen);
@@ -94,7 +93,6 @@ const BuyModalContent = () => {
94
93
 
95
94
  return collection.type === ContractType.ERC721 ? (
96
95
  <CheckoutModal
97
- key={modalId}
98
96
  buy={buyAction}
99
97
  collectable={collectable as TokenMetadata}
100
98
  order={order}
@@ -0,0 +1,253 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import {
3
+ render,
4
+ screen,
5
+ cleanup,
6
+ waitFor,
7
+ } from '../../../../_internal/test-utils';
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
+ import { BuyModal } from '../Modal';
10
+ import { buyModal$ } from '../store';
11
+ import { useLoadData } from '../hooks/useLoadData';
12
+ import { useBuyCollectable } from '../hooks/useBuyCollectable';
13
+ import type { Mock } from 'vitest';
14
+ import type { Order } from '../../../../_internal';
15
+ import { MarketplaceKind } from '../../../../_internal';
16
+ import { ContractType } from '../../../../_internal';
17
+
18
+ const mockOrder = {
19
+ orderId: '1',
20
+ priceAmount: '1000000000000000000',
21
+ priceCurrencyAddress: '0x0',
22
+ quantityRemaining: '1',
23
+ createdAt: new Date().toISOString(),
24
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
25
+ } as Order;
26
+
27
+ vi.mock('../hooks/useLoadData', () => ({
28
+ useLoadData: vi.fn(),
29
+ }));
30
+
31
+ vi.mock('../hooks/useBuyCollectable', () => ({
32
+ useBuyCollectable: vi.fn(),
33
+ }));
34
+
35
+ describe('BuyModal', () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+
39
+ (useLoadData as Mock).mockReturnValue({
40
+ collection: null,
41
+ collectable: null,
42
+ checkoutOptions: null,
43
+ isLoading: false,
44
+ isError: false,
45
+ });
46
+
47
+ (useBuyCollectable as Mock).mockReturnValue({
48
+ buy: vi.fn(),
49
+ isLoading: false,
50
+ isError: false,
51
+ });
52
+
53
+ buyModal$.close();
54
+
55
+ cleanup();
56
+ });
57
+
58
+ it('should not render when isOpen is false', () => {
59
+ render(<BuyModal />);
60
+
61
+ expect(screen.queryByText('Loading Sequence Pay')).not.toBeInTheDocument();
62
+ expect(screen.queryByText('Checkout')).not.toBeInTheDocument();
63
+ expect(screen.queryByText('Select Quantity')).not.toBeInTheDocument();
64
+ });
65
+
66
+ it('should render error modal when there is an error', async () => {
67
+ const mockOrderBasic = {
68
+ ...mockOrder,
69
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
70
+ collectionContractAddress: '0x123',
71
+ tokenId: '1',
72
+ quantityRemaining: '1',
73
+ priceCurrencyAddress: '0x0',
74
+ chainId: 1,
75
+ priceAmount: '1000000000000000000',
76
+ };
77
+
78
+ // Test loading error
79
+ (useLoadData as Mock).mockReturnValue({
80
+ collection: { type: ContractType.ERC721 },
81
+ collectable: { decimals: 0 },
82
+ checkoutOptions: { someOption: true },
83
+ isLoading: false,
84
+ isError: true,
85
+ });
86
+
87
+ buyModal$.open({
88
+ order: mockOrderBasic,
89
+ callbacks: {},
90
+ chainId: '1',
91
+ collectionAddress: '0x123',
92
+ tokenId: '1',
93
+ });
94
+
95
+ render(<BuyModal />);
96
+
97
+ // Should show error modal
98
+ await waitFor(() => {
99
+ expect(screen.getByTestId('error-modal')).toBeInTheDocument();
100
+ expect(
101
+ screen.getByText('Error loading item details'),
102
+ ).toBeInTheDocument();
103
+ });
104
+
105
+ // Cleanup
106
+ cleanup();
107
+
108
+ // Test buy error
109
+ (useLoadData as Mock).mockReturnValue({
110
+ collection: { type: ContractType.ERC721 },
111
+ collectable: { decimals: 0 },
112
+ checkoutOptions: { someOption: true },
113
+ isLoading: false,
114
+ isError: false,
115
+ });
116
+
117
+ (useBuyCollectable as Mock).mockReturnValue({
118
+ buy: vi.fn(),
119
+ isLoading: false,
120
+ isError: true,
121
+ status: 'error',
122
+ });
123
+
124
+ buyModal$.open({
125
+ order: mockOrderBasic,
126
+ callbacks: {},
127
+ chainId: '1',
128
+ collectionAddress: '0x123',
129
+ tokenId: '1',
130
+ });
131
+
132
+ render(<BuyModal />);
133
+
134
+ // Should show error modal for buy error too
135
+ expect(screen.getByText('Error')).toBeInTheDocument();
136
+ });
137
+
138
+ it('should render ERC1155QuantityModal when contract type is ERC1155', async () => {
139
+ const mockOrderERC1155 = {
140
+ ...mockOrder,
141
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
142
+ collectionContractAddress: '0x123',
143
+ tokenId: '1',
144
+ quantityRemaining: '10',
145
+ priceCurrencyAddress: '0x0',
146
+ chainId: 1,
147
+ quantityDecimals: 0,
148
+ priceAmount: '1000000000000000000',
149
+ };
150
+
151
+ (useLoadData as Mock).mockReturnValue({
152
+ collection: { type: ContractType.ERC1155 },
153
+ collectable: { decimals: 0 },
154
+ checkoutOptions: { someOption: true },
155
+ isLoading: false,
156
+ isError: false,
157
+ });
158
+
159
+ (useBuyCollectable as Mock).mockReturnValue({
160
+ buy: vi.fn(),
161
+ isLoading: false,
162
+ isError: false,
163
+ });
164
+
165
+ buyModal$.open({
166
+ order: mockOrderERC1155,
167
+ callbacks: {},
168
+ chainId: '1',
169
+ collectionAddress: '0x123',
170
+ tokenId: '1',
171
+ });
172
+
173
+ // Initialize quantity state
174
+ buyModal$.state.quantity.set('1');
175
+ buyModal$.state.checkoutModalLoaded.set(false);
176
+ buyModal$.state.checkoutModalIsLoading.set(false);
177
+
178
+ render(<BuyModal />);
179
+
180
+ await waitFor(() => {
181
+ expect(screen.getByText('Select Quantity')).toBeInTheDocument();
182
+ });
183
+ });
184
+
185
+ it('should not render ERC1155QuantityModal when contract type is ERC721', async () => {
186
+ const mockBuy = vi.fn();
187
+ const mockOrderERC721 = {
188
+ ...mockOrder,
189
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
190
+ collectionContractAddress: '0x123',
191
+ tokenId: '1',
192
+ quantityRemaining: '1',
193
+ priceCurrencyAddress: '0x0',
194
+ chainId: 1,
195
+ priceAmount: '1000000000000000000',
196
+ };
197
+
198
+ // First, mock loading state
199
+ (useLoadData as Mock).mockReturnValue({
200
+ collection: null,
201
+ collectable: null,
202
+ checkoutOptions: null,
203
+ isLoading: true,
204
+ isError: false,
205
+ });
206
+
207
+ (useBuyCollectable as Mock).mockReturnValue({
208
+ buy: mockBuy,
209
+ isLoading: false,
210
+ isError: false,
211
+ status: 'ready',
212
+ });
213
+
214
+ buyModal$.open({
215
+ order: mockOrderERC721,
216
+ callbacks: {},
217
+ chainId: '1',
218
+ collectionAddress: '0x123',
219
+ tokenId: '1',
220
+ });
221
+
222
+ const { rerender } = render(<BuyModal />);
223
+
224
+ // Verify loading modal is shown
225
+ expect(screen.getByText('Loading Sequence Pay')).toBeInTheDocument();
226
+
227
+ // Then update the mock to simulate data loaded
228
+ (useLoadData as Mock).mockReturnValue({
229
+ collection: { type: ContractType.ERC721 },
230
+ collectable: { decimals: 0 },
231
+ checkoutOptions: { someOption: true },
232
+ isLoading: false,
233
+ isError: false,
234
+ });
235
+
236
+ // Force a re-render with the new mock values
237
+ rerender(<BuyModal />);
238
+
239
+ // Wait for loading to complete and verify the rest of the flow
240
+ await waitFor(() => {
241
+ expect(mockBuy).toHaveBeenCalledWith({
242
+ orderId: mockOrderERC721.orderId,
243
+ collectableDecimals: 0,
244
+ quantity: '1',
245
+ marketplace: mockOrderERC721.marketplace,
246
+ checkoutOptions: { someOption: true },
247
+ });
248
+ });
249
+
250
+ expect(screen.queryByText('Loading Sequence Pay')).not.toBeInTheDocument();
251
+ expect(screen.queryByText('Select Quantity')).not.toBeInTheDocument();
252
+ });
253
+ });