Skip to content

Commit

Permalink
feat: add accept trade flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Melisa Anabella Rossi committed Jul 16, 2024
1 parent e51db0a commit 42d108c
Show file tree
Hide file tree
Showing 18 changed files with 340 additions and 87 deletions.
41 changes: 37 additions & 4 deletions webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@0xsquid/squid-types": "^0.1.29",
"@covalenthq/client-sdk": "^0.6.4",
"@dcl/crypto": "^3.0.0",
"@dcl/schemas": "^11.10.5",
"@dcl/schemas": "^12.0.0",
"@dcl/single-sign-on-client": "^0.1.0",
"@dcl/ui-env": "^1.5.0",
"@ethersproject/providers": "^5.6.2",
Expand Down
7 changes: 6 additions & 1 deletion webapp/src/components/AssetPage/YourOffer/YourOffer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ const YourOffer = (props: Props) => {
<Button inverted fluid className={styles.actions} onClick={() => onCancel(bid)}>
{t('offers_table.remove')}
</Button>
<Button primary fluid className={styles.actions} onClick={() => history.push(locations.bid(bid.contractAddress, bid.tokenId))}>
<Button
primary
fluid
className={styles.actions}
onClick={() => 'tokenId' in bid && history.push(locations.bid(bid.contractAddress, bid.tokenId))}
>
{t('global.update')}
</Button>
</div>
Expand Down
14 changes: 8 additions & 6 deletions webapp/src/components/Bid/AcceptButton/AcceptButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React, { useEffect, useState } from 'react'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Button, Popup } from 'decentraland-ui'
import { isNFT } from '../../../modules/asset/utils'
import { isInsufficientMANA, checkFingerprint } from '../../../modules/bid/utils'
import { useFingerprint } from '../../../modules/nft/hooks'
import { isLandLocked } from '../../../modules/rental/utils'
import { LandLockedPopup } from '../../LandLockedPopup'
import { Props } from './AcceptButton.types'

const AcceptButton = (props: Props) => {
const { nft, bid, onClick, rental, userAddress } = props
const { asset, bid, onClick, rental, userAddress } = props

const [fingerprint, isLoadingFingerprint] = useFingerprint(nft)
const [fingerprint, isLoadingFingerprint] = useFingerprint(asset && isNFT(asset) ? asset : null)
const [hasInsufficientMANA, setHasInsufficientMANA] = useState(false)
const isCurrentlyLocked = rental && nft && isLandLocked(userAddress, rental, nft)
const isCurrentlyLocked = rental && asset && isLandLocked(userAddress, rental, asset)

useEffect(() => {
isInsufficientMANA(bid)
Expand All @@ -21,9 +22,10 @@ const AcceptButton = (props: Props) => {
}, [bid])

const isValidFingerprint = checkFingerprint(bid, fingerprint)
const isValidSeller = !!nft && nft.owner === bid.seller
const assetOwner = !!asset && (isNFT(asset) ? asset.owner : asset?.creator)
const isValidSeller = assetOwner && assetOwner === userAddress

const isDisabled = isCurrentlyLocked || !nft || isLoadingFingerprint || hasInsufficientMANA || !isValidFingerprint || !isValidSeller
const isDisabled = isCurrentlyLocked || !asset || isLoadingFingerprint || hasInsufficientMANA || !isValidFingerprint || !isValidSeller

let button = (
<Button primary disabled={isDisabled} onClick={onClick}>
Expand Down Expand Up @@ -51,7 +53,7 @@ const AcceptButton = (props: Props) => {
button = <Popup content={t('bid.invalid_seller')} position="top center" trigger={<div className="popup-button">{button}</div>} />
} else if (isCurrentlyLocked) {
button = (
<LandLockedPopup asset={nft} rental={rental} userAddress={userAddress}>
<LandLockedPopup asset={asset} rental={rental} userAddress={userAddress}>
{button}
</LandLockedPopup>
)
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/components/Bid/AcceptButton/AcceptButton.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Bid, RentalListing } from '@dcl/schemas'
import { NFT } from '../../../modules/nft/types'
import { Asset } from '../../../modules/asset/types'

export type Props = {
nft: NFT | null
asset: Asset | null
rental: RentalListing | null
bid: Bid
userAddress: string
Expand Down
7 changes: 6 additions & 1 deletion webapp/src/components/Bid/Bid.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import { connect } from 'react-redux'
import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors'
import { cancelBidRequest, archiveBid, unarchiveBid, acceptBidRequest, ACCEPT_BID_REQUEST } from '../../modules/bid/actions'
import { getLoading } from '../../modules/bid/selectors'
import { getContract } from '../../modules/contract/selectors'
import { getIsBidsOffChainEnabled } from '../../modules/features/selectors'
import { RootState } from '../../modules/reducer'
import { getArchivedBidIds } from '../../modules/ui/nft/bid/selectors'
import { Contract } from '../../modules/vendor/services'
import { getWallet } from '../../modules/wallet/selectors'
import Bid from './Bid'
import { MapStateProps, MapDispatchProps, MapDispatch } from './Bid.types'

const mapState = (state: RootState): MapStateProps => ({
wallet: getWallet(state),
archivedBidIds: getArchivedBidIds(state),
isAcceptingBid: isLoadingType(getLoading(state), ACCEPT_BID_REQUEST)
isAcceptingBid: isLoadingType(getLoading(state), ACCEPT_BID_REQUEST),
isBidsOffchainEnabled: getIsBidsOffChainEnabled(state),
getContract: (query: Partial<Contract>) => getContract(state, query)
})

const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
Expand Down
111 changes: 90 additions & 21 deletions webapp/src/components/Bid/Bid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { Link, useHistory } from 'react-router-dom'
import { ethers } from 'ethers'
import { Contract } from '@dcl/schemas'
import { withAuthorizedAction } from 'decentraland-dapps/dist/containers'
import { AuthorizedAction } from 'decentraland-dapps/dist/containers/withAuthorizedAction/AuthorizationModal'
import { AuthorizationType } from 'decentraland-dapps/dist/modules/authorization'
import { T, t } from 'decentraland-dapps/dist/modules/translation/utils'
import { ContractName, getContract as getDCLContract } from 'decentraland-transactions'
import { Loader, Stats, Button } from 'decentraland-ui'
import { formatDistanceToNow } from '../../lib/date'
import { formatWeiMANA } from '../../lib/mana'
Expand All @@ -15,35 +20,86 @@ import { ConfirmInputValueModal } from '../ConfirmInputValueModal'
import { LinkedProfile } from '../LinkedProfile'
import { Mana } from '../Mana'
import { AcceptButton } from './AcceptButton'
import { fetchContractName } from './utils'
import { WarningMessage } from './WarningMessage'
import { Props } from './Bid.types'
import './Bid.css'

const Bid = (props: Props) => {
const { bid, wallet, archivedBidIds, onAccept, onArchive, onUnarchive, onCancel, isArchivable, hasImage, isAcceptingBid } = props
const {
bid,
wallet,
archivedBidIds,
isBidsOffchainEnabled,
onAuthorizedAction,
onAccept,
onArchive,
onUnarchive,
onCancel,
getContract,
isArchivable,
hasImage,
isAcceptingBid
} = props
const history = useHistory()
const [targetContractLabel, setTargetContractLabel] = useState<string | null>('')

const isArchived = archivedBidIds.includes(bid.id)
const isBidder = !!wallet && addressEquals(wallet.address, bid.bidder)
const isSeller = !!wallet && addressEquals(wallet.address, bid.seller)
const nftContract = getContract({ address: bid.contractAddress, chainId: bid.chainId })

useEffect(() => {
fetchContractName(nftContract)
.then(name => setTargetContractLabel(name))
.catch(() => console.error('Could not fetch contract name'))
}, [nftContract])

const [showConfirmationModal, setShowConfirmationModal] = useState(false)
const handleConfirm = useCallback(() => onAccept(bid), [bid, onAccept])
const handleConfirm = useCallback(() => {
if (isBidsOffchainEnabled && 'tradeId' in bid) {
const offchainMarketplaceContract = getDCLContract(ContractName.OffChainMarketplace, bid.chainId)

if ('tokenId' in bid) {
onAuthorizedAction({
targetContractName: ContractName.ERC721,
targetContractLabel: targetContractLabel || nftContract?.label || nftContract?.name,
authorizedAddress: offchainMarketplaceContract.address,
targetContract: nftContract as Contract,
authorizationType: AuthorizationType.APPROVAL,
authorizedContractLabel: offchainMarketplaceContract.name,
tokenId: bid.tokenId,
onAuthorized: () => onAccept(bid)
})
} else {
console.error('Implement bid acceptance for items')
}
} else {
onAccept(bid)
}
}, [bid, onAccept])
const handleAccept = () => setShowConfirmationModal(true)
const isNftBid = 'tokenId' in bid

return (
<>
<div className="Bid">
<div className="bid-row">
{hasImage ? (
<div className="image">
<AssetProvider type={AssetType.NFT} contractAddress={bid.contractAddress} tokenId={bid.tokenId}>
{(nft, _order, _rental, isLoading) => (
<AssetProvider
type={isNftBid ? AssetType.NFT : AssetType.ITEM}
contractAddress={bid.contractAddress}
tokenId={isNftBid ? bid.tokenId : bid.itemId}
>
{(asset, _order, _rental, isLoading) => (
<>
{!nft && isLoading ? <Loader active /> : null}
{nft ? (
<Link to={locations.nft(bid.contractAddress, bid.tokenId)}>
<AssetImage asset={nft} />{' '}
{!asset && isLoading ? <Loader active /> : null}
{asset ? (
<Link
to={isNftBid ? locations.nft(bid.contractAddress, bid.tokenId) : locations.item(bid.contractAddress, bid.itemId)}
>
<AssetImage asset={asset} />{' '}
</Link>
) : null}
</>
Expand All @@ -65,7 +121,7 @@ const Bid = (props: Props) => {
</div>
{isBidder || isSeller ? (
<div className="actions">
{isBidder ? (
{isBidder && 'bidAddress' in bid ? (
<>
<Button primary onClick={() => history.push(locations.bid(bid.contractAddress, bid.tokenId))}>
{t('global.update')}
Expand All @@ -75,9 +131,13 @@ const Bid = (props: Props) => {
) : null}
{isSeller ? (
<>
<AssetProvider type={AssetType.NFT} contractAddress={bid.contractAddress} tokenId={bid.tokenId}>
{(nft, _order, rental) => (
<AcceptButton userAddress={wallet.address} nft={nft} rental={rental} bid={bid} onClick={handleAccept} />
<AssetProvider
type={isNftBid ? AssetType.NFT : AssetType.ITEM}
contractAddress={bid.contractAddress}
tokenId={isNftBid ? bid.tokenId : bid.itemId}
>
{(asset, _order, rental) => (
<AcceptButton userAddress={wallet.address} asset={asset} rental={rental} bid={bid} onClick={handleAccept} />
)}
</AssetProvider>

Expand All @@ -95,15 +155,19 @@ const Bid = (props: Props) => {
</div>
</div>
{isBidder ? (
<AssetProvider type={AssetType.NFT} contractAddress={bid.contractAddress} tokenId={bid.tokenId}>
<AssetProvider type={AssetType.NFT} contractAddress={bid.contractAddress} tokenId={isNftBid ? bid.tokenId : bid.itemId}>
{nft => <WarningMessage nft={nft} bid={bid} />}
</AssetProvider>
) : null}
</div>
{showConfirmationModal ? (
<AssetProvider type={AssetType.NFT} contractAddress={bid.contractAddress} tokenId={bid.tokenId}>
{nft =>
nft && (
<AssetProvider
type={isNftBid ? AssetType.NFT : AssetType.ITEM}
contractAddress={bid.contractAddress}
tokenId={isNftBid ? bid.tokenId : bid.itemId}
>
{asset =>
asset && (
<ConfirmInputValueModal
open={showConfirmationModal}
headerTitle={t('bid_page.confirm.title')}
Expand All @@ -112,9 +176,9 @@ const Bid = (props: Props) => {
<T
id="bid_page.confirm.accept_bid_line_one"
values={{
name: <b>{getAssetName(nft)}</b>,
name: <b>{getAssetName(asset)}</b>,
amount: (
<Mana showTooltip network={nft.network} inline>
<Mana showTooltip network={asset.network} inline>
{formatWeiMANA(bid.price)}
</Mana>
)
Expand All @@ -126,7 +190,7 @@ const Bid = (props: Props) => {
}
onConfirm={handleConfirm}
valueToConfirm={ethers.utils.formatEther(bid.price)}
network={nft.network}
network={asset.network}
onCancel={() => setShowConfirmationModal(false)}
loading={isAcceptingBid}
disabled={isAcceptingBid}
Expand All @@ -144,4 +208,9 @@ Bid.defaultProps = {
hasImage: true
}

export default React.memo(Bid)
export default withAuthorizedAction(React.memo(Bid), AuthorizedAction.BID, {
confirm_transaction: {
title: 'accept_bid.authorization.confirm_transaction.title'
},
title: 'accept_bid.authorization.title'
})
Loading

0 comments on commit 42d108c

Please sign in to comment.