diff --git a/package-lock.json b/package-lock.json
index 263b284e7..a0aec3c1b 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",
@@ -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",
@@ -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",
@@ -11943,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 a9e0101d5..d8eca1a48 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",
@@ -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/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..c7d80643b 100644
--- a/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx
+++ b/src/components/CollectionDetailPage/CollectionItem/CollectionItem.tsx
@@ -25,20 +25,29 @@ 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()
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) {
+ onOpenModal('PutForSaleOffchainModal', { itemId: item.id })
+ return
+ }
+
onOpenModal('EditPriceAndBeneficiaryModal', { itemId: item.id })
}, [item, onOpenModal])
@@ -69,6 +78,13 @@ export default function CollectionItem({
onOpenModal('PutForSaleOffchainModal', { itemId: item.id })
}, [])
+ const handleRemoveFromSale = useCallback(() => {
+ if (!item.tradeId) {
+ return
+ }
+ onRemoveFromSale(item.tradeId)
+ }, [item])
+
const renderPrice = useCallback(() => {
if (!item.price) {
return (
@@ -78,12 +94,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 +107,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 +230,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/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 {
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..388eb6b0c 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,27 @@ 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 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 })
+
+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.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 a27debd18..9123d709a 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,12 @@ import {
CreateItemOrderTradeRequestAction,
createItemOrderTradeSuccess,
createItemOrderTradeFailure,
- CREATE_ITEM_ORDER_TRADE_REQUEST
+ CREATE_ITEM_ORDER_TRADE_REQUEST,
+ CANCEL_ITEM_ORDER_TRADE_REQUEST,
+ cancelItemOrderTradeSuccess,
+ cancelItemOrderTradeFailure,
+ CancelItemOrderTradeRequestAction,
+ cancelItemOrderTradeTxSuccess
} from './actions'
import { fromRemoteItem } from 'lib/api/transformations'
import { isThirdParty } from 'lib/urn'
@@ -157,6 +162,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 +191,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 +201,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 +290,27 @@ export function* itemSaga(legacyBuilder: LegacyBuilderAPI, builder: BuilderClien
isFetchingMultiplePages ? page : [page],
restOfOptions
)
- yield put(fetchCollectionItemsSuccess(collectionId, items, overridePaginationData ? paginationStats : undefined))
+ let itemsToSave = items
+ 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 => {
+ 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 +845,31 @@ 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)
+ 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'
+ 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": "拒绝旧价格"
}
}