From 6297c24436ecebdd59acacdd5bd528710f10c662 Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Wed, 2 Oct 2024 18:22:46 -0300 Subject: [PATCH 1/7] feat: add put for sale logic --- package-lock.json | 31 +-- package.json | 4 +- .../CollectionDetailPage.tsx | 3 +- .../CollectionItem/CollectionItem.tsx | 47 ++-- .../CreateSingleItemModal.container.ts | 3 +- .../CreateSingleItemModal.tsx | 8 +- .../CreateSingleItemModal.types.ts | 6 +- .../EditPriceAndBeneficiaryModal.css | 31 ++- .../EditPriceAndBeneficiaryModal.tsx | 205 ++++++++++-------- .../EditPriceAndBeneficiaryModal.types.ts | 8 +- .../EditPriceAndBeneficiaryModal/utils.ts | 7 + .../PutForSaleOffchainModal.container.ts | 33 +++ .../PutForSaleOffchainModal.tsx | 27 +++ .../PutForSaleOffchainModal.types.ts | 22 ++ .../Modals/PutForSaleOffchainModal/index.ts | 3 + .../SellCollectionModal.tsx | 4 +- src/components/Modals/index.ts | 1 + src/modules/common/sagas.ts | 6 +- src/modules/common/store.ts | 5 +- src/modules/item/actions.ts | 15 +- src/modules/item/reducer.ts | 24 +- src/modules/item/sagas.ts | 27 ++- src/modules/item/utils.ts | 67 +++++- src/modules/translation/languages/en.json | 5 +- 24 files changed, 441 insertions(+), 151 deletions(-) create mode 100644 src/components/Modals/EditPriceAndBeneficiaryModal/utils.ts create mode 100644 src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.container.ts create mode 100644 src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx create mode 100644 src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.types.ts create mode 100644 src/components/Modals/PutForSaleOffchainModal/index.ts diff --git a/package-lock.json b/package-lock.json index b398f810a..263b284e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,10 +44,10 @@ "decentraland-builder-scripts": "^0.24.0", "decentraland-connect": "^6.3.1", "decentraland-crypto-fetch": "^2.0.1", - "decentraland-dapps": "^23.5.0", + "decentraland-dapps": "^23.6.1", "decentraland-ecs": "6.12.4-7784644013.commit-f770b3e", "decentraland-experiments": "^1.0.2", - "decentraland-transactions": "^2.13.0", + "decentraland-transactions": "^2.15.0", "decentraland-ui": "^6.9.2", "ethers": "^5.6.8", "file-saver": "^2.0.1", @@ -11564,14 +11564,14 @@ } }, "node_modules/decentraland-dapps": { - "version": "23.5.0", - "resolved": "https://registry.npmjs.org/decentraland-dapps/-/decentraland-dapps-23.5.0.tgz", - "integrity": "sha512-VtV9JIQjQYotPnQzcCn2WbH4k9X5OmIDgiUcPtOuLTHMn5ILJck4fSLh/FjVdYoA4tMK++K6xJKZ7hMYKbkkDQ==", + "version": "23.6.1", + "resolved": "https://registry.npmjs.org/decentraland-dapps/-/decentraland-dapps-23.6.1.tgz", + "integrity": "sha512-jQ2eQWxk61uC2PMaPiiCoQWWXapz5t2iqd0BILY6jokjdYCbUUQNPC6d2i13l0pbaXt1YD0Ali0W2CV60vJwGg==", "dependencies": { "@0xsequence/multicall": "^0.25.1", "@0xsequence/relayer": "^0.25.1", "@dcl/crypto": "^3.3.1", - "@dcl/schemas": "^13.9.0", + "@dcl/schemas": "^14.0.0", "@dcl/single-sign-on-client": "^0.1.0", "@dcl/ui-env": "^1.5.0", "@transak/transak-sdk": "^1.0.31", @@ -11584,7 +11584,7 @@ "dcl-catalyst-client": "^21.1.0", "decentraland-connect": "^7.0.0", "decentraland-crypto-fetch": "^2.0.1", - "decentraland-transactions": "^2.13.0", + "decentraland-transactions": "^2.15.0", "decentraland-ui": "^6.9.2", "ethers": "^5.6.8", "events": "^3.3.0", @@ -11614,17 +11614,6 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, - "node_modules/decentraland-dapps/node_modules/@dcl/schemas": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-13.13.0.tgz", - "integrity": "sha512-UY7YY7pn0TxSrinO5nayBushF5Sf1jcBpbsLdw4wWOq7urhzSQZGjbAomgJZ7UfOMi1C7DD2XaTZ89BCp/4Z9Q==", - "dependencies": { - "ajv": "^8.11.0", - "ajv-errors": "^3.0.0", - "ajv-keywords": "^5.1.0", - "mitt": "^3.0.1" - } - }, "node_modules/decentraland-dapps/node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -11954,9 +11943,9 @@ } }, "node_modules/decentraland-transactions": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/decentraland-transactions/-/decentraland-transactions-2.13.0.tgz", - "integrity": "sha512-u+a0hT+l3dqXC9dv+JhX24W6Ca64MSMabsq6664DBmS1Dx+EajqPvdN4axfRhDVoqavH33sXY1XFog8krSlLZQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/decentraland-transactions/-/decentraland-transactions-2.15.0.tgz", + "integrity": "sha512-vWKaxCldMRc2Uy5kUJnV9ggIyQqGWn766wJr+X+/FeXgjtDvBNMLSdgd2EolbxnZ+DHLWCXsDBmJviRdeJ7d3Q==", "dependencies": { "@0xsquid/sdk": "^2.8.13", "@0xsquid/squid-types": "^0.1.78", diff --git a/package.json b/package.json index 356ae6d81..a9e0101d5 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,10 @@ "decentraland-builder-scripts": "^0.24.0", "decentraland-connect": "^6.3.1", "decentraland-crypto-fetch": "^2.0.1", - "decentraland-dapps": "^23.5.0", + "decentraland-dapps": "^23.6.1", "decentraland-ecs": "6.12.4-7784644013.commit-f770b3e", "decentraland-experiments": "^1.0.2", - "decentraland-transactions": "^2.13.0", + "decentraland-transactions": "^2.15.0", "decentraland-ui": "^6.9.2", "ethers": "^5.6.8", "file-saver": "^2.0.1", diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.tsx b/src/components/CollectionDetailPage/CollectionDetailPage.tsx index e615a0eb1..00e363ab8 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.tsx +++ b/src/components/CollectionDetailPage/CollectionDetailPage.tsx @@ -86,7 +86,7 @@ export default function CollectionDetailPage({ if (collection) { onOpenModal('SellCollectionModal', { collectionId: collection.id, - isOnSale: isOffchainPublicItemOrdersEnabled ? isEnableForSaleOffchain(collection, wallet) : isCollectionOnSale(collection, wallet) + isOnSale: isEnableForSaleOffchain(collection, wallet) || isCollectionOnSale(collection, wallet) }) } }, [collection, wallet, onOpenModal]) @@ -277,7 +277,6 @@ export default function CollectionDetailPage({ const hasOnlyWearables = hasWearables && !hasEmotes const filteredItems = items.filter(item => (hasOnlyWearables ? isWearable(item) : hasOnlyEmotes ? isEmote(item) : item.type === tab)) const showShowTabs = hasEmotes && hasWearables - return ( <>
diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx index e4039b6e4..3e59a3458 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx @@ -9,7 +9,7 @@ import { Link, useHistory } from 'react-router-dom' import { locations } from 'routing/locations' import { preventDefault } from 'lib/event' import { extractThirdPartyTokenId, extractTokenId, isThirdParty } from 'lib/urn' -import { isComplete, isFree, canManageItem, getMaxSupply, isSmart, isEmote } from 'modules/item/utils' +import { isComplete, canManageItem, getMaxSupply, isSmart, isEmote, isFree } from 'modules/item/utils' import { isEnableForSaleOffchain, isLocked, isOnSale } from 'modules/collection/utils' import { isEmoteData, SyncStatus, VIDEO_PATH, WearableData } from 'modules/item/types' import { FromParam } from 'modules/location/types' @@ -36,6 +36,7 @@ export default function CollectionItem({ const history = useHistory() const isOnSaleLegacy = wallet && isOnSale(collection, wallet) const isEnableForSaleOffchainMarketplace = wallet && isOffchainPublicItemOrdersEnabled && isEnableForSaleOffchain(collection, wallet) + const shouldAllowPriceEdition = !isOffchainPublicItemOrdersEnabled || isEnableForSaleOffchainMarketplace || isOnSaleLegacy const handleEditPriceAndBeneficiary = useCallback(() => { onOpenModal('EditPriceAndBeneficiaryModal', { itemId: item.id }) @@ -64,21 +65,31 @@ export default function CollectionItem({ onOpenModal('MoveItemToAnotherCollectionModal', { item, fromCollection: collection }) }, [item, onOpenModal, collection]) + const handlePutForSale = useCallback(() => { + onOpenModal('PutForSaleOffchainModal', { itemId: item.id }) + }, []) + const renderPrice = useCallback(() => { - return item.price ? ( -
- {isFree(item) ? ( - t('global.free') - ) : ( - - {ethers.utils.formatEther(item.price)} - - )} -
- ) : ( -
- {t('collection_item.set_price')} -
+ if (!item.price) { + return ( +
+ {t('collection_item.set_price')} +
+ ) + } + + if (isFree(item)) { + return {t('global.free')} + } + + if (item.price === ethers.constants.MaxUint256.toString()) { + return - + } + + return ( + + {ethers.utils.formatEther(item.price)} + ) }, [item, handleEditPriceAndBeneficiary]) @@ -135,7 +146,9 @@ export default function CollectionItem({ )} {canManageItem(collection, item, ethAddress) && !isLocked(collection) ? ( <> - {item.price ? : null} + {item.price && shouldAllowPriceEdition ? ( + + ) : null} {!item.isPublished ? ( <> @@ -203,7 +216,7 @@ export default function CollectionItem({ {renderItemStatus()} {isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy && ( - diff --git a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.container.ts b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.container.ts index 00adf87b8..eb7c28616 100644 --- a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.container.ts +++ b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.container.ts @@ -4,7 +4,7 @@ import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors import { RootState } from 'modules/common/types' import { getCollection } from 'modules/collection/selectors' import { Collection } from 'modules/collection/types' -import { getIsLinkedWearablesV2Enabled } from 'modules/features/selectors' +import { getIsLinkedWearablesV2Enabled, getIsOffchainPublicItemOrdersEnabled } from 'modules/features/selectors' import { saveItemRequest, SAVE_ITEM_REQUEST } from 'modules/item/actions' import { getLoading, getError, getStatusByItemId } from 'modules/item/selectors' import { MapStateProps, MapDispatchProps, MapDispatch, OwnProps } from './CreateSingleItemModal.types' @@ -21,6 +21,7 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { address: getAddress(state), error: getError(state), isThirdPartyV2Enabled: getIsLinkedWearablesV2Enabled(state), + isOffchainPublicItemOrdersEnabled: getIsOffchainPublicItemOrdersEnabled(state), itemStatus, isLoading: isLoadingType(getLoading(state), SAVE_ITEM_REQUEST) } diff --git a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx index c8fbc4518..1222fa6a6 100644 --- a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx +++ b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx @@ -272,7 +272,7 @@ export default class CreateSingleItemModal extends React.PureComponent { - const { address, collection, onSave } = this.props + const { address, collection, isOffchainPublicItemOrdersEnabled, onSave } = this.props const { id, name, @@ -361,6 +361,12 @@ export default class CreateSingleItemModal extends React.PureComponent export type OwnProps = Pick & { metadata: CreateSingleItemModalMetadata } -export type MapStateProps = Pick +export type MapStateProps = Pick< + Props, + 'address' | 'error' | 'isLoading' | 'collection' | 'itemStatus' | 'isThirdPartyV2Enabled' | 'isOffchainPublicItemOrdersEnabled' +> export type MapDispatchProps = Pick export type MapDispatch = Dispatch diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.css b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.css index fc53f040f..37c814776 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.css +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.css @@ -2,10 +2,31 @@ padding: 16px 32px 38px; } +.EditPriceAndBeneficiaryModal.ui.modal .modalContainer { + display: grid; + grid-template-columns: 200px 1fr; + gap: 40px; +} + +.EditPriceAndBeneficiaryModal.ui.modal .priceThumbnail { + display: flex; + flex-direction: column; + gap: 10px; +} + +.EditPriceAndBeneficiaryModal.ui.modal .priceThumbnail .priceName { + text-align: center; + line-height: 20px; +} + .EditPriceAndBeneficiaryModal .ui.form .dcl.field { margin-bottom: 4px; } +.EditPriceAndBeneficiaryModal .ui.form .dcl.field .field { + width: 100%; +} + .EditPriceAndBeneficiaryModal .ui.form .dcl.field > .message { display: none; } @@ -59,12 +80,18 @@ } .EditPriceAndBeneficiaryModal .ui.form > .actions { - width: auto; + display: flex; + gap: 10px; flex-wrap: wrap; } +.EditPriceAndBeneficiaryModal .ItemImage { + min-height: 200px; +} + .EditPriceAndBeneficiaryModal .ui.form > .actions > button { - width: 100%; + width: auto; + flex: 1; margin: 1em 0 0; } diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx index 1bf5e0f68..b25528bf0 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx @@ -19,12 +19,13 @@ import { NetworkButton } from 'decentraland-dapps/dist/containers' import Modal from 'decentraland-dapps/dist/containers/Modal' import { T, t } from 'decentraland-dapps/dist/modules/translation/utils' import { toFixedMANAValue } from 'decentraland-dapps/dist/lib/mana' - +import ItemImage from 'components/ItemImage' import Info from 'components/Info' import { isValid } from 'lib/address' import { Item, ItemType } from 'modules/item/types' import { Props, State } from './EditPriceAndBeneficiaryModal.types' import './EditPriceAndBeneficiaryModal.css' +import { getOneYearFromNowDate } from './utils' const MIN_SALE_VALUE = ethers.utils.formatEther(config.get('MIN_SALE_VALUE_IN_WEI', '0')) @@ -44,7 +45,8 @@ export default class EditPriceAndBeneficiaryModal extends React.PureComponent, props: InputOnChangeData) => { + const expirationDate = props.value + this.setState({ expirationDate }) + } + handleSubmit = () => { const { item, itemSortedContents, onSave, onSetPriceAndBeneficiary } = this.props - const { price, isFree } = this.state + const { price, isFree, expirationDate } = this.state const priceInWei = ethers.utils.parseEther(isFree ? '0' : price!).toString() const beneficiary = this.getBeneficiary() + const expiresAt = expirationDate ? new Date(expirationDate) : undefined if (item.isPublished) { - onSetPriceAndBeneficiary(item.id, priceInWei, beneficiary) + onSetPriceAndBeneficiary(item.id, priceInWei, beneficiary, expiresAt) } else { const newItem: Item = { ...item, @@ -110,7 +118,7 @@ export default class EditPriceAndBeneficiaryModal extends React.PureComponent= new Date()) + } + isValidBeneficiary() { return isValid(this.getBeneficiary()) } render() { - const { name, error, isLoading, mountNode, onClose, onSkip } = this.props - const { isFree, isOwnerBeneficiary, price = '' } = this.state + const { name, error, isLoading, mountNode, item, withExpirationDate, onClose, onSkip } = this.props + const { isFree, isOwnerBeneficiary, price = '', expirationDate } = this.state const beneficiary = this.getBeneficiary() + const expirationError = !this.isValidExpirationDate() ? t('edit_price_and_beneficiary_modal.expiration_date_error') : null + const errorMessage = error || expirationError return ( - + - -
- -
+
+
+ + {item.name} +
+ + +
+ + +
+ +   + {t('edit_price_and_beneficiary_modal.free')} +
+
+ {t('edit_price_and_beneficiary_modal.beneficiary_label')} + + + ) as FieldProps['label'] + } + type="address" + placeholder="0x..." + value={beneficiary} + disabled={isFree || isOwnerBeneficiary} + onChange={this.handleBeneficiaryChange} + error={!!beneficiary && !this.isValidBeneficiary()} /> - -
- -   - {t('edit_price_and_beneficiary_modal.free')} +
+
-
- - {t('edit_price_and_beneficiary_modal.beneficiary_label')} - - - ) as FieldProps['label'] - } - type="address" - placeholder="0x..." - value={beneficiary} - disabled={isFree || isOwnerBeneficiary} - onChange={this.handleBeneficiaryChange} - error={!!beneficiary && !this.isValidBeneficiary()} - /> -
- -
- {this.isPriceTooLow() || isFree ? ( - - -
- {this.isPriceTooLow() ? ( - - {MIN_SALE_VALUE} - - ), - token: t(`tokens.${Network.MATIC.toLowerCase()}`), - br:
- }} - /> - ) : isFree ? ( - t('edit_price_and_beneficiary_modal.free_message') - ) : null} -
-
-
- ) : null} - {error ?

{error}

: null} -
- - - {t('global.save')} - - {!!onSkip && ( - - )} - - + {withExpirationDate ? ( + + ) : null} + {this.isPriceTooLow() || isFree ? ( + + +
+ {this.isPriceTooLow() ? ( + + {MIN_SALE_VALUE} + + ), + token: t(`tokens.${Network.MATIC.toLowerCase()}`), + br:
+ }} + /> + ) : isFree ? ( + t('edit_price_and_beneficiary_modal.free_message') + ) : null} +
+
+
+ ) : null} + {errorMessage ?

{errorMessage}

: null} + + + {onSkip ? ( + + ) : ( + + )} + + {t('global.save')} + + + +
) } diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts index 8a0c47fc4..1f22ed8a9 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.types.ts @@ -15,8 +15,11 @@ export type Props = ModalProps & { metadata: EditPriceAndBeneficiaryModalMetadata itemSortedContents?: Record mountNode?: HTMLDivElement | undefined - onSave: typeof saveItemRequest - onSetPriceAndBeneficiary: typeof setPriceAndBeneficiaryRequest + withExpirationDate?: boolean + onSave: typeof saveItemRequest | ((item: Item) => void) + onSetPriceAndBeneficiary: + | typeof setPriceAndBeneficiaryRequest + | ((itemId: string, priceInWei: string, beneficiary: string, expiresAt?: Date) => void) onSkip?: () => void } @@ -25,6 +28,7 @@ export type State = { beneficiary?: string isFree: boolean isOwnerBeneficiary: boolean + expirationDate?: string } export type EditPriceAndBeneficiaryModalMetadata = { diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/utils.ts b/src/components/Modals/EditPriceAndBeneficiaryModal/utils.ts new file mode 100644 index 000000000..a90417a85 --- /dev/null +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/utils.ts @@ -0,0 +1,7 @@ +export function getOneYearFromNowDate() { + const today = new Date() + const year = today.getFullYear() + 1 + const month = String(today.getMonth() + 1).padStart(2, '0') + const day = String(today.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} diff --git a/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.container.ts b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.container.ts new file mode 100644 index 000000000..1b2742b19 --- /dev/null +++ b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.container.ts @@ -0,0 +1,33 @@ +import { connect } from 'react-redux' + +import { RootState } from 'modules/common/types' +import { getAuthorizedItems, getError, getLoading } from 'modules/item/selectors' +import { OwnProps, MapStateProps, MapDispatch, MapDispatchProps } from './PutForSaleOffchainModal.types' +import PutForSaleOffchainModal from './PutForSaleOffchainModal' +import { CREATE_ITEM_ORDER_TRADE_REQUEST, createItemOrderTradeRequest } from 'modules/item/actions' +import { Item } from 'modules/item/types' +import { isLoadingType } from 'decentraland-dapps/dist/modules/loading' +import { getAuthorizedCollections } from 'modules/collection/selectors' +import { Collection } from 'modules/collection/types' + +const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { + const { itemId } = ownProps.metadata + const items = getAuthorizedItems(state) + const collections = getAuthorizedCollections(state) + const item = ownProps.item ?? items.find(item => item.id === itemId)! + const collection = collections.find(collection => collection.id === item.collectionId) + + return { + item, + collection, + isLoading: isLoadingType(getLoading(state), CREATE_ITEM_ORDER_TRADE_REQUEST), + error: getError(state) + } +} + +const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ + onCreateItemOrder: (item: Item, priceInWei: string, beneficiary: string, collection: Collection, expiresAt: Date) => + dispatch(createItemOrderTradeRequest(item, priceInWei, beneficiary, collection, expiresAt)) +}) + +export default connect(mapState, mapDispatch)(PutForSaleOffchainModal) diff --git a/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx new file mode 100644 index 000000000..98dd05353 --- /dev/null +++ b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx @@ -0,0 +1,27 @@ +import { Props } from './PutForSaleOffchainModal.types' +import EditPriceAndBeneficiaryModal from '../EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal' +import { Item } from 'modules/item/types' + +export default function PutForSaleOffchainModal({ item, collection, error, metadata, isLoading, onClose, onCreateItemOrder }: Props) { + const handlePutForSale = (_itemId: string, price: string, beneficiary: string, expiresAt = new Date()) => { + if (!collection || !item) { + console.error('Collection or item not found') + return + } + onCreateItemOrder(item as Item, price, beneficiary, collection, expiresAt) + } + + return ( + undefined} + withExpirationDate + /> + ) +} diff --git a/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.types.ts b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.types.ts new file mode 100644 index 000000000..5508e040f --- /dev/null +++ b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.types.ts @@ -0,0 +1,22 @@ +import { ModalProps } from 'decentraland-dapps/dist/providers/ModalProvider/ModalProvider.types' +import { Collection } from 'modules/collection/types' +import { createItemOrderTradeRequest, CreateItemOrderTradeRequestAction } from 'modules/item/actions' +import { Item, ItemType } from 'modules/item/types' +import { Dispatch } from 'redux' + +export type Props = ModalProps & { + item: Item + isLoading: boolean + error: string | null + collection?: Collection + onCreateItemOrder: typeof createItemOrderTradeRequest +} + +export type EditPriceAndBeneficiaryModalMetadata = { + itemId: string +} + +export type OwnProps = Pick +export type MapStateProps = Pick +export type MapDispatchProps = Pick +export type MapDispatch = Dispatch diff --git a/src/components/Modals/PutForSaleOffchainModal/index.ts b/src/components/Modals/PutForSaleOffchainModal/index.ts new file mode 100644 index 000000000..da8487b1a --- /dev/null +++ b/src/components/Modals/PutForSaleOffchainModal/index.ts @@ -0,0 +1,3 @@ +import PutForSaleOffchainModal from './PutForSaleOffchainModal.container' + +export default PutForSaleOffchainModal diff --git a/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx b/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx index 7fda43058..7720bd9e5 100644 --- a/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx +++ b/src/components/Modals/SellCollectionModal/SellCollectionModal.tsx @@ -3,7 +3,7 @@ import { ModalNavigation, Button } from 'decentraland-ui' import Modal from 'decentraland-dapps/dist/containers/Modal' import { t } from 'decentraland-dapps/dist/modules/translation/utils' -import { setOnSale, enableSaleOffchain } from 'modules/collection/utils' +import { setOnSale, enableSaleOffchain, isOnSale } from 'modules/collection/utils' import { Props } from './SellCollectionModal.types' import './SellCollectionModal.css' @@ -12,7 +12,7 @@ export default class SellCollectionModal extends React.PureComponent { const { collection, wallet, metadata, isOffchainPublicItemOrdersEnabled, onSetMinters } = this.props onSetMinters( collection, - isOffchainPublicItemOrdersEnabled + isOffchainPublicItemOrdersEnabled && !isOnSale(collection, wallet) ? enableSaleOffchain(collection, wallet, !metadata.isOnSale) : setOnSale(collection, wallet, !metadata.isOnSale) ) diff --git a/src/components/Modals/index.ts b/src/components/Modals/index.ts index a01d47ebf..e86cbafbf 100644 --- a/src/components/Modals/index.ts +++ b/src/components/Modals/index.ts @@ -51,4 +51,5 @@ export { default as WorldsForENSOwnersAnnouncementModal } from './WorldsForENSOw export { default as EnsMapAddressModal } from './ENSMapAddressModal' export { default as ReclaimNameModal } from './ReclaimNameModal' export { default as WorldPermissionsModal } from './WorldPermissionsModal' +export { default as PutForSaleOffchainModal } from './PutForSaleOffchainModal' export { CreateCollectionSelectorModal } from './CreateCollectionSelectorModal' diff --git a/src/modules/common/sagas.ts b/src/modules/common/sagas.ts index 40c4ce093..09265d1c0 100644 --- a/src/modules/common/sagas.ts +++ b/src/modules/common/sagas.ts @@ -53,6 +53,7 @@ import { config } from 'config' import { getPeerWithNoGBCollectorURL } from './utils' import { RootStore } from './types' import { WorldsAPI } from 'lib/api/worlds' +import { TradeService } from 'decentraland-dapps/dist/modules/trades/TradeService' const newIdentitySaga = createIdentitySaga({ authURL: config.get('AUTH_URL') @@ -72,7 +73,8 @@ export function* rootSaga( getIdentity: () => AuthIdentity | undefined, store: RootStore, ensApi: ENSApi, - worldsApi: WorldsAPI + worldsApi: WorldsAPI, + tradeService: TradeService ) { yield all([ analyticsSaga(), @@ -88,7 +90,7 @@ export function* rootSaga( forumSaga(builderAPI), identitySaga(), newIdentitySaga(), - itemSaga(builderAPI, newBuilderClient), + itemSaga(builderAPI, newBuilderClient, tradeService), keyboardSaga(), landSaga(), locationSaga(), diff --git a/src/modules/common/store.ts b/src/modules/common/store.ts index 387a7fac7..b281ad560 100644 --- a/src/modules/common/store.ts +++ b/src/modules/common/store.ts @@ -40,6 +40,7 @@ import { createRootReducer } from './reducer' import { rootSaga } from './sagas' import { RootState, RootStore } from './types' import { WorldsAPI } from 'lib/api/worlds' +import { TradeService } from 'decentraland-dapps/dist/modules/trades/TradeService' const isTestEnv = process.env.NODE_ENV === 'test' @@ -169,14 +170,14 @@ const getClientAuthAuthority = () => { // As the builder client manages by itself the version of the API, we need to remove it from // the environment variable that we're using to with the older client. const builderClientUrl: string = BUILDER_SERVER_URL.replace('/v1', '') - const newBuilderClient = new BuilderClient(builderClientUrl, getClientAuthAuthority, getClientAddress, fetch) const ensApi = new ENSApi(config.get('ENS_SUBGRAPH_URL')) const worldsAPI = new WorldsAPI(new Authorization(() => getAddress(store.getState()))) -sagasMiddleware.run(rootSaga, builderAPI, newBuilderClient, catalystClient, getClientAuthAuthority, store, ensApi, worldsAPI) +const tradeService = new TradeService('dcl:builder', config.get('MARKETPLACE_API'), getClientAuthAuthority) +sagasMiddleware.run(rootSaga, builderAPI, newBuilderClient, catalystClient, getClientAuthAuthority, store, ensApi, worldsAPI, tradeService) loadStorageMiddleware(store) if (isDevelopment) { diff --git a/src/modules/item/actions.ts b/src/modules/item/actions.ts index abff08713..8f80e701e 100644 --- a/src/modules/item/actions.ts +++ b/src/modules/item/actions.ts @@ -1,5 +1,5 @@ import { action } from 'typesafe-actions' -import { ChainId } from '@dcl/schemas' +import { ChainId, TradeCreation } from '@dcl/schemas' import { buildTransactionPayload } from 'decentraland-dapps/dist/modules/transaction/utils' import { PaginationStats } from 'lib/api/pagination' import { FetchCollectionItemsParams } from 'lib/api/builder' @@ -279,3 +279,16 @@ export const downloadItemFailure = (itemId: string, error: string) => action(DOW export type DownloadItemRequestAction = ReturnType export type DownloadItemSuccessAction = ReturnType export type DownloadItemFailureAction = ReturnType + +export const CREATE_ITEM_ORDER_TRADE_REQUEST = '[Request] Create Item Order Trade' +export const CREATE_ITEM_ORDER_TRADE_SUCCESS = '[Success] Create Item Order Trade' +export const CREATE_ITEM_ORDER_TRADE_FAILURE = '[Failure] Create Item Order Trade' + +export const createItemOrderTradeRequest = (item: Item, priceInWei: string, beneficiary: string, collection: Collection, expiresAt: Date) => + action(CREATE_ITEM_ORDER_TRADE_REQUEST, { item, priceInWei, beneficiary, collection, expiresAt }) +export const createItemOrderTradeSuccess = (trade: TradeCreation) => action(CREATE_ITEM_ORDER_TRADE_SUCCESS, { trade }) +export const createItemOrderTradeFailure = (error: string) => action(CREATE_ITEM_ORDER_TRADE_FAILURE, { error }) + +export type CreateItemOrderTradeRequestAction = ReturnType +export type CreateItemOrderTradeSuccessAction = ReturnType +export type CreateItemOrderTradeFailureAction = ReturnType diff --git a/src/modules/item/reducer.ts b/src/modules/item/reducer.ts index f5e4bbc20..b2ed8ed36 100644 --- a/src/modules/item/reducer.ts +++ b/src/modules/item/reducer.ts @@ -100,7 +100,13 @@ import { FetchOrphanItemRequestAction, FetchOrphanItemFailureAction, FETCH_ORPHAN_ITEM_REQUEST, - FETCH_ORPHAN_ITEM_FAILURE + FETCH_ORPHAN_ITEM_FAILURE, + CreateItemOrderTradeRequestAction, + CreateItemOrderTradeFailureAction, + CreateItemOrderTradeSuccessAction, + CREATE_ITEM_ORDER_TRADE_REQUEST, + CREATE_ITEM_ORDER_TRADE_FAILURE, + CREATE_ITEM_ORDER_TRADE_SUCCESS } from './actions' import { PublishThirdPartyItemsSuccessAction, @@ -194,6 +200,9 @@ type ItemReducerAction = | FetchOrphanItemSuccessAction | FetchOrphanItemFailureAction | CloseModalAction + | CreateItemOrderTradeRequestAction + | CreateItemOrderTradeFailureAction + | CreateItemOrderTradeSuccessAction export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReducerAction): ItemState { switch (action.type) { @@ -215,12 +224,20 @@ export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReduce case DELETE_ITEM_REQUEST: case RESET_ITEM_REQUEST: case RESCUE_ITEMS_REQUEST: - case DOWNLOAD_ITEM_REQUEST: { + case DOWNLOAD_ITEM_REQUEST: + case CREATE_ITEM_ORDER_TRADE_REQUEST: { return { ...state, loading: loadingReducer(state.loading, action) } } + case CREATE_ITEM_ORDER_TRADE_SUCCESS: { + return { + ...state, + error: null, + loading: loadingReducer(state.loading, action) + } + } case FETCH_ORPHAN_ITEM_REQUEST: { // TODO: Remove this reducer when there are no users with orphan items return { @@ -328,7 +345,8 @@ export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReduce case DELETE_ITEM_FAILURE: case RESET_ITEM_FAILURE: case RESCUE_ITEMS_FAILURE: - case DOWNLOAD_ITEM_FAILURE: { + case DOWNLOAD_ITEM_FAILURE: + case CREATE_ITEM_ORDER_TRADE_FAILURE: { return { ...state, loading: loadingReducer(state.loading, action), diff --git a/src/modules/item/sagas.ts b/src/modules/item/sagas.ts index 6c7490681..a27debd18 100644 --- a/src/modules/item/sagas.ts +++ b/src/modules/item/sagas.ts @@ -4,7 +4,7 @@ import { Contract, providers } from 'ethers' import { LOCATION_CHANGE } from 'connected-react-router' import { takeEvery, call, put, takeLatest, select, take, delay, fork, race, cancelled, getContext } from 'redux-saga/effects' import { channel } from 'redux-saga' -import { ChainId, Network, Entity, EntityType, WearableCategory } from '@dcl/schemas' +import { ChainId, Network, Entity, EntityType, WearableCategory, TradeCreation } from '@dcl/schemas' import { ContractName, getContract } from 'decentraland-transactions' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { ModalState } from 'decentraland-dapps/dist/modules/modal/reducer' @@ -99,7 +99,11 @@ import { FETCH_ORPHAN_ITEM_REQUEST, FetchOrphanItemRequestAction, fetchOrphanItemSuccess, - fetchOrphanItemFailure + fetchOrphanItemFailure, + CreateItemOrderTradeRequestAction, + createItemOrderTradeSuccess, + createItemOrderTradeFailure, + CREATE_ITEM_ORDER_TRADE_REQUEST } from './actions' import { fromRemoteItem } from 'lib/api/transformations' import { isThirdParty } from 'lib/urn' @@ -147,14 +151,16 @@ import { isWearableFileSizeValid, isEmoteFileSizeValid, isSkinFileSizeValid, - isSmartWearableFileSizeValid + isSmartWearableFileSizeValid, + createItemOrderTrade } from './utils' import { ItemPaginationData } from './reducer' import { getSuccessfulDeletedItemToast, getSuccessfulMoveItemToAnotherCollectionToast } from './toasts' +import { TradeService } from 'decentraland-dapps/dist/modules/trades/TradeService' export const SAVE_AND_EDIT_FILES_BATCH_SIZE = 8 -export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClient) { +export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClient, tradeService: TradeService) { const createOrEditCancelledItemsChannel = channel() const createOrEditProgressChannel = channel() yield takeEvery(FETCH_ITEMS_REQUEST, handleFetchItemsRequest) @@ -178,12 +184,25 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien yield takeEvery(DOWNLOAD_ITEM_REQUEST, handleDownloadItemRequest) yield takeEvery(createOrEditProgressChannel, handleCreateOrEditProgress) yield takeEvery(createOrEditCancelledItemsChannel, handleCreateOrEditCancelledItems) + yield takeEvery(CREATE_ITEM_ORDER_TRADE_REQUEST, handleCreateItemOrderTradeRequest) yield takeLatestCancellable( { initializer: SAVE_MULTIPLE_ITEMS_REQUEST, cancellable: CANCEL_SAVE_MULTIPLE_ITEMS }, handleSaveMultipleItemsRequest ) yield fork(fetchItemEntities) + function* handleCreateItemOrderTradeRequest(action: CreateItemOrderTradeRequestAction) { + const { item, beneficiary, priceInWei, collection, expiresAt } = action.payload + try { + const trade: TradeCreation = yield call(createItemOrderTrade, item, priceInWei, beneficiary, collection, expiresAt) + yield call([tradeService, 'addTrade'], trade) + yield put(createItemOrderTradeSuccess(trade)) + yield put(closeAllModals()) + } catch (error) { + yield put(createItemOrderTradeFailure(isErrorWithMessage(error) ? error.message : 'Unknown error')) + } + } + function* handleFetchRaritiesRequest() { try { const rarities: BlockchainRarity[] = yield call([legacyBuilder, 'fetchRarities']) diff --git a/src/modules/item/utils.ts b/src/modules/item/utils.ts index f599b59e8..f51124256 100644 --- a/src/modules/item/utils.ts +++ b/src/modules/item/utils.ts @@ -17,7 +17,11 @@ import { Entity, ContractNetwork, Mapping, - Mappings + Mappings, + TradeAssetType, + TradeCreation, + TradeType, + Network } from '@dcl/schemas' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import future from 'fp-future' @@ -48,6 +52,11 @@ import { EmotePlayMode, VIDEO_PATH } from './types' +import { getChainIdByNetwork, getSigner } from 'decentraland-dapps/dist/lib' +import { getOffChainMarketplaceContract, getTradeSignature } from 'decentraland-dapps/dist/lib/trades' +import { ContractName, getContract } from 'decentraland-transactions' +import { BigNumber } from 'eth-connect' +import { ethers } from 'ethers' export const MAX_VIDEO_FILE_SIZE = 262144000 // 250 MB export const MAX_NFTS_PER_MINT = 50 @@ -778,3 +787,59 @@ export const isSkinFileSizeValid = (fileSize: number): boolean => { export const isSmartWearableFileSizeValid = (fileSize: number): boolean => { return fileSize < MAX_SMART_WEARABLE_FILE_SIZE } + +export async function createItemOrderTrade( + item: Item, + priceInWei: string, + beneficiary: string, + collection: Collection, + expiresAt: Date +): Promise { + const signer = await getSigner() + const address = await signer.getAddress() + const chainId = getChainIdByNetwork(Network.MATIC) + const marketplaceContract = await getOffChainMarketplaceContract(chainId) + const manaContract = getContract(ContractName.MANAToken, chainId) + const contractSignatureIndex = (await marketplaceContract.contractSignatureIndex()) as BigNumber + const signerSignatureIndex = (await marketplaceContract.signerSignatureIndex(address)) as BigNumber + + if (!item.isPublished) { + return Promise.reject(new Error('Item is not published')) + } + + const tradeToSign: Omit = { + signer: address, + network: Network.MATIC, + chainId: chainId, + type: TradeType.PUBLIC_ITEM_ORDER, + checks: { + uses: getMaxSupply(item) - (item.totalSupply || 0), + allowedRoot: '0x', + contractSignatureIndex: contractSignatureIndex.toNumber(), + signerSignatureIndex: signerSignatureIndex.toNumber(), + effective: Date.now(), + expiration: expiresAt.getTime(), + externalChecks: [], + salt: ethers.utils.hexlify(Math.floor(Math.random() * 1000000000000)) + }, + sent: [ + { + assetType: TradeAssetType.COLLECTION_ITEM, + contractAddress: collection.contractAddress!, + itemId: item.tokenId!, + extra: '' + } + ], + received: [ + { + assetType: TradeAssetType.ERC20, + contractAddress: manaContract.address, + amount: priceInWei, + extra: '', + beneficiary + } + ] + } + + return { ...tradeToSign, signature: await getTradeSignature(tradeToSign) } +} diff --git a/src/modules/translation/languages/en.json b/src/modules/translation/languages/en.json index 33d7c092e..72bb2c73f 100644 --- a/src/modules/translation/languages/en.json +++ b/src/modules/translation/languages/en.json @@ -1764,7 +1764,10 @@ } }, "edit_price_and_beneficiary_modal": { - "title": "Set Price", + "expiration_date_label": "Expiration Date", + "expiration_date_error": "Expiration date must be in the future", + "cancel": "Cancel", + "title": "Set Price and Beneficiary", "for_me": "I'm the beneficiary", "free": "Make it free", "price_label": "Price", From 58fbd50cf69f2e53e446f42c7d78457396f6fe65 Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Wed, 2 Oct 2024 19:05:13 -0300 Subject: [PATCH 2/7] fix tests --- .../EditPriceAndBeneficiaryModal.container.ts | 3 +- src/modules/item/sagas.spec.ts | 131 +++++++++--------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.container.ts b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.container.ts index 837687eff..3f4509e1f 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.container.ts +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.container.ts @@ -30,7 +30,8 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ onSave: (item, contents) => dispatch(saveItemRequest(item, contents)), - onSetPriceAndBeneficiary: (itemId, price, beneficiary) => dispatch(setPriceAndBeneficiaryRequest(itemId, price, beneficiary)) + onSetPriceAndBeneficiary: (itemId: string, price: string, beneficiary: string) => + dispatch(setPriceAndBeneficiaryRequest(itemId, price, beneficiary)) }) export default connect(mapState, mapDispatch)(EditPriceAndBeneficiaryModal) diff --git a/src/modules/item/sagas.spec.ts b/src/modules/item/sagas.spec.ts index f0a8a3859..b6053c0f2 100644 --- a/src/modules/item/sagas.spec.ts +++ b/src/modules/item/sagas.spec.ts @@ -94,6 +94,7 @@ import { getCollectionItems, getData as getItemsById, getEntityByItemId, getItem import { ItemPaginationData } from './reducer' import * as toasts from './toasts' import { fromRemoteItem } from 'lib/api/transformations' +import { TradeService } from 'decentraland-dapps/dist/modules/trades/TradeService' const blob: Blob = new Blob() let contents: Record @@ -113,6 +114,7 @@ let dateNowSpy: jest.SpyInstance let pushMock: jest.Mock const updatedAt = Date.now() const mockAddress = '0x6D7227d6F36FC997D53B4646132b3B55D751cc7c' +let tradeService: TradeService beforeEach(() => { dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => updatedAt) @@ -122,6 +124,7 @@ beforeEach(() => { } as unknown as BuilderClient contents = { path: blob } pushMock = jest.fn() + tradeService = new TradeService('dcl:test', 'test.com', () => undefined) }) afterEach(() => { @@ -141,7 +144,7 @@ describe('when handling the save item request action', () => { }) it('should put a saveItemFailure action with invalid character message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true] @@ -159,7 +162,7 @@ describe('when handling the save item request action', () => { }) it('should put a saveItemFailure action with invalid character message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true] @@ -178,7 +181,7 @@ describe('when handling the save item request action', () => { }) it('should put a saveItemFailure action with item too big message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true], @@ -202,7 +205,7 @@ describe('when handling the save item request action', () => { }) it('should put a saveItemFailure action with item too big message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true], @@ -223,7 +226,7 @@ describe('when handling the save item request action', () => { }) it('should put a saveItemFailure action with item too big message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true], @@ -240,7 +243,7 @@ describe('when handling the save item request action', () => { describe('and thumbnail file size is larger than 1MB', () => { it('should put a saveItemFailure action with thumbnail too big message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true], @@ -257,7 +260,7 @@ describe('when handling the save item request action', () => { describe('and video file size is larger than 250MB', () => { it('should put a saveItemFailure action with video too big message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItem, item.id), undefined], [select(getIsLinkedWearablesV2Enabled), true], @@ -296,7 +299,7 @@ describe('when handling the save item request action', () => { }) it('should dispatch the saveItemFailure signaling that the item is locked and not save the item', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [select(getItem, item.id), undefined], @@ -328,7 +331,7 @@ describe('when handling the save item request action', () => { }) it('should put a save item success action with the catalyst image', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -365,7 +368,7 @@ describe('when handling the save item request action', () => { it('should put a save item success action with the catalyst image', () => { const { [THUMBNAIL_PATH]: thumbnailContent, ...modelContents } = contentsToSave const { [THUMBNAIL_PATH]: _, ...itemContents } = itemWithCatalystImage.contents - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -398,7 +401,7 @@ describe('when handling the save item request action', () => { it('should put a save item success action without a new catalyst image', () => { const { [THUMBNAIL_PATH]: thumbnailContent, ...modelContents } = contents const { [THUMBNAIL_PATH]: _, ...itemContents } = item.contents - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -430,7 +433,7 @@ describe('when handling the save item request action', () => { it('should put a save item success action with a new catalyst image', () => { const { [THUMBNAIL_PATH]: thumbnailContent, ...modelContents } = newContentsContainingNewCatalystImage const { [THUMBNAIL_PATH]: _, ...itemContents } = itemWithCatalystImage.contents - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -469,7 +472,7 @@ describe('when handling the save item request action', () => { it('should put a save item success action', () => { const { [THUMBNAIL_PATH]: thumbnailContent, ...modelContents } = contents const { [THUMBNAIL_PATH]: _, ...itemContents } = item.contents - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -496,7 +499,7 @@ describe('when handling the save item request action', () => { it('should save item if it is already published', () => { const { [THUMBNAIL_PATH]: thumbnailContent, ...modelContents } = contents const { [THUMBNAIL_PATH]: _, ...itemContents } = item.contents - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -521,7 +524,7 @@ describe('when handling the save item request action', () => { }) it('should not calculate the size of the contents', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [matchers.call.fn(reHashOlderContents), {}], [getContext('history'), { push: pushMock, location: { pathname: 'notTPdetailPage' } }], @@ -551,7 +554,7 @@ describe('when handling the save item request action', () => { it("should update the item's content with the new hash and upload the files", () => { const { [THUMBNAIL_PATH]: thumbnailContent, ...modelContents } = newContents const { [THUMBNAIL_PATH]: _, ...itemContents } = itemWithNewHashes.contents - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [ call(reHashOlderContents, item.contents, builderAPI), @@ -595,7 +598,7 @@ describe('when handling the save item success action', () => { paginationData = { currentPage: 1, limit: 20, total: 5, ids: [item.id], totalPages: 1 } }) it('should put a fetch collection items success action to fetch the same page again', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], [select(getOpenModals), { EditItemURNModal: true }], @@ -615,7 +618,7 @@ describe('when handling the save item success action', () => { }) it('should put a fetch collection items success action to fetch the same page again', () => { const newPageNumber = Math.ceil((paginationData.total + paginationData.ids.length) / paginationData.limit) - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], [select(getOpenModals), { EditItemURNModal: true }], @@ -633,7 +636,7 @@ describe('when handling the save item success action', () => { describe('and the onlySaveItem option is set', () => { it('should not fetch the new collection items paginated', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], [select(getOpenModals), {}], @@ -651,7 +654,7 @@ describe('when handling the save item success action', () => { describe('and the CreateSingleItemModal is opened', () => { describe('and the item type is wearable', () => { it('should close the modal CreateSingleItemModal', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.collectionDetail(collection.id) } }], [select(getOpenModals), { CreateSingleItemModal: true }], @@ -669,7 +672,7 @@ describe('when handling the save item success action', () => { }) it('should put a location change to the item editor', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.collectionDetail(collection.id) } }], [select(getOpenModals), { CreateSingleItemModal: true }], @@ -700,7 +703,7 @@ describe('when handling the save item success action', () => { describe('and the CreateSingleItemModal is opened', () => { describe('and the item type is wearable', () => { it('should close the modal CreateSingleItemModal', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.itemEditor() } }], [select(getOpenModals), { CreateSingleItemModal: true }], @@ -721,7 +724,7 @@ describe('when handling the save item success action', () => { describe('and the FF EmotesFlow is enabled', () => { it('should close the modal CreateSingleItemModal', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.itemEditor() } }], [select(getOpenModals), { CreateSingleItemModal: true }], @@ -737,7 +740,7 @@ describe('when handling the save item success action', () => { describe('and the FF EmotesFlow is disabled', () => { it('should close the modal CreateSingleItemModal', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.itemEditor() } }], [select(getOpenModals), { CreateSingleItemModal: true }], @@ -808,7 +811,7 @@ describe('when handling the setPriceAndBeneficiaryRequest action', () => { const price = '1000' const beneficiary = '0xpepe' - await expectSaga(itemSaga, builderAPI, builderClient) + await expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItems), [item]], [select(getCollections), [collection]], @@ -842,7 +845,7 @@ describe('when handling the setPriceAndBeneficiaryRequest action', () => { const nonExistentItemId = 'non-existent-id' const errorMessage = 'Error message' - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItems), [item]], [select(getCollections), [collection]], @@ -871,7 +874,7 @@ describe('when handling the setPriceAndBeneficiaryRequest action', () => { const errorMessage = 'Error message' - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItems), [item]], [select(getCollections), [collection]], @@ -1074,7 +1077,7 @@ describe('when handling the downloadItemRequest action', () => { describe('when id is not found', () => { const itemId = 'invalid' it('should throw an error with a message that says the item was not found', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([[select(getItemsById), itemsById]]) .put(downloadItemFailure(itemId, 'Item not found for itemId="invalid"')) .dispatch(downloadItemRequest(itemId)) @@ -1089,7 +1092,7 @@ describe('when handling the downloadItemRequest action', () => { const model = new Blob() const files: Record = { 'male/model.glb': model } const zip: Record = { 'male/model.glb': model } - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItemsById), itemsById], [call([builderAPI, 'fetchContents'], item.contents), files], @@ -1110,7 +1113,7 @@ describe('when handling the downloadItemRequest action', () => { const femaleModel = new Blob() const files: Record = { 'male/model.glb': maleModel, 'female/model.glb': femaleModel } const zip: Record = { 'male/model.glb': maleModel, 'female/model.glb': femaleModel } - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItemsById), itemsById], [call([builderAPI, 'fetchContents'], item.contents), files], @@ -1130,7 +1133,7 @@ describe('when handling the downloadItemRequest action', () => { const model = new Blob() const files: Record = { 'male/model.glb': model, 'female/model.glb': model } const zip: Record = { 'model.glb': model } - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getItemsById), itemsById], [call([builderAPI, 'fetchContents'], item.contents), files], @@ -1187,7 +1190,7 @@ describe('when handling the save multiple items requests action', () => { ;(builderClient.upsertItem as jest.Mock).mockResolvedValueOnce(remoteItems[2]) }) it('should dispatch the update progress action for each uploaded item and the success action with the upserted items and the name of the files of the upserted items', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: 'notTPDetailPage' } }], [select(getOpenModals), { EditItemURNModal: true }] @@ -1214,7 +1217,7 @@ describe('when handling the save multiple items requests action', () => { paginationData = { currentPage: 1, limit: 20, total: 5, ids: items.map(item => item.id), totalPages: 1 } }) it('should request the same page of items if the user is in the TP detail page', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [ getContext('history'), @@ -1236,7 +1239,7 @@ describe('when handling the save multiple items requests action', () => { }) it('should push the tp detail page location with the new page of items', () => { const newPageNumber = Math.ceil((paginationData.total + items.length) / paginationData.limit) - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [ getContext('history'), @@ -1262,7 +1265,7 @@ describe('when handling the save multiple items requests action', () => { ;(builderClient.upsertItem as jest.Mock).mockResolvedValueOnce(remoteItems[2]) }) it('should dispatch the update progress action for the non-failing item upload and the success action with the items that failed, the upserted items and the name of the files of the upserted items', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { pathname: locations.thirdPartyCollectionDetail(items[0].collectionId) }], [select(getPaginationData, items[0].collectionId!), paginationData] @@ -1295,7 +1298,7 @@ describe('when handling the save multiple items requests action', () => { }) it('should dispatch the update progress action for the first non-cancelled upsert and the cancelling action with the upserted items and the name of the files of the upserted items', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(items[0].collectionId) } }], [select(getPaginationData, items[0].collectionId!), paginationData] @@ -1326,7 +1329,7 @@ describe('when handling the save multiple items requests action', () => { paginationData = { currentPage: 1, limit: 20, total: 5, ids: items.map(item => item.id), totalPages: 1 } }) it('should request the same page of items if the user is in the TP detail page', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [ getContext('history'), @@ -1357,7 +1360,7 @@ describe('when handling the save multiple items requests action', () => { const newPageNumber = Math.ceil( (paginationData.total + (savedFilesWithCancelled.length - amountOfFilesThatWillBeCancelled)) / paginationData.limit ) - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [ getContext('history'), @@ -1390,7 +1393,7 @@ describe('when handling the save multiple items requests action', () => { paginationData = { currentPage: 1, limit: 20, total: 5, ids: items.map(item => item.id), totalPages: 1 } }) it('should request the same page of items if the user is in the TP detail page', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { pathname: locations.thirdPartyCollectionDetail(items[0].collectionId) }], [select(getOpenModals), { EditItemURNModal: true }], @@ -1442,7 +1445,7 @@ describe('when handling the rescue items request action', () => { describe('and the meta transactions are successful', () => { it('should dispatch a rescueItemsChunkSuccess per chunk and the rescueItemsSuccess once the transactions finish', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI], [matchers.call.fn(getMethodData), transactionData], @@ -1460,7 +1463,7 @@ describe('when handling the rescue items request action', () => { describe('and the meta transactions are unsuccessful', () => { describe('and the transaction fails to get mined', () => { it('should dispatch the rescueItemsFailure with the information about the error', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI], [matchers.call.fn(getMethodData), transactionData], @@ -1474,7 +1477,7 @@ describe('when handling the rescue items request action', () => { describe('and the call to the transaction service fails', () => { it('should dispatch the rescueItemsFailure with the information about the error', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [call(getChainIdByNetwork, Network.MATIC), ChainId.MATIC_MUMBAI], [matchers.call.fn(getMethodData), transactionData], @@ -1507,7 +1510,7 @@ describe('when handling the fetch of collection items', () => { ;(builderAPI.fetchCollectionItems as jest.Mock).mockReturnValue(paginationData) }) it('should put a fetchCollectionItemsSuccess action with items and pagination data', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch(fetchCollectionItemsRequest(item.collectionId!, { page: 1, limit: paginationData.limit })) .put( fetchCollectionItemsSuccess(item.collectionId!, [item], { @@ -1527,7 +1530,7 @@ describe('when handling the fetch of collection items', () => { ;(builderAPI.fetchCollectionItems as jest.Mock).mockRejectedValue(new Error(errorMessage)) }) it('should put a fetchCollectionItemsFailure action with items and pagination data', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch(fetchCollectionItemsRequest(item.collectionId!, { page: 1, limit: paginationData.limit })) .put(fetchCollectionItemsFailure(item.collectionId!, errorMessage)) .run({ silenceTimeout: true }) @@ -1553,7 +1556,7 @@ describe('when handling the fetch of collection items pages', () => { ;(builderAPI.fetchCollectionItems as jest.Mock).mockReturnValue(paginationData) }) it('should put a fetchCollectionItemsSuccess action with items and pagination data', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch( fetchCollectionItemsRequest(item.collectionId!, { page: [1], limit: paginationData.limit, overridePaginationData: false }) ) @@ -1568,7 +1571,7 @@ describe('when handling the fetch of collection items pages', () => { ;(builderAPI.fetchCollectionItems as jest.Mock).mockRejectedValue(new Error(errorMessage)) }) it('should put a fetchCollectionItemsFailure action with items and pagination data', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch(fetchCollectionItemsRequest(item.collectionId!, { page: [1], limit: paginationData.limit })) .put(fetchCollectionItemsFailure(item.collectionId!, errorMessage)) .run({ silenceTimeout: true }) @@ -1588,7 +1591,7 @@ describe('when handling the delete item success action', () => { paginationData = { currentPage: 3, limit: 20, total: 65, ids: [item.id, item.id], totalPages: 3 } }) it('should put a fetch collection items success action to fetch the same page again', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getAddress), mockAddress], [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], @@ -1608,7 +1611,7 @@ describe('when handling the delete item success action', () => { paginationData = { currentPage: 3, limit: 20, total: 61, ids: [item.id], totalPages: 3 } }) it('should put a fetch collection items success action to fetch the previous page', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getAddress), mockAddress], [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], @@ -1631,7 +1634,7 @@ describe('when handling the delete item success action', () => { paginationData = { currentPage: 1, limit: 20, total: 1, ids: [item.id], totalPages: 1 } }) it('should put a fetch collection items success action to fetch the same first page', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getAddress), mockAddress], [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], @@ -1651,7 +1654,7 @@ describe('when handling the delete item success action', () => { paginationData = { currentPage: 3, limit: 20, total: 65, ids: [item.id, item.id], totalPages: 3 } }) it('should put a fetch address items success action to fetch the same page again', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getAddress), mockAddress], [getContext('history'), { push: pushMock, location: { pathname: locations.collections() } }], @@ -1672,7 +1675,7 @@ describe('when handling the save item curation success action', () => { }) it('should put a fetch item curation request action if the item is a TP one', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], [select(getOpenModals), { EditItemURNModal: true }], @@ -1690,7 +1693,7 @@ describe('when handling the save item curation success action', () => { }) it('should not put a fetch item curation request action if the item is a standard one', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.thirdPartyCollectionDetail(item.collectionId) } }], [select(getOpenModals), { EditItemURNModal: true }], @@ -1703,7 +1706,7 @@ describe('when handling the save item curation success action', () => { }) it('should not put a location change to the item detail if the CreateSingleItemModal was opened and the location was not /collections', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [getContext('history'), { push: pushMock, location: { pathname: locations.collectionDetail('id') } }], [select(getOpenModals), { CreateSingleItemModal: true }], @@ -1737,7 +1740,7 @@ describe('when handling the fetch of rarities', () => { }) it('should put a fetch rarities success action with the fetched rarities', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([[call([builderAPI, builderAPI.fetchRarities]), rarities]]) .dispatch(fetchRaritiesRequest()) .put(fetchRaritiesSuccess(rarities)) @@ -1746,7 +1749,7 @@ describe('when handling the fetch of rarities', () => { describe('when the request to the builder fails', () => { it('should put a fetch rarities failure action with the error', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([[call([builderAPI, builderAPI.fetchRarities]), Promise.reject(new Error('Failed to fetch rarities'))]]) .dispatch(fetchRaritiesRequest()) .put(fetchRaritiesFailure('Failed to fetch rarities')) @@ -1764,7 +1767,7 @@ describe('when handling the setCollection action', () => { const item = { ...mockedItem } - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getAddress), mockAddress], [getContext('history'), { push: pushMock, location: { pathname: locations.collections() } }], @@ -1791,7 +1794,7 @@ describe('when handling the setCollection action', () => { const item = { ...mockedItem } - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getAddress), mockAddress], [getContext('history'), { push: pushMock, location: { pathname: locations.collections() } }], @@ -1825,7 +1828,7 @@ describe('when handling the failure of setting token items id', () => { describe('when error code is 401', () => { it('should put the setItemsTokenIdRequest action to retry the request', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [delay(5000), void 0], [select(getCollection, collection.id), collection], @@ -1838,7 +1841,7 @@ describe('when handling the failure of setting token items id', () => { }) describe('when error code is not 401', () => { it('should display a toast message saying that the publishing failed', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [delay(5000), void 0], [select(getCollection, collection.id), collection], @@ -1866,7 +1869,7 @@ describe('when handling the fetch of an orphan item', () => { ;(builderAPI.fetchItems as jest.Mock).mockReturnValue(paginationData) }) it('should put a fetchOrphanItemSuccess action with hasUserOrphanItems true', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch(fetchOrphanItemRequest(mockAddress)) .put(fetchOrphanItemSuccess(paginationData.total !== 0)) .run({ silenceTimeout: true }) @@ -1884,7 +1887,7 @@ describe('when handling the fetch of an orphan item', () => { ;(builderAPI.fetchItems as jest.Mock).mockReturnValue(paginationData) }) it('should put a fetchOrphanItemSuccess action with hasUserOrphanItems false', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch(fetchOrphanItemRequest(mockAddress)) .put(fetchOrphanItemSuccess(paginationData.total !== 0)) .run({ silenceTimeout: true }) @@ -1898,7 +1901,7 @@ describe('when handling the fetch of an orphan item', () => { ;(builderAPI.fetchItems as jest.Mock).mockRejectedValue(new Error(errorMessage)) }) it('should put a fetchOrphanItemFailure action with the error message', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .dispatch(fetchOrphanItemRequest(mockAddress)) .put(fetchOrphanItemFailure(errorMessage)) .run({ silenceTimeout: true }) @@ -1923,7 +1926,7 @@ describe('when handling the setItemCollection action', () => { describe("and the item's collection is updated successfully", () => { it('should put a save item request action with the new collection id, show the successful toast and close the modal', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getOpenModals), { MoveItemToAnotherCollectionModal: true }], [getContext('history'), { push: pushMock, location: { pathname: locations.collections() } }], @@ -1944,7 +1947,7 @@ describe('when handling the setItemCollection action', () => { describe("and the item's collections fails to be updated", () => { it('should not not the toast message nor close the modal nor close the modal', () => { - return expectSaga(itemSaga, builderAPI, builderClient) + return expectSaga(itemSaga, builderAPI, builderClient, tradeService) .provide([ [select(getOpenModals), { MoveItemToAnotherCollectionModal: true }], [getContext('history'), { push: pushMock, location: { pathname: locations.collections() } }], From 30cd6fd778d0f19c719286deb58168141675ff8c Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Thu, 3 Oct 2024 17:13:55 -0300 Subject: [PATCH 3/7] feaT: add remove order logic --- package-lock.json | 9 +-- package.json | 2 +- .../CollectionDetailPage.css | 5 ++ .../CollectionItem.container.ts | 21 +++++-- .../CollectionItem/CollectionItem.tsx | 41 ++++++++++--- .../CollectionItem/CollectionItem.types.ts | 14 +++-- .../EditPriceAndBeneficiaryModal.tsx | 2 +- .../PutForSaleOffchainModal.container.ts | 11 +++- .../PutForSaleOffchainModal.module.css | 48 +++++++++++++++ .../PutForSaleOffchainModal.tsx | 45 +++++++++++++- .../PutForSaleOffchainModal.types.ts | 15 +++-- src/lib/api/marketplace.ts | 10 +++- src/modules/item/actions.ts | 18 +++++- src/modules/item/reducer.ts | 53 +++++++++++++++-- src/modules/item/sagas.ts | 58 ++++++++++++++++--- src/modules/item/types.ts | 2 + src/modules/translation/languages/en.json | 10 +++- src/modules/translation/languages/es.json | 10 +++- src/modules/translation/languages/zh.json | 10 +++- 19 files changed, 336 insertions(+), 48 deletions(-) create mode 100644 src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.module.css diff --git a/package-lock.json b/package-lock.json index 263b284e7..ae74b7b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@dcl/crypto": "^3.4.5", "@dcl/hashing": "^3.0.4", "@dcl/mini-rpc": "^1.0.7", - "@dcl/schemas": "^14.0.0", + "@dcl/schemas": "^14.1.0", "@dcl/sdk": "7.5.5", "@dcl/single-sign-on-client": "^0.1.0", "@dcl/ui-env": "^1.5.0", @@ -2748,9 +2748,10 @@ } }, "node_modules/@dcl/schemas": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-14.0.0.tgz", - "integrity": "sha512-w3P/5g/gkYcBUB6+eN1PYdQBaiS8nLh7ldGfUI6GuiHQKvPIC3wS26Dx5i2ukwHrds2koOaqhFdLzHLVDDitQg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-14.1.0.tgz", + "integrity": "sha512-KE7679WNJqlrXH088pvXh2a6YLtDL+PvZYXY8ZXShhEJrnxjUhXGYH1LJckPLy4Tb7GEJBwaCcfYlVVVMrDLeA==", + "license": "Apache-2.0", "dependencies": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", diff --git a/package.json b/package.json index a9e0101d5..bd8c7ad89 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@dcl/crypto": "^3.4.5", "@dcl/hashing": "^3.0.4", "@dcl/mini-rpc": "^1.0.7", - "@dcl/schemas": "^14.0.0", + "@dcl/schemas": "^14.1.0", "@dcl/sdk": "7.5.5", "@dcl/single-sign-on-client": "^0.1.0", "@dcl/ui-env": "^1.5.0", diff --git a/src/components/CollectionDetailPage/CollectionDetailPage.css b/src/components/CollectionDetailPage/CollectionDetailPage.css index b8f3e8203..aece15d4a 100644 --- a/src/components/CollectionDetailPage/CollectionDetailPage.css +++ b/src/components/CollectionDetailPage/CollectionDetailPage.css @@ -357,3 +357,8 @@ .CollectionDetailPage.popup-mint:before { background-color: var(--smart-grey) !important; } + +.toast-info .body { + max-width: 450px; + overflow: hidden; +} diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.container.ts b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.container.ts index e8adeedc3..398ffb2b3 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.container.ts +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.container.ts @@ -2,29 +2,40 @@ import { connect } from 'react-redux' import { getAddress } from 'decentraland-dapps/dist/modules/wallet/selectors' import { RootState } from 'modules/common/types' import { openModal } from 'decentraland-dapps/dist/modules/modal/actions' -import { deleteItemRequest } from 'modules/item/actions' -import { getStatusByItemId } from 'modules/item/selectors' +import { + CANCEL_ITEM_ORDER_TRADE_REQUEST, + cancelItemOrderTradeRequest, + CancelItemOrderTradeRequestAction, + deleteItemRequest +} from 'modules/item/actions' +import { getLoading, getStatusByItemId } from 'modules/item/selectors' import { setItems } from 'modules/editor/actions' import { MapStateProps, MapDispatch, MapDispatchProps, OwnProps } from './CollectionItem.types' import CollectionItem from './CollectionItem' import { getIsOffchainPublicItemOrdersEnabled } from 'modules/features/selectors' import { getWallet } from 'modules/wallet/selectors' +import { isLoadingType } from 'decentraland-dapps/dist/modules/loading' const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => { const statusByItemId = getStatusByItemId(state) - + const loadingTradeIds = getLoading(state) + .filter(action => action.type === CANCEL_ITEM_ORDER_TRADE_REQUEST) + .map(action => (action as CancelItemOrderTradeRequestAction).payload.tradeId) return { ethAddress: getAddress(state), status: statusByItemId[ownProps.item.id], wallet: getWallet(state), - isOffchainPublicItemOrdersEnabled: getIsOffchainPublicItemOrdersEnabled(state) + isOffchainPublicItemOrdersEnabled: getIsOffchainPublicItemOrdersEnabled(state), + isCancellingItemOrder: isLoadingType(getLoading(state), CANCEL_ITEM_ORDER_TRADE_REQUEST), + loadingTradeIds } } const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ onOpenModal: (name, metadata) => dispatch(openModal(name, metadata)), onDeleteItem: item => dispatch(deleteItemRequest(item)), - onSetItems: items => dispatch(setItems(items)) + onSetItems: items => dispatch(setItems(items)), + onRemoveFromSale: tradeId => dispatch(cancelItemOrderTradeRequest(tradeId, true)) }) export default connect(mapState, mapDispatch)(CollectionItem) diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx index 3e59a3458..193ede27d 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx @@ -25,12 +25,15 @@ const LENGTH_LIMIT = 25 export default function CollectionItem({ onOpenModal, onSetItems, + onRemoveFromSale, item, isOffchainPublicItemOrdersEnabled, collection, status, ethAddress, - wallet + wallet, + loadingTradeIds, + isCancellingItemOrder }: Props) { analytics = getAnalytics() const history = useHistory() @@ -39,6 +42,11 @@ export default function CollectionItem({ const shouldAllowPriceEdition = !isOffchainPublicItemOrdersEnabled || isEnableForSaleOffchainMarketplace || isOnSaleLegacy const handleEditPriceAndBeneficiary = useCallback(() => { + if (isOffchainPublicItemOrdersEnabled && isEnableForSaleOffchainMarketplace) { + onOpenModal('PutForSaleOffchainModal', { itemId: item.id }) + return + } + onOpenModal('EditPriceAndBeneficiaryModal', { itemId: item.id }) }, [item, onOpenModal]) @@ -69,6 +77,13 @@ export default function CollectionItem({ onOpenModal('PutForSaleOffchainModal', { itemId: item.id }) }, []) + const handleRemoveFromSale = useCallback(() => { + if (!item.tradeId) { + return + } + onRemoveFromSale(item.tradeId) + }, []) + const renderPrice = useCallback(() => { if (!item.price) { return ( @@ -78,12 +93,12 @@ export default function CollectionItem({ ) } - if (isFree(item)) { - return {t('global.free')} + if (item.price === ethers.constants.MaxUint256.toString() || (isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy && !item.tradeId)) { + return - } - if (item.price === ethers.constants.MaxUint256.toString()) { - return - + if (isFree(item)) { + return {t('global.free')} } return ( @@ -91,7 +106,7 @@ export default function CollectionItem({ {ethers.utils.formatEther(item.price)} ) - }, [item, handleEditPriceAndBeneficiary]) + }, [item, isOnSaleLegacy, isOffchainPublicItemOrdersEnabled, handleEditPriceAndBeneficiary]) const renderItemStatus = useCallback(() => { return status === SyncStatus.UNSYNCED ? ( @@ -214,13 +229,25 @@ export default function CollectionItem({ ) : null} {renderItemStatus()} - {isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy && ( + {isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy && !item.tradeId && ( )} + {isOffchainPublicItemOrdersEnabled && item.tradeId && ( + + + + )} {renderItemContextMenu()} ) diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.types.ts b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.types.ts index 5b2b26b6a..d0d1c8ffa 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.types.ts +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.types.ts @@ -2,7 +2,7 @@ import { Dispatch } from 'redux' import { Collection } from 'modules/collection/types' import { Item, SyncStatus } from 'modules/item/types' import { openModal, OpenModalAction } from 'decentraland-dapps/dist/modules/modal/actions' -import { deleteItemRequest, DeleteItemRequestAction } from 'modules/item/actions' +import { CancelItemOrderTradeRequestAction, deleteItemRequest, DeleteItemRequestAction } from 'modules/item/actions' import { setItems, SetItemsAction } from 'modules/editor/actions' import { Wallet } from 'decentraland-dapps/dist/modules/wallet' @@ -13,12 +13,18 @@ export type Props = { status: SyncStatus isOffchainPublicItemOrdersEnabled: boolean wallet: Wallet | null + isCancellingItemOrder: boolean + loadingTradeIds: string[] onOpenModal: typeof openModal onDeleteItem: typeof deleteItemRequest onSetItems: typeof setItems + onRemoveFromSale: (tradeId: string) => void } -export type MapStateProps = Pick -export type MapDispatchProps = Pick -export type MapDispatch = Dispatch +export type MapStateProps = Pick< + Props, + 'ethAddress' | 'status' | 'isOffchainPublicItemOrdersEnabled' | 'wallet' | 'isCancellingItemOrder' | 'loadingTradeIds' +> +export type MapDispatchProps = Pick +export type MapDispatch = Dispatch export type OwnProps = Pick diff --git a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx index b25528bf0..1fcc62c0c 100644 --- a/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx +++ b/src/components/Modals/EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal.tsx @@ -113,7 +113,7 @@ export default class EditPriceAndBeneficiaryModal extends React.PureComponent { item, collection, isLoading: isLoadingType(getLoading(state), CREATE_ITEM_ORDER_TRADE_REQUEST), + isLoadingCancel: isLoadingType(getLoading(state), CANCEL_ITEM_ORDER_TRADE_REQUEST), error: getError(state) } } const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ onCreateItemOrder: (item: Item, priceInWei: string, beneficiary: string, collection: Collection, expiresAt: Date) => - dispatch(createItemOrderTradeRequest(item, priceInWei, beneficiary, collection, expiresAt)) + dispatch(createItemOrderTradeRequest(item, priceInWei, beneficiary, collection, expiresAt)), + onRemoveFromSale: (tradeId: string) => dispatch(cancelItemOrderTradeRequest(tradeId)) }) export default connect(mapState, mapDispatch)(PutForSaleOffchainModal) diff --git a/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.module.css b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.module.css new file mode 100644 index 000000000..191193003 --- /dev/null +++ b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.module.css @@ -0,0 +1,48 @@ +.modalTitle { + font-size: 20px; + font-weight: 700; + line-height: 32px; + text-align: center; +} + +.modalSubtitle { + font-size: 16px; + line-height: 24px; + text-align: center; +} + +:global(.PutForSaleOffchainModal.ui.modal > .content) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.modalContent { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + max-width: 500px; + padding: 40px 0; +} + +.modalIcon { + background: var(--secondary-text); + border-radius: 50%; + width: 80px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; +} + +.modalIcon :global(i.icon) { + margin: 0; +} + +.error { + max-width: 100%; + overflow: hidden; + color: var(--error); +} diff --git a/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx index 98dd05353..137302820 100644 --- a/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx +++ b/src/components/Modals/PutForSaleOffchainModal/PutForSaleOffchainModal.tsx @@ -1,8 +1,22 @@ import { Props } from './PutForSaleOffchainModal.types' +import Modal from 'decentraland-dapps/dist/containers/Modal' import EditPriceAndBeneficiaryModal from '../EditPriceAndBeneficiaryModal/EditPriceAndBeneficiaryModal' import { Item } from 'modules/item/types' +import { Button, Icon, Loader, ModalActions, ModalContent } from 'decentraland-ui' +import styles from './PutForSaleOffchainModal.module.css' +import { t } from 'decentraland-dapps/dist/modules/translation' -export default function PutForSaleOffchainModal({ item, collection, error, metadata, isLoading, onClose, onCreateItemOrder }: Props) { +export default function PutForSaleOffchainModal({ + item, + collection, + error, + metadata, + isLoading, + isLoadingCancel, + onClose, + onCreateItemOrder, + onRemoveFromSale +}: Props) { const handlePutForSale = (_itemId: string, price: string, beneficiary: string, expiresAt = new Date()) => { if (!collection || !item) { console.error('Collection or item not found') @@ -11,6 +25,35 @@ export default function PutForSaleOffchainModal({ item, collection, error, metad onCreateItemOrder(item as Item, price, beneficiary, collection, expiresAt) } + const handleRemoveFromSale = () => { + onRemoveFromSale(item.tradeId!) + } + + if (item.tradeId) { + return ( + + +
+
+ {isLoadingCancel ? : } +
+

{t('put_for_sale_offchain_modal.title')}

+

{t('put_for_sale_offchain_modal.subtitle')}

+
+ {error ? {error} : null} +
+ + + + +
+ ) + } + return ( isLoading: boolean + isLoadingCancel: boolean error: string | null collection?: Collection onCreateItemOrder: typeof createItemOrderTradeRequest + onRemoveFromSale: typeof cancelItemOrderTradeRequest } export type EditPriceAndBeneficiaryModalMetadata = { @@ -17,6 +24,6 @@ export type EditPriceAndBeneficiaryModalMetadata = { } export type OwnProps = Pick -export type MapStateProps = Pick -export type MapDispatchProps = Pick -export type MapDispatch = Dispatch +export type MapStateProps = Pick +export type MapDispatchProps = Pick +export type MapDispatch = Dispatch diff --git a/src/lib/api/marketplace.ts b/src/lib/api/marketplace.ts index 4cf5236b4..c4dfefccf 100644 --- a/src/lib/api/marketplace.ts +++ b/src/lib/api/marketplace.ts @@ -1,10 +1,10 @@ import { gql } from 'apollo-boost' import { config } from 'config' import { createClient } from './graph' +import { BaseAPI } from 'decentraland-dapps/dist/lib' export const MARKETPLACE_GRAPH_URL = config.get('MARKETPLACE_GRAPH_URL', '') const marketplaceGraphClient = createClient(MARKETPLACE_GRAPH_URL) - const BATCH_SIZE = 1000 const getSubdomainQuery = () => gql` @@ -52,7 +52,7 @@ type OwnerByNameQueryResult = { nfts: OwnerByNameTuple[] } -export class MarketplaceAPI { +export class MarketplaceAPI extends BaseAPI { public async fetchENSOwnerByDomain(domains: string[]): Promise> { if (!domains) { return {} @@ -102,6 +102,10 @@ export class MarketplaceAPI { } return results } + + public async fetchCollectionItems(collectionAddress: string) { + return this.request('get', `/items?contractAddress=${collectionAddress}`) + } } -export const marketplace = new MarketplaceAPI() +export const marketplace = new MarketplaceAPI(config.get('MARKETPLACE_API')) diff --git a/src/modules/item/actions.ts b/src/modules/item/actions.ts index 8f80e701e..96883ffeb 100644 --- a/src/modules/item/actions.ts +++ b/src/modules/item/actions.ts @@ -1,5 +1,5 @@ import { action } from 'typesafe-actions' -import { ChainId, TradeCreation } from '@dcl/schemas' +import { ChainId, Trade } from '@dcl/schemas' import { buildTransactionPayload } from 'decentraland-dapps/dist/modules/transaction/utils' import { PaginationStats } from 'lib/api/pagination' import { FetchCollectionItemsParams } from 'lib/api/builder' @@ -286,9 +286,23 @@ export const CREATE_ITEM_ORDER_TRADE_FAILURE = '[Failure] Create Item Order Trad export const createItemOrderTradeRequest = (item: Item, priceInWei: string, beneficiary: string, collection: Collection, expiresAt: Date) => action(CREATE_ITEM_ORDER_TRADE_REQUEST, { item, priceInWei, beneficiary, collection, expiresAt }) -export const createItemOrderTradeSuccess = (trade: TradeCreation) => action(CREATE_ITEM_ORDER_TRADE_SUCCESS, { trade }) +export const createItemOrderTradeSuccess = (trade: Trade, item: Item, priceInWei: string, beneficiary: string, expiresAt: number) => + action(CREATE_ITEM_ORDER_TRADE_SUCCESS, { trade, priceInWei, beneficiary, expiresAt, item }) export const createItemOrderTradeFailure = (error: string) => action(CREATE_ITEM_ORDER_TRADE_FAILURE, { error }) export type CreateItemOrderTradeRequestAction = ReturnType export type CreateItemOrderTradeSuccessAction = ReturnType export type CreateItemOrderTradeFailureAction = ReturnType + +export const CANCEL_ITEM_ORDER_TRADE_REQUEST = '[Request] Cancel Item Order Trade' +export const CANCEL_ITEM_ORDER_TRADE_SUCCESS = '[Success] Cancel Item Order Trade' +export const CANCEL_ITEM_ORDER_TRADE_FAILURE = '[Failure] Cancel Item Order Trade' + +export const cancelItemOrderTradeRequest = (tradeId: string, errorToast = false) => + action(CANCEL_ITEM_ORDER_TRADE_REQUEST, { tradeId, errorToast }) +export const cancelItemOrderTradeSuccess = (tradeId: string) => action(CANCEL_ITEM_ORDER_TRADE_SUCCESS, { tradeId }) +export const cancelItemOrderTradeFailure = (error: string) => action(CANCEL_ITEM_ORDER_TRADE_FAILURE, { error }) + +export type CancelItemOrderTradeRequestAction = ReturnType +export type CancelItemOrderTradeSuccessAction = ReturnType +export type CancelItemOrderTradeFailureAction = ReturnType diff --git a/src/modules/item/reducer.ts b/src/modules/item/reducer.ts index b2ed8ed36..4bd468644 100644 --- a/src/modules/item/reducer.ts +++ b/src/modules/item/reducer.ts @@ -106,7 +106,13 @@ import { CreateItemOrderTradeSuccessAction, CREATE_ITEM_ORDER_TRADE_REQUEST, CREATE_ITEM_ORDER_TRADE_FAILURE, - CREATE_ITEM_ORDER_TRADE_SUCCESS + CREATE_ITEM_ORDER_TRADE_SUCCESS, + CANCEL_ITEM_ORDER_TRADE_REQUEST, + CancelItemOrderTradeRequestAction, + CancelItemOrderTradeSuccessAction, + CancelItemOrderTradeFailureAction, + CANCEL_ITEM_ORDER_TRADE_FAILURE, + CANCEL_ITEM_ORDER_TRADE_SUCCESS } from './actions' import { PublishThirdPartyItemsSuccessAction, @@ -120,6 +126,7 @@ import { toItemObject } from './utils' import { Item, BlockchainRarity } from './types' import { buildCatalystItemURN, buildThirdPartyURN, decodeURN, isThirdPartyCollectionDecodedUrn, URNType } from 'lib/urn' import { CLOSE_MODAL, CloseModalAction } from 'decentraland-dapps/dist/modules/modal/actions' +import { ethers } from 'ethers' export type ItemPaginationData = { ids: string[] @@ -203,6 +210,9 @@ type ItemReducerAction = | CreateItemOrderTradeRequestAction | CreateItemOrderTradeFailureAction | CreateItemOrderTradeSuccessAction + | CancelItemOrderTradeRequestAction + | CancelItemOrderTradeSuccessAction + | CancelItemOrderTradeFailureAction export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReducerAction): ItemState { switch (action.type) { @@ -225,17 +235,29 @@ export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReduce case RESET_ITEM_REQUEST: case RESCUE_ITEMS_REQUEST: case DOWNLOAD_ITEM_REQUEST: - case CREATE_ITEM_ORDER_TRADE_REQUEST: { + case CREATE_ITEM_ORDER_TRADE_REQUEST: + case CANCEL_ITEM_ORDER_TRADE_REQUEST: { return { ...state, loading: loadingReducer(state.loading, action) } } case CREATE_ITEM_ORDER_TRADE_SUCCESS: { + const { trade, item, priceInWei, expiresAt, beneficiary } = action.payload return { ...state, error: null, - loading: loadingReducer(state.loading, action) + loading: loadingReducer(state.loading, action), + data: { + ...state.data, + [item.id]: { + ...state.data[item.id], + tradeId: trade.id, + price: priceInWei, + beneficiary: beneficiary, + tradeExpiresAt: expiresAt + } + } } } case FETCH_ORPHAN_ITEM_REQUEST: { @@ -346,7 +368,8 @@ export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReduce case RESET_ITEM_FAILURE: case RESCUE_ITEMS_FAILURE: case DOWNLOAD_ITEM_FAILURE: - case CREATE_ITEM_ORDER_TRADE_FAILURE: { + case CREATE_ITEM_ORDER_TRADE_FAILURE: + case CANCEL_ITEM_ORDER_TRADE_FAILURE: { return { ...state, loading: loadingReducer(state.loading, action), @@ -569,6 +592,28 @@ export function itemReducer(state: ItemState = INITIAL_STATE, action: ItemReduce }, {} as ItemState['data']) } } + case CANCEL_ITEM_ORDER_TRADE_SUCCESS: { + const { tradeId } = action.payload + return { + ...state, + loading: loadingReducer(state.loading, action), + error: null, + data: Object.values(state.data).reduce((accum, item) => { + if (item.tradeId === tradeId) { + accum[item.id] = { + ...item, + tradeId: undefined, + price: ethers.constants.MaxUint256.toString(), + beneficiary: undefined, + tradeExpiresAt: undefined + } + } else { + accum[item.id] = item + } + return accum + }, {} as ItemState['data']) + } + } default: return state } diff --git a/src/modules/item/sagas.ts b/src/modules/item/sagas.ts index a27debd18..61addecd2 100644 --- a/src/modules/item/sagas.ts +++ b/src/modules/item/sagas.ts @@ -1,10 +1,10 @@ import PQueue from 'p-queue' import { History } from 'history' -import { Contract, providers } from 'ethers' +import { Contract, ethers, providers } from 'ethers' import { LOCATION_CHANGE } from 'connected-react-router' import { takeEvery, call, put, takeLatest, select, take, delay, fork, race, cancelled, getContext } from 'redux-saga/effects' import { channel } from 'redux-saga' -import { ChainId, Network, Entity, EntityType, WearableCategory, TradeCreation } from '@dcl/schemas' +import { ChainId, Network, Entity, EntityType, WearableCategory, TradeCreation, Trade, Item as DCLItem } from '@dcl/schemas' import { ContractName, getContract } from 'decentraland-transactions' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { ModalState } from 'decentraland-dapps/dist/modules/modal/reducer' @@ -103,7 +103,11 @@ import { CreateItemOrderTradeRequestAction, createItemOrderTradeSuccess, createItemOrderTradeFailure, - CREATE_ITEM_ORDER_TRADE_REQUEST + CREATE_ITEM_ORDER_TRADE_REQUEST, + CANCEL_ITEM_ORDER_TRADE_REQUEST, + cancelItemOrderTradeSuccess, + cancelItemOrderTradeFailure, + CancelItemOrderTradeRequestAction } from './actions' import { fromRemoteItem } from 'lib/api/transformations' import { isThirdParty } from 'lib/urn' @@ -157,6 +161,7 @@ import { import { ItemPaginationData } from './reducer' import { getSuccessfulDeletedItemToast, getSuccessfulMoveItemToAnotherCollectionToast } from './toasts' import { TradeService } from 'decentraland-dapps/dist/modules/trades/TradeService' +import { marketplace } from 'lib/api/marketplace' export const SAVE_AND_EDIT_FILES_BATCH_SIZE = 8 @@ -185,6 +190,7 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien yield takeEvery(createOrEditProgressChannel, handleCreateOrEditProgress) yield takeEvery(createOrEditCancelledItemsChannel, handleCreateOrEditCancelledItems) yield takeEvery(CREATE_ITEM_ORDER_TRADE_REQUEST, handleCreateItemOrderTradeRequest) + yield takeEvery(CANCEL_ITEM_ORDER_TRADE_REQUEST, handleCancelItemOrderTradeRequest) yield takeLatestCancellable( { initializer: SAVE_MULTIPLE_ITEMS_REQUEST, cancellable: CANCEL_SAVE_MULTIPLE_ITEMS }, handleSaveMultipleItemsRequest @@ -194,9 +200,9 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien function* handleCreateItemOrderTradeRequest(action: CreateItemOrderTradeRequestAction) { const { item, beneficiary, priceInWei, collection, expiresAt } = action.payload try { - const trade: TradeCreation = yield call(createItemOrderTrade, item, priceInWei, beneficiary, collection, expiresAt) - yield call([tradeService, 'addTrade'], trade) - yield put(createItemOrderTradeSuccess(trade)) + const tradeToCreate: TradeCreation = yield call(createItemOrderTrade, item, priceInWei, beneficiary, collection, expiresAt) + const trade: Trade = yield call([tradeService, 'addTrade'], tradeToCreate) + yield put(createItemOrderTradeSuccess(trade, item, priceInWei, beneficiary, expiresAt.getTime())) yield put(closeAllModals()) } catch (error) { yield put(createItemOrderTradeFailure(isErrorWithMessage(error) ? error.message : 'Unknown error')) @@ -283,7 +289,22 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien isFetchingMultiplePages ? page : [page], restOfOptions ) - yield put(fetchCollectionItemsSuccess(collectionId, items, overridePaginationData ? paginationStats : undefined)) + let itemsToSave = items + const collection: Collection | undefined = collectionId ? yield select(getCollection, collectionId) : undefined + if (collection && collection.isPublished) { + const result: { data: DCLItem[] } = yield call([marketplace, 'fetchCollectionItems'], collection.contractAddress!) + itemsToSave = items.map(item => { + const publishedItem = result.data.find(publishedItem => publishedItem.id === `${collection.contractAddress}-${item.tokenId}`) + return { + ...item, + tradeExpiresAt: publishedItem?.tradeExpiresAt, + tradeId: publishedItem?.tradeId, + beneficiary: publishedItem?.beneficiary || item.beneficiary, + price: publishedItem?.price || item.price + } + }) + } + yield put(fetchCollectionItemsSuccess(collectionId, itemsToSave, overridePaginationData ? paginationStats : undefined)) } catch (error) { yield put(fetchCollectionItemsFailure(collectionId, isErrorWithMessage(error) ? error.message : 'Unknown error')) } @@ -818,6 +839,29 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien yield put(downloadItemFailure(itemId, isErrorWithMessage(error) ? error.message : 'Unknown error')) } } + + function* handleCancelItemOrderTradeRequest(action: CancelItemOrderTradeRequestAction) { + const { tradeId, errorToast } = action.payload + try { + const trade: Trade = yield call([tradeService, 'fetchTrade'], tradeId) + yield call([tradeService, 'cancel'], trade, ethers.constants.AddressZero) + yield put(cancelItemOrderTradeSuccess(tradeId)) + } catch (error) { + const errorMessage = isErrorWithMessage(error) ? error.message : 'Unknown error' + yield put(cancelItemOrderTradeFailure(errorMessage)) + if (errorToast) { + yield put( + showToast({ + type: ToastType.ERROR, + title: t('collection_item.cancel_order_error'), + body: errorMessage, + timeout: 10000, + closable: true + }) + ) + } + } + } } export function* handleResetItemRequest(action: ResetItemRequestAction) { diff --git a/src/modules/item/types.ts b/src/modules/item/types.ts index 479d8aa01..7abd5f989 100644 --- a/src/modules/item/types.ts +++ b/src/modules/item/types.ts @@ -124,6 +124,8 @@ export type Item = Omit & { metrics: T extends ItemType.WEARABLE ? ModelMetrics : AnimationMetrics mappings: Partial>> | null isMappingComplete?: boolean + tradeId?: string + tradeExpiresAt?: number } export const isEmoteItemType = (item: Item | Item): item is Item => diff --git a/src/modules/translation/languages/en.json b/src/modules/translation/languages/en.json index a5bb590e0..3d3857be3 100644 --- a/src/modules/translation/languages/en.json +++ b/src/modules/translation/languages/en.json @@ -1737,7 +1737,9 @@ "reset_item": "Reset changes", "move_to_another_collection": "Move to another collection", "preview": "Preview in Editor", - "put_for_sale": "Put up for sale" + "put_for_sale": "Put up for sale", + "remove_from_sale": "Remove from sale", + "cancel_order_error": "Error when removing order for item" }, "collection": { "type": { @@ -2387,5 +2389,11 @@ "push_changes_modal": { "title": "Push Changes", "description": "Changes have been made to the collection or the items contained by it since the last time a curator has reviewed them.{br}In order to have this changes reflected in world, they have to be reviewed and approved again by the committee.{br}Are you sure you want to push the changes for review?" + }, + "put_for_sale_offchain_modal": { + "title": "Action needed", + "subtitle": "In order to update the new price, you have to remove the previous sale first", + "cancel": "Cancel", + "reject_old_prices": "Reject old prices" } } diff --git a/src/modules/translation/languages/es.json b/src/modules/translation/languages/es.json index 06343c047..f04d77d05 100644 --- a/src/modules/translation/languages/es.json +++ b/src/modules/translation/languages/es.json @@ -1746,7 +1746,9 @@ "reset_item": "Deshacer cambios", "move_to_another_collection": "Mover a otra colección", "preview": "Preview en el Editor", - "put_for_sale": "Poner en venta" + "put_for_sale": "Poner en venta", + "remove_from_sale": "Eliminar de la venta", + "cancel_order_error": "Error al eliminar el orden para el artículo" }, "collection": { "type": { @@ -2405,5 +2407,11 @@ "push_changes_modal": { "title": "Enviar Cambios", "description": "Se han realizado cambios en la colección o en los elementos que contiene desde la última vez que un curador los revisó.{br}Para que estos cambios se reflejen en el mundo, el comité debe revisarlos y aprobarlos nuevamente.{br}¿Está seguro de que desea enviar los cambios para su revisión?" + }, + "put_for_sale_offchain_modal": { + "title": "Se necesita acción", + "subtitle": "Para actualizar el nuevo precio, debe eliminar los precios antiguos primero", + "cancel": "Cancelar", + "reject_old_prices": "Rechazar el precio anterior" } } diff --git a/src/modules/translation/languages/zh.json b/src/modules/translation/languages/zh.json index 3070eb835..1ab0b5989 100644 --- a/src/modules/translation/languages/zh.json +++ b/src/modules/translation/languages/zh.json @@ -1725,7 +1725,9 @@ "reset_item": "撤消", "move_to_another_collection": "移动到另一个集合", "preview": "在编辑器中预览", - "put_for_sale": "出售" + "put_for_sale": "出售", + "remove_from_sale": "删除出售", + "cancel_order_error": "删除项目订单时出错" }, "collection": { "type": { @@ -2386,5 +2388,11 @@ "push_changes_modal": { "title": "推送变更", "description": "自上次策展人审核以来,馆藏或其包含的物品已发生更改。{br}为了使这些更改反映在世界上,委员会必须再次审核和批准这些更改。{br }您确定要推送更改以供审核吗?" + }, + "put_for_sale_offchain_modal": { + "title": "需要采取行动", + "subtitle": "为了更新新价格,您必须首先删除先前的销售", + "cancel": "取消", + "reject_old_prices": "拒绝旧价格" } } From 7afc09a8408ae9850390e96db6cb33ae906d4add Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Fri, 4 Oct 2024 17:48:30 -0300 Subject: [PATCH 4/7] fix: remove from sale --- package-lock.json | 8 ++++---- package.json | 2 +- .../CollectionItem/CollectionItem.tsx | 3 ++- src/modules/item/actions.ts | 4 ++++ src/modules/item/sagas.ts | 14 +++++++++++--- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae74b7b24..a0aec3c1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "decentraland-dapps": "^23.6.1", "decentraland-ecs": "6.12.4-7784644013.commit-f770b3e", "decentraland-experiments": "^1.0.2", - "decentraland-transactions": "^2.15.0", + "decentraland-transactions": "^2.16.0", "decentraland-ui": "^6.9.2", "ethers": "^5.6.8", "file-saver": "^2.0.1", @@ -11944,9 +11944,9 @@ } }, "node_modules/decentraland-transactions": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/decentraland-transactions/-/decentraland-transactions-2.15.0.tgz", - "integrity": "sha512-vWKaxCldMRc2Uy5kUJnV9ggIyQqGWn766wJr+X+/FeXgjtDvBNMLSdgd2EolbxnZ+DHLWCXsDBmJviRdeJ7d3Q==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/decentraland-transactions/-/decentraland-transactions-2.16.0.tgz", + "integrity": "sha512-rvpRnL+qMNkD9cSj6UUb+QsYNEJE57Tr/YuM3IzBpdxdlzlUr4NBkvP5p/ePnbarixgK39iciKhLo7P/t53TkQ==", "dependencies": { "@0xsquid/sdk": "^2.8.13", "@0xsquid/squid-types": "^0.1.78", diff --git a/package.json b/package.json index bd8c7ad89..d8eca1a48 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "decentraland-dapps": "^23.6.1", "decentraland-ecs": "6.12.4-7784644013.commit-f770b3e", "decentraland-experiments": "^1.0.2", - "decentraland-transactions": "^2.15.0", + "decentraland-transactions": "^2.16.0", "decentraland-ui": "^6.9.2", "ethers": "^5.6.8", "file-saver": "^2.0.1", diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx index 193ede27d..03cab8283 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx @@ -39,7 +39,8 @@ export default function CollectionItem({ const history = useHistory() const isOnSaleLegacy = wallet && isOnSale(collection, wallet) const isEnableForSaleOffchainMarketplace = wallet && isOffchainPublicItemOrdersEnabled && isEnableForSaleOffchain(collection, wallet) - const shouldAllowPriceEdition = !isOffchainPublicItemOrdersEnabled || isEnableForSaleOffchainMarketplace || isOnSaleLegacy + const shouldAllowPriceEdition = + !isOffchainPublicItemOrdersEnabled || (isEnableForSaleOffchainMarketplace && item.tradeId) || isOnSaleLegacy const handleEditPriceAndBeneficiary = useCallback(() => { if (isOffchainPublicItemOrdersEnabled && isEnableForSaleOffchainMarketplace) { diff --git a/src/modules/item/actions.ts b/src/modules/item/actions.ts index 96883ffeb..388eb6b0c 100644 --- a/src/modules/item/actions.ts +++ b/src/modules/item/actions.ts @@ -298,8 +298,12 @@ export const CANCEL_ITEM_ORDER_TRADE_REQUEST = '[Request] Cancel Item Order Trad export const CANCEL_ITEM_ORDER_TRADE_SUCCESS = '[Success] Cancel Item Order Trade' export const CANCEL_ITEM_ORDER_TRADE_FAILURE = '[Failure] Cancel Item Order Trade' +export const CANCEL_ITEM_ORDER_TRADE_TX_SUCCESS = '[Success] Cancel Item Order Trade Tx' + export const cancelItemOrderTradeRequest = (tradeId: string, errorToast = false) => action(CANCEL_ITEM_ORDER_TRADE_REQUEST, { tradeId, errorToast }) +export const cancelItemOrderTradeTxSuccess = (trade: Trade, txHash: string) => + action(CANCEL_ITEM_ORDER_TRADE_TX_SUCCESS, buildTransactionPayload(trade.chainId, txHash, { tradeId: trade.id })) export const cancelItemOrderTradeSuccess = (tradeId: string) => action(CANCEL_ITEM_ORDER_TRADE_SUCCESS, { tradeId }) export const cancelItemOrderTradeFailure = (error: string) => action(CANCEL_ITEM_ORDER_TRADE_FAILURE, { error }) diff --git a/src/modules/item/sagas.ts b/src/modules/item/sagas.ts index 61addecd2..2aea3f749 100644 --- a/src/modules/item/sagas.ts +++ b/src/modules/item/sagas.ts @@ -107,7 +107,8 @@ import { CANCEL_ITEM_ORDER_TRADE_REQUEST, cancelItemOrderTradeSuccess, cancelItemOrderTradeFailure, - CancelItemOrderTradeRequestAction + CancelItemOrderTradeRequestAction, + cancelItemOrderTradeTxSuccess } from './actions' import { fromRemoteItem } from 'lib/api/transformations' import { isThirdParty } from 'lib/urn' @@ -290,7 +291,12 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien restOfOptions ) let itemsToSave = items - const collection: Collection | undefined = collectionId ? yield select(getCollection, collectionId) : undefined + let collection: Collection | undefined = yield select(getCollection, collectionId) + + if (!collection) { + collection = yield call([legacyBuilder, 'fetchCollection'], collectionId) + } + if (collection && collection.isPublished) { const result: { data: DCLItem[] } = yield call([marketplace, 'fetchCollectionItems'], collection.contractAddress!) itemsToSave = items.map(item => { @@ -844,7 +850,9 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien const { tradeId, errorToast } = action.payload try { const trade: Trade = yield call([tradeService, 'fetchTrade'], tradeId) - yield call([tradeService, 'cancel'], trade, ethers.constants.AddressZero) + const txHash: string = yield call([tradeService, 'cancel'], trade, ethers.constants.AddressZero) + yield put(cancelItemOrderTradeTxSuccess(trade, txHash)) + yield call(waitForTx, txHash) yield put(cancelItemOrderTradeSuccess(tradeId)) } catch (error) { const errorMessage = isErrorWithMessage(error) ? error.message : 'Unknown error' From 8d4ea27c70d81da7d156a2663c8abfaf2f80f913 Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Fri, 4 Oct 2024 18:06:15 -0300 Subject: [PATCH 5/7] fix beneficiary --- .../Modals/CreateSingleItemModal/CreateSingleItemModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx index 1222fa6a6..17ac08824 100644 --- a/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx +++ b/src/components/Modals/CreateSingleItemModal/CreateSingleItemModal.tsx @@ -363,7 +363,7 @@ export default class CreateSingleItemModal extends React.PureComponent Date: Sun, 6 Oct 2024 23:06:37 -0300 Subject: [PATCH 6/7] fix: tests --- src/modules/item/sagas.spec.ts | 5 ++++- src/modules/item/sagas.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/item/sagas.spec.ts b/src/modules/item/sagas.spec.ts index b6053c0f2..b83f8dc95 100644 --- a/src/modules/item/sagas.spec.ts +++ b/src/modules/item/sagas.spec.ts @@ -105,7 +105,8 @@ const builderAPI = { fetchContents: jest.fn(), fetchCollectionItems: jest.fn(), fetchRarities: jest.fn(), - fetchItems: jest.fn() + fetchItems: jest.fn(), + fetchCollection: jest.fn() } as unknown as BuilderAPI let builderClient: BuilderClient @@ -1511,6 +1512,7 @@ describe('when handling the fetch of collection items', () => { }) it('should put a fetchCollectionItemsSuccess action with items and pagination data', () => { return expectSaga(itemSaga, builderAPI, builderClient, tradeService) + .provide([[select(getCollection, item.collectionId!), { isPublished: false } as Collection]]) .dispatch(fetchCollectionItemsRequest(item.collectionId!, { page: 1, limit: paginationData.limit })) .put( fetchCollectionItemsSuccess(item.collectionId!, [item], { @@ -1557,6 +1559,7 @@ describe('when handling the fetch of collection items pages', () => { }) it('should put a fetchCollectionItemsSuccess action with items and pagination data', () => { return expectSaga(itemSaga, builderAPI, builderClient, tradeService) + .provide([[select(getCollection, item.collectionId!), { isPublished: false } as Collection]]) .dispatch( fetchCollectionItemsRequest(item.collectionId!, { page: [1], limit: paginationData.limit, overridePaginationData: false }) ) diff --git a/src/modules/item/sagas.ts b/src/modules/item/sagas.ts index 2aea3f749..9123d709a 100644 --- a/src/modules/item/sagas.ts +++ b/src/modules/item/sagas.ts @@ -300,7 +300,7 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien if (collection && collection.isPublished) { const result: { data: DCLItem[] } = yield call([marketplace, 'fetchCollectionItems'], collection.contractAddress!) itemsToSave = items.map(item => { - const publishedItem = result.data.find(publishedItem => publishedItem.id === `${collection.contractAddress}-${item.tokenId}`) + const publishedItem = result.data.find(publishedItem => publishedItem.id === `${collection?.contractAddress}-${item.tokenId}`) return { ...item, tradeExpiresAt: publishedItem?.tradeExpiresAt, From 067d39a4aad72647a82271caf6ac581803aa3a09 Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Mon, 7 Oct 2024 16:59:58 -0300 Subject: [PATCH 7/7] fix reload when item change --- .../CollectionDetailPage/CollectionItem/CollectionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx index 03cab8283..c7d80643b 100644 --- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx +++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx @@ -83,7 +83,7 @@ export default function CollectionItem({ return } onRemoveFromSale(item.tradeId) - }, []) + }, [item]) const renderPrice = useCallback(() => { if (!item.price) {