Skip to content

Commit

Permalink
feat: add enable sales logic (#3192)
Browse files Browse the repository at this point in the history
  • Loading branch information
Melisa Anabella Rossi authored Sep 30, 2024
1 parent 7e9c1f7 commit 2f3a400
Show file tree
Hide file tree
Showing 17 changed files with 182 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DELETE_COLLECTION_REQUEST, SET_COLLECTION_MINTERS_REQUEST } from 'modul
import { openModal } from 'decentraland-dapps/dist/modules/modal/actions'
import { getCollectionItems } from 'modules/item/selectors'
import { getLastLocation } from 'modules/ui/location/selector'
import { getIsOffchainPublicItemOrdersEnabled } from 'modules/features/selectors'
import { fetchCollectionForumPostReplyRequest, FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST } from 'modules/forum/actions'
import { MapStateProps, MapDispatchProps, MapDispatch } from './CollectionDetailPage.types'
import CollectionDetailPage from './CollectionDetailPage'
Expand All @@ -26,7 +27,8 @@ const mapState = (state: RootState): MapStateProps => {
isLoading:
isLoadingType(getLoadingCollection(state), DELETE_COLLECTION_REQUEST) ||
isLoadingType(getLoadingCollection(state), FETCH_COLLECTION_FORUM_POST_REPLY_REQUEST),
lastLocation: getLastLocation(state)
lastLocation: getLastLocation(state),
isOffchainPublicItemOrdersEnabled: getIsOffchainPublicItemOrdersEnabled(state)
}
}

Expand Down
44 changes: 35 additions & 9 deletions src/components/CollectionDetailPage/CollectionDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import classNames from 'classnames'
import { useHistory, useLocation } from 'react-router-dom'
import { Network } from '@dcl/schemas'
import { Section, Row, Narrow, Column, Header, Button, Popup, Tabs, Table, Label, SemanticSIZES } from 'decentraland-ui'
import { Section, Row, Narrow, Column, Header, Button, Popup, Tabs, Table, Label, SemanticSIZES, Icon } from 'decentraland-ui'
import { NetworkCheck } from 'decentraland-dapps/dist/containers'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { locations } from 'routing/locations'
Expand All @@ -13,7 +13,8 @@ import {
getCollectionEditorURL,
isOnSale as isCollectionOnSale,
isLocked as isCollectionLocked,
isOwner
isOwner,
isEnableForSaleOffchain
} from 'modules/collection/utils'
import { CollectionType } from 'modules/collection/types'
import { isEmote, isSmart, isWearable } from 'modules/item/utils'
Expand Down Expand Up @@ -41,6 +42,7 @@ export default function CollectionDetailPage({
status,
onOpenModal,
isLoading,
isOffchainPublicItemOrdersEnabled,
onFetchCollectionForumPostReply
}: Props) {
const location = useLocation()
Expand Down Expand Up @@ -82,7 +84,10 @@ export default function CollectionDetailPage({

const handleOnSaleChange = useCallback(() => {
if (collection) {
onOpenModal('SellCollectionModal', { collectionId: collection.id, isOnSale: isCollectionOnSale(collection, wallet) })
onOpenModal('SellCollectionModal', {
collectionId: collection.id,
isOnSale: isOffchainPublicItemOrdersEnabled ? isEnableForSaleOffchain(collection, wallet) : isCollectionOnSale(collection, wallet)
})
}
}, [collection, wallet, onOpenModal])

Expand Down Expand Up @@ -177,24 +182,42 @@ export default function CollectionDetailPage({
return null
}

const isOnSale = isCollectionOnSale(collection, wallet)
const isOnSaleLegacy = isCollectionOnSale(collection, wallet)
const isEnableForSaleOffchainMarketplace = isEnableForSaleOffchain(collection, wallet)

if (isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy) {
return !isEnableForSaleOffchainMarketplace ? (
<NetworkCheck network={Network.MATIC}>
{isEnabled => (
<Button
className={classNames('action-button', isEnableForSaleOffchainMarketplace ? 'secondary' : 'primary')}
disabled={isOnSaleLoading || !isEnabled}
onClick={handleOnSaleChange}
>
<Icon name="lock" />
<span className="text">{t('collection_detail_page.enable_sales')}</span>
</Button>
)}
</NetworkCheck>
) : null
}

return (
<NetworkCheck network={Network.MATIC}>
{isEnabled => (
<Button
className={classNames('action-button', isOnSale ? 'basic' : 'primary')}
className={classNames('action-button', isOnSaleLegacy ? 'basic' : 'primary')}
disabled={isOnSaleLoading || !isEnabled}
onClick={handleOnSaleChange}
>
<span className="text">
{isOnSale ? t('collection_detail_page.remove_from_marketplace') : t('collection_detail_page.put_for_sale')}
{isOnSaleLegacy ? t('collection_detail_page.remove_from_marketplace') : t('collection_detail_page.put_for_sale')}
</span>
</Button>
)}
</NetworkCheck>
)
}, [collection, handleOnSaleChange])
}, [collection, isOffchainPublicItemOrdersEnabled, handleOnSaleChange])

const renderForumRepliesBadge = useCallback(
(size: SemanticSIZES = 'tiny') => {
Expand Down Expand Up @@ -244,7 +267,7 @@ export default function CollectionDetailPage({

const canMint = canMintCollectionItems(collection, wallet.address)
const isLocked = isCollectionLocked(collection)
const isOnSale = isCollectionOnSale(collection, wallet)
const isOnSaleLegacy = isCollectionOnSale(collection, wallet)
const hasEmotes = items.some(isEmote)
const hasWearables = items.some(isWearable)
const isEmoteMissingPrice = hasEmotes ? items.some(item => isEmote(item) && !item.price) : false
Expand Down Expand Up @@ -274,7 +297,7 @@ export default function CollectionDetailPage({
{collection.name}
</Header>
<BuilderIcon name="edit" className="edit-collection-name" />
{isOnSale ? (
{isOnSaleLegacy ? (
<Label className="badge-on-sale" size="small" circular>
{t('collection_detail_page.on_sale')}
</Label>
Expand Down Expand Up @@ -359,6 +382,9 @@ export default function CollectionDetailPage({
<Table.HeaderCell>{t('collection_detail_page.table.supply')}</Table.HeaderCell>
) : null}
<Table.HeaderCell>{t('collection_detail_page.table.status')}</Table.HeaderCell>
{isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy && (
<Table.HeaderCell>{t('collection_detail_page.table.actions')}</Table.HeaderCell>
)}
<Table.HeaderCell />
</Table.Row>
</Table.Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Props = {
items: Item[]
status: SyncStatus
lastLocation?: string
isOffchainPublicItemOrdersEnabled: boolean
onOpenModal: typeof openModal
onFetchCollectionForumPostReply: typeof fetchCollectionForumPostReplyRequest
}
Expand All @@ -21,6 +22,9 @@ export type State = {
tab: ItemType
}

export type MapStateProps = Pick<Props, 'wallet' | 'collection' | 'isOnSaleLoading' | 'isLoading' | 'items' | 'status' | 'lastLocation'>
export type MapStateProps = Pick<
Props,
'wallet' | 'collection' | 'isOnSaleLoading' | 'isLoading' | 'items' | 'status' | 'lastLocation' | 'isOffchainPublicItemOrdersEnabled'
>
export type MapDispatchProps = Pick<Props, 'onOpenModal' | 'onFetchCollectionForumPostReply'>
export type MapDispatch = Dispatch<OpenModalAction | FetchCollectionForumPostReplyRequestAction>
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import { 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'

const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => {
const statusByItemId = getStatusByItemId(state)

return {
ethAddress: getAddress(state),
status: statusByItemId[ownProps.item.id]
status: statusByItemId[ownProps.item.id],
wallet: getWallet(state),
isOffchainPublicItemOrdersEnabled: getIsOffchainPublicItemOrdersEnabled(state)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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 { isLocked } from 'modules/collection/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'
import ItemStatus from 'components/ItemStatus'
Expand All @@ -22,9 +22,20 @@ import styles from './CollectionItem.module.css'

const LENGTH_LIMIT = 25

export default function CollectionItem({ onOpenModal, onSetItems, item, collection, status, ethAddress }: Props) {
export default function CollectionItem({
onOpenModal,
onSetItems,
item,
isOffchainPublicItemOrdersEnabled,
collection,
status,
ethAddress,
wallet
}: Props) {
analytics = getAnalytics()
const history = useHistory()
const isOnSaleLegacy = wallet && isOnSale(collection, wallet)
const isEnableForSaleOffchainMarketplace = wallet && isOffchainPublicItemOrdersEnabled && isEnableForSaleOffchain(collection, wallet)

const handleEditPriceAndBeneficiary = useCallback(() => {
onOpenModal('EditPriceAndBeneficiaryModal', { itemId: item.id })
Expand Down Expand Up @@ -190,6 +201,13 @@ export default function CollectionItem({ onOpenModal, onSetItems, item, collecti
</Table.Cell>
) : null}
<Table.Cell>{renderItemStatus()}</Table.Cell>
{isOffchainPublicItemOrdersEnabled && !isOnSaleLegacy && (
<Table.Cell>
<Button primary size="tiny" disabled={!isEnableForSaleOffchainMarketplace}>
{t('collection_item.put_for_sale')}
</Button>
</Table.Cell>
)}
<Table.Cell className={styles.contextMenuButton}>{renderItemContextMenu()}</Table.Cell>
</Table.Row>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ 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 { setItems, SetItemsAction } from 'modules/editor/actions'
import { Wallet } from 'decentraland-dapps/dist/modules/wallet'

export type Props = {
ethAddress?: string
collection: Collection
item: Item
status: SyncStatus
isOffchainPublicItemOrdersEnabled: boolean
wallet: Wallet | null
onOpenModal: typeof openModal
onDeleteItem: typeof deleteItemRequest
onSetItems: typeof setItems
}

export type MapStateProps = Pick<Props, 'ethAddress' | 'status'>
export type MapStateProps = Pick<Props, 'ethAddress' | 'status' | 'isOffchainPublicItemOrdersEnabled' | 'wallet'>
export type MapDispatchProps = Pick<Props, 'onOpenModal' | 'onDeleteItem' | 'onSetItems'>
export type MapDispatch = Dispatch<OpenModalAction | DeleteItemRequestAction | SetItemsAction>
export type OwnProps = Pick<Props, 'item'>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { setCollectionMintersRequest, SET_COLLECTION_MINTERS_REQUEST } from 'mod
import { OwnProps, MapStateProps, MapDispatchProps, MapDispatch } from './SellCollectionModal.types'
import SellCollectionModal from './SellCollectionModal'
import { UNSYNCED_STATES } from 'modules/item/utils'
import { getIsOffchainPublicItemOrdersEnabled } from 'modules/features/selectors'

const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => {
const { collectionId } = ownProps.metadata
Expand All @@ -15,7 +16,8 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => {
collection: getCollection(state, collectionId)!,
wallet: getWallet(state)!,
isLoading: isLoadingType(getLoading(state), SET_COLLECTION_MINTERS_REQUEST),
hasUnsyncedItems: UNSYNCED_STATES.has(getStatusByCollectionId(state)[collectionId])
hasUnsyncedItems: UNSYNCED_STATES.has(getStatusByCollectionId(state)[collectionId]),
isOffchainPublicItemOrdersEnabled: getIsOffchainPublicItemOrdersEnabled(state)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ 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 } from 'modules/collection/utils'
import { setOnSale, enableSaleOffchain } from 'modules/collection/utils'
import { Props } from './SellCollectionModal.types'
import './SellCollectionModal.css'

export default class SellCollectionModal extends React.PureComponent<Props> {
handleToggleOnSale = () => {
const { collection, wallet, metadata, onSetMinters } = this.props
onSetMinters(collection, setOnSale(collection, wallet, !metadata.isOnSale))
const { collection, wallet, metadata, isOffchainPublicItemOrdersEnabled, onSetMinters } = this.props
onSetMinters(
collection,
isOffchainPublicItemOrdersEnabled
? enableSaleOffchain(collection, wallet, !metadata.isOnSale)
: setOnSale(collection, wallet, !metadata.isOnSale)
)
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type Props = ModalProps & {
isLoading: boolean
isOnSale: boolean
hasUnsyncedItems: boolean
isOffchainPublicItemOrdersEnabled: boolean
onSetMinters: typeof setCollectionMintersRequest
}

Expand All @@ -19,7 +20,7 @@ export type PublishCollectionModalMetadata = {
isOnSale: boolean
}

export type MapStateProps = Pick<Props, 'collection' | 'wallet' | 'isLoading' | 'hasUnsyncedItems'>
export type MapStateProps = Pick<Props, 'collection' | 'wallet' | 'isLoading' | 'hasUnsyncedItems' | 'isOffchainPublicItemOrdersEnabled'>
export type MapDispatchProps = Pick<Props, 'onSetMinters'>
export type MapDispatch = Dispatch<SetCollectionMintersRequestAction>
export type OwnProps = Pick<Props, 'metadata'>
53 changes: 52 additions & 1 deletion src/modules/collection/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChainId, BodyShape } from '@dcl/schemas'
import * as dappsEth from 'decentraland-dapps/dist/lib/eth'
import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types'
import { buildCatalystItemURN, buildThirdPartyURN } from 'lib/urn'
import { Item } from 'modules/item/types'
import { Collection, CollectionType } from 'modules/collection/types'
Expand All @@ -11,7 +12,10 @@ import {
isTPCollection,
getTPThresholdToReview,
toPaginationStats,
getFiatGatewayCommodityAmount
getFiatGatewayCommodityAmount,
getOffchainSaleAddress,
isEnableForSaleOffchain,
enableSaleOffchain
} from './utils'
import { MAX_TP_ITEMS_TO_REVIEW, MIN_TP_ITEMS_TO_REVIEW, TP_TRESHOLD_TO_REVIEW } from './constants'
import { CollectionPaginationData } from './reducer'
Expand Down Expand Up @@ -319,3 +323,50 @@ describe('when getting the fiat commodity amount', () => {
})
})
})

describe('when getting if a collection is enable for offchain purchases', () => {
describe('when the offchain contract is a minter of the collection', () => {
it('should return true', () => {
const offchainContract = getOffchainSaleAddress(ChainId.MATIC_AMOY)
expect(
isEnableForSaleOffchain(
{ minters: [offchainContract], id: '1' } as Collection,
{ networks: { MATIC: { chainId: ChainId.MATIC_AMOY } } } as Wallet
)
).toBe(true)
})
})

describe('when the offchain contract is not a minter of the collection', () => {
it('should return true', () => {
expect(
isEnableForSaleOffchain(
{ minters: [], id: '1' } as unknown as Collection,
{ networks: { MATIC: { chainId: ChainId.MATIC_AMOY } } } as Wallet
)
).toBe(false)
})
})
})

describe('when toggling the permissions for the offchain marketplace contract', () => {
describe('and the user wants to enable the contract', () => {
it('should return the correct set of permissions', () => {
const address = getOffchainSaleAddress(ChainId.MATIC_AMOY)
const collection = { id: 'id' } as Collection
expect(enableSaleOffchain(collection, { networks: { MATIC: { chainId: ChainId.MATIC_AMOY } } } as Wallet, true)).toEqual([
{ address, hasAccess: true, collection }
])
})
})

describe('and the user wants to disable the contract', () => {
it('should return the correct set of permissions', () => {
const address = getOffchainSaleAddress(ChainId.MATIC_AMOY)
const collection = { id: 'id' } as Collection
expect(enableSaleOffchain(collection, { networks: { MATIC: { chainId: ChainId.MATIC_AMOY } } } as Wallet, false)).toEqual([
{ address, hasAccess: false, collection }
])
})
})
})
14 changes: 14 additions & 0 deletions src/modules/collection/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export function setOnSale(collection: Collection, wallet: Wallet, isOnSale: bool
return [{ address, hasAccess: isOnSale, collection }]
}

export function enableSaleOffchain(collection: Collection, wallet: Wallet, isOnSale: boolean): Access[] {
const address = getOffchainSaleAddress(wallet.networks.MATIC.chainId)
return [{ address, hasAccess: isOnSale, collection }]
}

export function isEnableForSaleOffchain(collection: Collection, wallet: Wallet) {
const address = getOffchainSaleAddress(wallet.networks.MATIC.chainId)
return includes(collection.minters, address)
}

export function isOnSale(collection: Collection, wallet: Wallet) {
const address = getSaleAddress(wallet.networks.MATIC.chainId)
return includes(collection.minters, address)
Expand All @@ -39,6 +49,10 @@ export function getSaleAddress(chainId: ChainId) {
return getContract(ContractName.CollectionStore, chainId).address.toLowerCase()
}

export function getOffchainSaleAddress(chainId: ChainId) {
return getContract(ContractName.OffChainMarketplace, chainId).address.toLowerCase()
}

export function getCollectionEditorURL(collection: Collection, items: Item[]): string {
return locations.itemEditor({
collectionId: collection.id,
Expand Down
Loading

0 comments on commit 2f3a400

Please sign in to comment.