@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
@@ -1,5 +1,6 @@
1
1
  import { http, HttpResponse } from 'msw';
2
2
  import { WalletOptions, type MarketplaceConfig } from '../../../../types';
3
+ import { mockCurrencies } from '../../../_internal/api/__mocks__/marketplace.msw';
3
4
 
4
5
  // Mock data
5
6
  export const mockConfig: MarketplaceConfig = {
@@ -12,7 +13,15 @@ export const mockConfig: MarketplaceConfig = {
12
13
  logoUrl: 'https://example.com/logo.png',
13
14
  titleTemplate: '%s | Test Marketplace',
14
15
  walletOptions: [WalletOptions.Sequence],
15
- collections: [],
16
+ collections: [
17
+ {
18
+ collectionAddress: '0x1234567890123456789012345678901234567890',
19
+ chainId: 1,
20
+ marketplaceFeePercentage: 2.5,
21
+ marketplaceType: 'orderbook',
22
+ currencyOptions: mockCurrencies.map((c) => c.contractAddress),
23
+ },
24
+ ],
16
25
  landingPageLayout: 'default',
17
26
  cssString: '',
18
27
  manifestUrl: '',
@@ -0,0 +1,35 @@
1
+ import { queryOptions, useQuery } from '@tanstack/react-query';
2
+ import type { SdkConfig } from '../../types';
3
+ import { getMarketplaceClient } from '../_internal';
4
+ import { useConfig } from './useConfig';
5
+
6
+ type UseCollectionDetails = {
7
+ collectionAddress: string;
8
+ chainId: number;
9
+ };
10
+
11
+ const fetchCollectionDetails = async (
12
+ args: { collectionAddress: string },
13
+ marketplaceClient: Awaited<ReturnType<typeof getMarketplaceClient>>,
14
+ ) => {
15
+ const { collection } = await marketplaceClient.getCollectionDetail({
16
+ contractAddress: args.collectionAddress,
17
+ });
18
+ return collection;
19
+ };
20
+
21
+ export const collectionDetailsOptions = (
22
+ args: UseCollectionDetails,
23
+ config: SdkConfig,
24
+ ) => {
25
+ const marketplaceClient = getMarketplaceClient(args.chainId, config);
26
+ return queryOptions({
27
+ queryKey: ['collectionDetails', args],
28
+ queryFn: () => fetchCollectionDetails(args, marketplaceClient),
29
+ });
30
+ };
31
+
32
+ export const useCollectionDetails = (args: UseCollectionDetails) => {
33
+ const config = useConfig();
34
+ return useQuery(collectionDetailsOptions(args, config));
35
+ };
@@ -0,0 +1,60 @@
1
+ import { queryOptions, useQuery } from '@tanstack/react-query';
2
+ import type { SdkConfig } from '../../types';
3
+ import { CollectionStatus } from '../_internal/api/marketplace.gen';
4
+ import { useConfig } from './useConfig';
5
+ import { collectionDetailsOptions } from './useCollectionDetails';
6
+
7
+ type UseCollectionDetailsPolling = {
8
+ collectionAddress: string;
9
+ chainId: number;
10
+ };
11
+
12
+ const INITIAL_POLLING_INTERVAL = 2000; // 2 seconds
13
+ const MAX_POLLING_INTERVAL = 30000; // 30 seconds
14
+ const MAX_ATTEMPTS = 30;
15
+
16
+ const isTerminalState = (status: CollectionStatus): boolean => {
17
+ return [
18
+ CollectionStatus.active,
19
+ CollectionStatus.failed,
20
+ CollectionStatus.inactive,
21
+ CollectionStatus.incompatible_type,
22
+ ].includes(status);
23
+ };
24
+
25
+ export const collectionDetailsPollingOptions = (
26
+ args: UseCollectionDetailsPolling,
27
+ config: SdkConfig,
28
+ ) => {
29
+ return queryOptions({
30
+ ...collectionDetailsOptions(args, config),
31
+ refetchInterval: (query) => {
32
+ const data = query.state.data;
33
+ if (data && isTerminalState(data.status)) {
34
+ return false;
35
+ }
36
+
37
+ // Calculate exponential backoff interval
38
+ const currentAttempt = (query.state.dataUpdateCount || 0) + 1;
39
+ if (currentAttempt >= MAX_ATTEMPTS) {
40
+ return false;
41
+ }
42
+
43
+ const interval = Math.min(
44
+ INITIAL_POLLING_INTERVAL * Math.pow(1.5, currentAttempt),
45
+ MAX_POLLING_INTERVAL,
46
+ );
47
+
48
+ return interval;
49
+ },
50
+ refetchOnWindowFocus: false,
51
+ retry: false,
52
+ });
53
+ };
54
+
55
+ export const useCollectionDetailsPolling = (
56
+ args: UseCollectionDetailsPolling,
57
+ ) => {
58
+ const config = useConfig();
59
+ return useQuery(collectionDetailsPollingOptions(args, config));
60
+ };
@@ -6,6 +6,7 @@ import '@0xsequence/design-system/styles.css';
6
6
  import type { SdkConfig } from '../types';
7
7
  import { PROVIDER_ID } from './_internal/get-provider';
8
8
  import { getQueryClient } from './_internal/api/get-query-client';
9
+ import { InvalidProjectAccessKeyError } from '../utils/_internal/error/config';
9
10
 
10
11
  export const MarketplaceSdkContext = createContext({} as SdkConfig);
11
12
 
@@ -29,6 +30,10 @@ export function MarketplaceProvider({
29
30
  config,
30
31
  children,
31
32
  }: MarketplaceSdkProviderProps) {
33
+ if (config.projectAccessKey === '' || !config.projectAccessKey) {
34
+ throw new InvalidProjectAccessKeyError(config.projectAccessKey);
35
+ }
36
+
32
37
  return (
33
38
  <MarketplaceQueryClientProvider>
34
39
  <MarketplaceSdkContext.Provider value={config}>
@@ -1,17 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { Button } from '@0xsequence/design-system';
4
3
  import { observer } from '@legendapp/state/react';
5
4
  import type { Hex } from 'viem';
6
- import { InvalidStepError } from '../../../../../utils/_internal/error/transaction';
7
5
  import type { Order, OrderbookKind } from '../../../../_internal';
8
- import { useBuyModal } from '../../../modals/BuyModal';
9
- import { useCreateListingModal } from '../../../modals/CreateListingModal';
10
- import { useMakeOfferModal } from '../../../modals/MakeOfferModal';
11
- import { useSellModal } from '../../../modals/SellModal';
12
- import { useTransferModal } from '../../../modals/TransferModal';
13
-
14
- import { CollectibleCardAction } from './types';
6
+ import type { CollectibleCardAction } from './types';
7
+ import { useActionButtonLogic } from './hooks/useActionButtonLogic';
8
+ import { OwnerActions } from './components/OwnerActions';
9
+ import { NonOwnerActions } from './components/NonOwnerActions';
15
10
 
16
11
  type ActionButtonProps = {
17
12
  chainId: string;
@@ -20,9 +15,12 @@ type ActionButtonProps = {
20
15
  orderbookKind?: OrderbookKind;
21
16
  isTransfer?: boolean;
22
17
  action: CollectibleCardAction;
23
- isOwned: boolean;
18
+ owned?: boolean;
24
19
  highestOffer?: Order;
25
20
  lowestListing?: Order;
21
+ onCannotPerformAction?: (
22
+ action: CollectibleCardAction.BUY | CollectibleCardAction.OFFER,
23
+ ) => void;
26
24
  };
27
25
 
28
26
  export const ActionButton = observer(
@@ -32,124 +30,44 @@ export const ActionButton = observer(
32
30
  tokenId,
33
31
  orderbookKind,
34
32
  action,
33
+ owned,
35
34
  highestOffer,
36
35
  lowestListing,
36
+ onCannotPerformAction,
37
37
  }: ActionButtonProps) => {
38
- const { show: showCreateListingModal } = useCreateListingModal();
39
- const { show: showMakeOfferModal } = useMakeOfferModal();
40
- const { show: showSellModal } = useSellModal();
41
- const { show: showTransferModal } = useTransferModal();
42
- const { show: showBuyModal } = useBuyModal();
43
-
44
- if (action === CollectibleCardAction.BUY) {
45
- if (!lowestListing)
46
- throw new InvalidStepError('BUY', 'lowestListing is required');
47
-
48
- return (
49
- <ActionButtonBody
50
- label="Buy"
51
- onClick={() =>
52
- showBuyModal({
53
- collectionAddress,
54
- chainId: chainId,
55
- tokenId: tokenId,
56
- order: lowestListing,
57
- })
58
- }
59
- />
60
- );
61
- }
62
-
63
- if (action === CollectibleCardAction.SELL) {
64
- if (!highestOffer)
65
- throw new InvalidStepError('SELL', 'highestOffer is required');
66
-
67
- return (
68
- <ActionButtonBody
69
- label="Sell"
70
- onClick={() =>
71
- showSellModal({
72
- collectionAddress,
73
- chainId: chainId,
74
- tokenId: tokenId,
75
- order: highestOffer,
76
- })
77
- }
78
- />
79
- );
80
- }
81
-
82
- if (action === CollectibleCardAction.LIST) {
83
- return (
84
- <ActionButtonBody
85
- label="Create listing"
86
- onClick={() =>
87
- showCreateListingModal({
88
- collectionAddress: collectionAddress as Hex,
89
- chainId: chainId,
90
- collectibleId: tokenId,
91
- orderbookKind,
92
- })
93
- }
94
- />
95
- );
96
- }
38
+ const { shouldShowAction, isOwnerAction } = useActionButtonLogic({
39
+ tokenId,
40
+ owned,
41
+ action,
42
+ onCannotPerformAction,
43
+ });
97
44
 
98
- if (action === CollectibleCardAction.OFFER) {
99
- return (
100
- <ActionButtonBody
101
- label="Make an offer"
102
- onClick={() =>
103
- showMakeOfferModal({
104
- collectionAddress: collectionAddress as Hex,
105
- chainId: chainId,
106
- collectibleId: tokenId,
107
- orderbookKind,
108
- })
109
- }
110
- />
111
- );
45
+ if (!shouldShowAction) {
46
+ return null;
112
47
  }
113
48
 
114
- if (action === CollectibleCardAction.TRANSFER) {
49
+ if (isOwnerAction) {
115
50
  return (
116
- <ActionButtonBody
117
- label="Transfer"
118
- onClick={() =>
119
- showTransferModal({
120
- collectionAddress: collectionAddress as Hex,
121
- chainId: chainId,
122
- collectibleId: tokenId,
123
- })
124
- }
51
+ <OwnerActions
52
+ action={action}
53
+ tokenId={tokenId}
54
+ collectionAddress={collectionAddress}
55
+ chainId={chainId}
56
+ orderbookKind={orderbookKind}
57
+ highestOffer={highestOffer}
125
58
  />
126
59
  );
127
60
  }
128
61
 
129
- return null;
62
+ return (
63
+ <NonOwnerActions
64
+ action={action}
65
+ tokenId={tokenId}
66
+ collectionAddress={collectionAddress}
67
+ chainId={chainId}
68
+ orderbookKind={orderbookKind}
69
+ lowestListing={lowestListing}
70
+ />
71
+ );
130
72
  },
131
73
  );
132
-
133
- type ActionButtonBodyProps = {
134
- label: string;
135
- onClick: () => void;
136
- };
137
-
138
- function ActionButtonBody({ label, onClick }: ActionButtonBodyProps) {
139
- return (
140
- <Button
141
- variant="primary"
142
- label={label}
143
- // eslint-disable-next-line @typescript-eslint/no-empty-function
144
- onClick={(e) => {
145
- e.preventDefault();
146
- e.stopPropagation();
147
- onClick();
148
- }}
149
- // leftIcon={leftIcon}
150
- size="xs"
151
- shape="square"
152
- width="full"
153
- />
154
- );
155
- }
@@ -0,0 +1,52 @@
1
+ import { Button } from '@0xsequence/design-system';
2
+ import { useAccount } from 'wagmi';
3
+ import { useOpenConnectModal } from '@0xsequence/kit';
4
+ import type { CollectibleCardAction } from '../types';
5
+ import { setPendingAction } from '../store';
6
+ import { actionButton } from '../styles.css';
7
+
8
+ type ActionButtonBodyProps = {
9
+ label: 'Buy now' | 'Sell' | 'Make an offer' | 'Create listing' | 'Transfer';
10
+ tokenId: string;
11
+ onClick: () => void;
12
+ icon?: React.ComponentType<{
13
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | undefined;
14
+ }>;
15
+ action?: CollectibleCardAction.BUY | CollectibleCardAction.OFFER;
16
+ };
17
+
18
+ export function ActionButtonBody({
19
+ tokenId,
20
+ label,
21
+ onClick,
22
+ icon,
23
+ action,
24
+ }: ActionButtonBodyProps) {
25
+ const { address } = useAccount();
26
+ const { setOpenConnectModal } = useOpenConnectModal();
27
+
28
+ const handleClick = (e: React.MouseEvent) => {
29
+ e.preventDefault();
30
+ e.stopPropagation();
31
+
32
+ if (!address && action) {
33
+ setPendingAction(action, onClick, tokenId);
34
+ setOpenConnectModal(true);
35
+ } else {
36
+ onClick();
37
+ }
38
+ };
39
+
40
+ return (
41
+ <Button
42
+ className={actionButton}
43
+ variant="primary"
44
+ label={label}
45
+ onClick={handleClick}
46
+ leftIcon={icon}
47
+ size="xs"
48
+ shape="square"
49
+ width="full"
50
+ />
51
+ );
52
+ }
@@ -0,0 +1,72 @@
1
+ import type { Hex } from 'viem';
2
+ import type { Order, OrderbookKind } from '../../../../../_internal';
3
+ import { CollectibleCardAction } from '../types';
4
+ import { ActionButtonBody } from './ActionButtonBody';
5
+ import { useBuyModal } from '../../../../modals/BuyModal';
6
+ import { useMakeOfferModal } from '../../../../modals/MakeOfferModal';
7
+ import { InvalidStepError } from '../../../../../../utils/_internal/error/transaction';
8
+ import SvgCartIcon from '../../../../icons/CartIcon';
9
+
10
+ type NonOwnerActionsProps = {
11
+ action: CollectibleCardAction;
12
+ tokenId: string;
13
+ collectionAddress: Hex;
14
+ chainId: string;
15
+ orderbookKind?: OrderbookKind;
16
+ lowestListing?: Order;
17
+ };
18
+
19
+ export function NonOwnerActions({
20
+ action,
21
+ tokenId,
22
+ collectionAddress,
23
+ chainId,
24
+ orderbookKind,
25
+ lowestListing,
26
+ }: NonOwnerActionsProps) {
27
+ const { show: showBuyModal } = useBuyModal();
28
+ const { show: showMakeOfferModal } = useMakeOfferModal();
29
+
30
+ if (action === CollectibleCardAction.BUY) {
31
+ if (!lowestListing) {
32
+ throw new InvalidStepError('BUY', 'lowestListing is required');
33
+ }
34
+
35
+ return (
36
+ <ActionButtonBody
37
+ action={CollectibleCardAction.BUY}
38
+ tokenId={tokenId}
39
+ label="Buy now"
40
+ onClick={() =>
41
+ showBuyModal({
42
+ collectionAddress,
43
+ chainId,
44
+ tokenId,
45
+ order: lowestListing,
46
+ })
47
+ }
48
+ icon={SvgCartIcon}
49
+ />
50
+ );
51
+ }
52
+
53
+ if (action === CollectibleCardAction.OFFER) {
54
+ return (
55
+ <ActionButtonBody
56
+ action={CollectibleCardAction.OFFER}
57
+ tokenId={tokenId}
58
+ label="Make an offer"
59
+ onClick={() =>
60
+ showMakeOfferModal({
61
+ collectionAddress,
62
+ chainId,
63
+ collectibleId: tokenId,
64
+ orderbookKind,
65
+ })
66
+ }
67
+ />
68
+ );
69
+ }
70
+
71
+ return null;
72
+ }
@@ -0,0 +1,81 @@
1
+ import type { Hex } from 'viem';
2
+ import type { Order, OrderbookKind } from '../../../../../_internal';
3
+ import { CollectibleCardAction } from '../types';
4
+ import { ActionButtonBody } from './ActionButtonBody';
5
+ import { useCreateListingModal } from '../../../../modals/CreateListingModal';
6
+ import { useSellModal } from '../../../../modals/SellModal';
7
+ import { useTransferModal } from '../../../../modals/TransferModal';
8
+
9
+ type OwnerActionsProps = {
10
+ action: CollectibleCardAction;
11
+ tokenId: string;
12
+ collectionAddress: Hex;
13
+ chainId: string;
14
+ orderbookKind?: OrderbookKind;
15
+ highestOffer?: Order;
16
+ };
17
+
18
+ export function OwnerActions({
19
+ action,
20
+ tokenId,
21
+ collectionAddress,
22
+ chainId,
23
+ orderbookKind,
24
+ highestOffer,
25
+ }: OwnerActionsProps) {
26
+ const { show: showCreateListingModal } = useCreateListingModal();
27
+ const { show: showSellModal } = useSellModal();
28
+ const { show: showTransferModal } = useTransferModal();
29
+
30
+ if (action === CollectibleCardAction.LIST) {
31
+ return (
32
+ <ActionButtonBody
33
+ label="Create listing"
34
+ tokenId={tokenId}
35
+ onClick={() =>
36
+ showCreateListingModal({
37
+ collectionAddress: collectionAddress as Hex,
38
+ chainId,
39
+ collectibleId: tokenId,
40
+ orderbookKind,
41
+ })
42
+ }
43
+ />
44
+ );
45
+ }
46
+
47
+ if (action === CollectibleCardAction.SELL && highestOffer) {
48
+ return (
49
+ <ActionButtonBody
50
+ tokenId={tokenId}
51
+ label="Sell"
52
+ onClick={() =>
53
+ showSellModal({
54
+ collectionAddress,
55
+ chainId,
56
+ tokenId,
57
+ order: highestOffer,
58
+ })
59
+ }
60
+ />
61
+ );
62
+ }
63
+
64
+ if (action === CollectibleCardAction.TRANSFER) {
65
+ return (
66
+ <ActionButtonBody
67
+ label="Transfer"
68
+ tokenId={tokenId}
69
+ onClick={() =>
70
+ showTransferModal({
71
+ collectionAddress: collectionAddress as Hex,
72
+ chainId,
73
+ collectibleId: tokenId,
74
+ })
75
+ }
76
+ />
77
+ );
78
+ }
79
+
80
+ return null;
81
+ }
@@ -0,0 +1,93 @@
1
+ import { useEffect } from 'react';
2
+ import { useAccount } from 'wagmi';
3
+ import { CollectibleCardAction } from '../types';
4
+ import {
5
+ actionButtonStore,
6
+ clearPendingAction,
7
+ executePendingActionIfExists,
8
+ } from '../store';
9
+
10
+ type UseActionButtonLogicProps = {
11
+ tokenId: string;
12
+ owned?: boolean;
13
+ action: CollectibleCardAction;
14
+ onCannotPerformAction?: (
15
+ action: CollectibleCardAction.BUY | CollectibleCardAction.OFFER,
16
+ ) => void;
17
+ };
18
+
19
+ export const useActionButtonLogic = ({
20
+ tokenId,
21
+ owned,
22
+ action,
23
+ onCannotPerformAction,
24
+ }: UseActionButtonLogicProps) => {
25
+ const { address } = useAccount();
26
+ const actionsThatOwnersCannotPerform = [
27
+ CollectibleCardAction.BUY,
28
+ CollectibleCardAction.OFFER,
29
+ ];
30
+ const pendingActionType = actionButtonStore.pendingAction.type.get();
31
+
32
+ // Handle owner restrictions
33
+ // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
34
+ useEffect(() => {
35
+ if (
36
+ owned &&
37
+ actionButtonStore.pendingAction.get() &&
38
+ address &&
39
+ !actionsThatOwnersCannotPerform.includes(action) &&
40
+ actionButtonStore.pendingAction.get()?.collectibleId === tokenId
41
+ ) {
42
+ onCannotPerformAction?.(
43
+ pendingActionType as
44
+ | CollectibleCardAction.BUY
45
+ | CollectibleCardAction.OFFER,
46
+ );
47
+ clearPendingAction();
48
+ }
49
+ }, [
50
+ owned,
51
+ actionButtonStore.pendingAction.get(),
52
+ address,
53
+ action,
54
+ tokenId,
55
+ onCannotPerformAction,
56
+ pendingActionType,
57
+ ]);
58
+
59
+ // Execute pending action when user becomes connected
60
+ useEffect(() => {
61
+ if (
62
+ address &&
63
+ !owned &&
64
+ actionButtonStore.pendingAction.get() &&
65
+ actionButtonStore.pendingAction.get()?.collectibleId === tokenId
66
+ ) {
67
+ // TODO: Remove this timeout once pointer-events: none issue is fixed on Radix UI side
68
+ setTimeout(() => {
69
+ executePendingActionIfExists();
70
+ clearPendingAction();
71
+ }, 1000);
72
+ }
73
+ }, [address, owned, tokenId]);
74
+
75
+ const shouldShowAction = !address
76
+ ? [CollectibleCardAction.BUY, CollectibleCardAction.OFFER].includes(action)
77
+ : true;
78
+
79
+ const isOwnerAction =
80
+ address &&
81
+ owned &&
82
+ [
83
+ CollectibleCardAction.LIST,
84
+ CollectibleCardAction.TRANSFER,
85
+ CollectibleCardAction.SELL,
86
+ ].includes(action);
87
+
88
+ return {
89
+ address,
90
+ shouldShowAction,
91
+ isOwnerAction,
92
+ };
93
+ };
@@ -0,0 +1,47 @@
1
+ import { observable } from '@legendapp/state';
2
+ import type { CollectibleCardAction } from './types';
3
+
4
+ type PendingAction = {
5
+ type: CollectibleCardAction.BUY | CollectibleCardAction.OFFER;
6
+ collectibleId: string;
7
+ callback: () => void;
8
+ timestamp: number;
9
+ };
10
+
11
+ export const actionButtonStore = observable({
12
+ pendingAction: null as PendingAction | null,
13
+ });
14
+
15
+ export const setPendingAction = (
16
+ type: CollectibleCardAction.BUY | CollectibleCardAction.OFFER,
17
+ callback: () => void,
18
+ collectibleId: string,
19
+ ) => {
20
+ actionButtonStore.pendingAction.set({
21
+ type,
22
+ callback,
23
+ timestamp: Date.now(),
24
+ collectibleId,
25
+ });
26
+ };
27
+
28
+ export const clearPendingAction = () => {
29
+ actionButtonStore.pendingAction.set(null);
30
+ };
31
+
32
+ export const executePendingActionIfExists = () => {
33
+ const timestamp = actionButtonStore.pendingAction.get()?.timestamp;
34
+ const callback = actionButtonStore.pendingAction.get()?.callback as
35
+ | (() => void)
36
+ | undefined;
37
+
38
+ if (timestamp && callback) {
39
+ // Only execute if the pending action is less than 5 minutes old
40
+ if (
41
+ Date.now() - timestamp < 5 * 60 * 1000 &&
42
+ typeof callback === 'function'
43
+ ) {
44
+ callback();
45
+ }
46
+ }
47
+ };
@@ -0,0 +1,8 @@
1
+ import { globalStyle, style } from '@vanilla-extract/css';
2
+
3
+ export const actionButton = style({});
4
+
5
+ globalStyle(`${actionButton} > div`, {
6
+ justifyContent: 'center',
7
+ alignItems: 'center',
8
+ });