chore: Refactor ListingButton with deprecation of Listing V1 (#6023)
* remove unused wrapper * remove old listing modal * simplify button styling * deprecate outdated listing logic and move button to styled components * remove unused linting protection * remove unused functions from sellAssets hook * undo and save this refactor for different PR * remove more unused items * hook dependencies * more trash cleanup * styled continue button * slight continue button tweaks * cleanup * add new standard hover state * no mixed conditionals * lint --------- Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
parent
b1e6d0ab7a
commit
1df9da9eff
@ -1,4 +1,5 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@ -6,12 +7,10 @@ import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRout
|
||||
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
|
||||
import { BagFooter } from 'nft/components/bag/BagFooter'
|
||||
import ListingModal from 'nft/components/bag/profile/ListingModal'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Column } from 'nft/components/Flex'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { buttonTextMedium, commonButtonStyles } from 'nft/css/common.css'
|
||||
import {
|
||||
useBag,
|
||||
useIsMobile,
|
||||
@ -89,6 +88,24 @@ const DetailsPageBackground = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContinueButton = styled.div`
|
||||
background: ${({ theme }) => theme.accentAction};
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
margin: 32px 28px 16px;
|
||||
padding: 10px 0px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
transition: ${({ theme }) => theme.transition.duration.medium};
|
||||
|
||||
:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
`
|
||||
|
||||
const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
|
||||
<Box
|
||||
marginX="24"
|
||||
@ -113,10 +130,7 @@ const Bag = () => {
|
||||
shallow
|
||||
)
|
||||
|
||||
const { profilePageState, setProfilePageState } = useProfilePageState(
|
||||
({ setProfilePageState, state }) => ({ profilePageState: state, setProfilePageState }),
|
||||
shallow
|
||||
)
|
||||
const { setProfilePageState } = useProfilePageState(({ setProfilePageState }) => ({ setProfilePageState }))
|
||||
|
||||
const {
|
||||
bagStatus,
|
||||
@ -396,8 +410,6 @@ const Bag = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<BagContainer data-testid="nft-bag" raiseZIndex={isMobile || isModalOpen} isProfilePage={isProfilePage}>
|
||||
{!(isProfilePage && profilePageState === ProfilePageStateType.LISTING) ? (
|
||||
<>
|
||||
<BagHeader
|
||||
numberOfAssets={isProfilePage ? sellAssets.length : itemsInBag.length}
|
||||
closeBag={handleCloseBag}
|
||||
@ -413,15 +425,7 @@ const Bag = () => {
|
||||
<BagFooter totalEthPrice={totalEthPrice} fetchAssets={fetchAssets} eventProperties={eventProperties} />
|
||||
)}
|
||||
{isSellingAssets && isProfilePage && (
|
||||
<Box
|
||||
marginTop="32"
|
||||
marginX="28"
|
||||
marginBottom="16"
|
||||
paddingY="10"
|
||||
className={`${buttonTextMedium} ${commonButtonStyles}`}
|
||||
backgroundColor="accentAction"
|
||||
color="white"
|
||||
textAlign="center"
|
||||
<ContinueButton
|
||||
onClick={() => {
|
||||
toggleBag()
|
||||
setProfilePageState(ProfilePageStateType.LISTING)
|
||||
@ -432,12 +436,8 @@ const Bag = () => {
|
||||
})
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ListingModal />
|
||||
<Trans>Continue</Trans>
|
||||
</ContinueButton>
|
||||
)}
|
||||
</BagContainer>
|
||||
|
||||
|
@ -1,269 +0,0 @@
|
||||
import { Plural, t, Trans } from '@lingui/macro'
|
||||
import ms from 'ms.macro'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
|
||||
import { useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { pluralize } from 'nft/utils/roundAndPluralize'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import * as styles from './ListingModal.css'
|
||||
import { getListings } from './utils'
|
||||
|
||||
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
|
||||
|
||||
interface ListingButtonProps {
|
||||
onClick: () => void
|
||||
buttonText: string
|
||||
showWarningOverride?: boolean
|
||||
}
|
||||
|
||||
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
|
||||
const {
|
||||
addMarketplaceWarning,
|
||||
sellAssets,
|
||||
removeAllMarketplaceWarnings,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
issues,
|
||||
setIssues,
|
||||
} = useSellAsset(
|
||||
({
|
||||
addMarketplaceWarning,
|
||||
sellAssets,
|
||||
removeAllMarketplaceWarnings,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
issues,
|
||||
setIssues,
|
||||
}) => ({
|
||||
addMarketplaceWarning,
|
||||
sellAssets,
|
||||
removeAllMarketplaceWarnings,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
issues,
|
||||
setIssues,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const { listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
|
||||
({ listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const [showWarning, setShowWarning] = useState(false)
|
||||
const [canContinue, setCanContinue] = useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
|
||||
setListings(newListings)
|
||||
setCollectionsRequiringApproval(newCollectionsToApprove)
|
||||
setListingStatus(ListingStatus.DEFINED)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sellAssets])
|
||||
|
||||
const [
|
||||
noMarketplacesSelected,
|
||||
missingExpiration,
|
||||
invalidExpiration,
|
||||
overMaxExpiration,
|
||||
listingsMissingPrice,
|
||||
listingsBelowFloor,
|
||||
listingsAboveSellOrderFloor,
|
||||
invalidPrices,
|
||||
] = useMemo(() => {
|
||||
const noMarketplacesSelected = sellAssets.some((asset: WalletAsset) => asset.marketplaces === undefined)
|
||||
const missingExpiration = sellAssets.some((asset) => {
|
||||
return (
|
||||
asset.expirationTime != null &&
|
||||
(isNaN(asset.expirationTime) || asset.expirationTime * 1000 - Date.now() < ms`60 seconds`)
|
||||
)
|
||||
})
|
||||
const invalidExpiration = sellAssets.some((asset) => {
|
||||
return asset.expirationTime != null && isNaN(asset.expirationTime)
|
||||
})
|
||||
const overMaxExpiration = sellAssets.some((asset) => {
|
||||
return asset.expirationTime != null && asset.expirationTime * 1000 - Date.now() > ms`180 days`
|
||||
})
|
||||
|
||||
const listingsMissingPrice: [WalletAsset, Listing][] = []
|
||||
const listingsBelowFloor: [WalletAsset, Listing][] = []
|
||||
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
|
||||
const invalidPrices: [WalletAsset, Listing][] = []
|
||||
for (const asset of sellAssets) {
|
||||
if (asset.newListings) {
|
||||
for (const listing of asset.newListings) {
|
||||
if (!listing.price) listingsMissingPrice.push([asset, listing])
|
||||
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
|
||||
else if (
|
||||
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
|
||||
!listing.overrideFloorPrice
|
||||
)
|
||||
listingsBelowFloor.push([asset, listing])
|
||||
else if (asset.floor_sell_order_price && listing.price >= asset.floor_sell_order_price)
|
||||
listingsAboveSellOrderFloor.push([asset, listing])
|
||||
}
|
||||
}
|
||||
}
|
||||
// set number of issues
|
||||
|
||||
const foundIssues =
|
||||
Number(missingExpiration) +
|
||||
Number(overMaxExpiration) +
|
||||
listingsMissingPrice.length +
|
||||
listingsAboveSellOrderFloor.length
|
||||
setIssues(foundIssues)
|
||||
!foundIssues && showResolveIssues && toggleShowResolveIssues()
|
||||
// Only show Resolve Issue text if there was a user submitted error (ie not when page loads with no prices set)
|
||||
if ((missingExpiration || overMaxExpiration || listingsAboveSellOrderFloor.length) && !showResolveIssues)
|
||||
toggleShowResolveIssues()
|
||||
|
||||
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
|
||||
setCanContinue(continueCheck)
|
||||
return [
|
||||
noMarketplacesSelected,
|
||||
missingExpiration,
|
||||
invalidExpiration,
|
||||
overMaxExpiration,
|
||||
listingsMissingPrice,
|
||||
listingsBelowFloor,
|
||||
listingsAboveSellOrderFloor,
|
||||
invalidPrices,
|
||||
]
|
||||
}, [sellAssets, setIssues, showResolveIssues, toggleShowResolveIssues])
|
||||
|
||||
const [disableListButton, warningMessage] = useMemo(() => {
|
||||
const disableListButton =
|
||||
noMarketplacesSelected ||
|
||||
missingExpiration ||
|
||||
invalidExpiration ||
|
||||
overMaxExpiration ||
|
||||
invalidPrices.length > 0 ||
|
||||
listingsMissingPrice.length > 0
|
||||
|
||||
const warningMessage = noMarketplacesSelected
|
||||
? 'No marketplaces selected'
|
||||
: missingExpiration
|
||||
? 'Set duration'
|
||||
: invalidExpiration
|
||||
? 'Invalid duration'
|
||||
: overMaxExpiration
|
||||
? 'Max duration is 6 months'
|
||||
: listingsMissingPrice.length > 0
|
||||
? `${listingsMissingPrice.length} item price${pluralize(listingsMissingPrice.length)} not set`
|
||||
: invalidPrices.length > 0
|
||||
? `${invalidPrices.length} price${pluralize(invalidPrices.length)} are invalid`
|
||||
: listingsBelowFloor.length > 0
|
||||
? `${listingsBelowFloor.length} item${pluralize(listingsBelowFloor.length)} listed below floor`
|
||||
: listingsAboveSellOrderFloor.length > 0
|
||||
? `${listingsAboveSellOrderFloor.length} item${pluralize(listingsAboveSellOrderFloor.length)} already listed`
|
||||
: ''
|
||||
return [disableListButton, warningMessage]
|
||||
}, [
|
||||
noMarketplacesSelected,
|
||||
missingExpiration,
|
||||
invalidExpiration,
|
||||
overMaxExpiration,
|
||||
listingsMissingPrice,
|
||||
invalidPrices,
|
||||
listingsBelowFloor,
|
||||
listingsAboveSellOrderFloor,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
setShowWarning(false)
|
||||
}, [warningMessage])
|
||||
|
||||
const addWarningMessages = () => {
|
||||
removeAllMarketplaceWarnings()
|
||||
if (!missingExpiration && !noMarketplacesSelected) {
|
||||
if (listingsMissingPrice.length > 0) {
|
||||
for (const [asset, listing] of listingsMissingPrice) {
|
||||
addMarketplaceWarning(asset, {
|
||||
message: 'PLEASE SET A PRICE',
|
||||
marketplace: listing.marketplace,
|
||||
})
|
||||
}
|
||||
} else if (invalidPrices.length > 0) {
|
||||
for (const [asset, listing] of invalidPrices) {
|
||||
!listing.overrideFloorPrice &&
|
||||
addMarketplaceWarning(asset, {
|
||||
message: `INVALID PRICE`,
|
||||
marketplace: listing.marketplace,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
setShowWarning(true)
|
||||
}
|
||||
|
||||
const warningWrappedClick = () => {
|
||||
if ((!disableListButton && canContinue) || showWarningOverride) {
|
||||
if (issues) !showResolveIssues && toggleShowResolveIssues()
|
||||
else if (listingsBelowFloor.length) setShowWarning(true)
|
||||
else onClick()
|
||||
} else addWarningMessages()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative">
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
backgroundColor={showResolveIssues ? 'accentFailure' : 'accentAction'}
|
||||
cursor={
|
||||
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
|
||||
disableListButton
|
||||
? 'default'
|
||||
: 'pointer'
|
||||
}
|
||||
className={styles.button}
|
||||
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
|
||||
type="button"
|
||||
style={{
|
||||
color: showResolveIssues ? theme.accentTextLightPrimary : theme.white,
|
||||
opacity:
|
||||
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
|
||||
(disableListButton && !showResolveIssues)
|
||||
? 0.3
|
||||
: 1,
|
||||
}}
|
||||
>
|
||||
{listingStatus === ListingStatus.SIGNING ? (
|
||||
<Trans>Proceed in wallet</Trans>
|
||||
) : listingStatus === ListingStatus.PENDING ? (
|
||||
<Trans>Pending</Trans>
|
||||
) : listingStatus === ListingStatus.APPROVED ? (
|
||||
<Trans>Complete!</Trans>
|
||||
) : listingStatus === ListingStatus.PAUSED ? (
|
||||
<Trans>Paused</Trans>
|
||||
) : listingStatus === ListingStatus.FAILED ? (
|
||||
<Trans>Try again</Trans>
|
||||
) : listingStatus === ListingStatus.CONTINUE ? (
|
||||
<Trans>Continue</Trans>
|
||||
) : showResolveIssues ? (
|
||||
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
|
||||
) : (
|
||||
buttonText
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{showWarning && (
|
||||
<BelowFloorWarningModal
|
||||
listingsBelowFloor={listingsBelowFloor}
|
||||
closeModal={() => setShowWarning(false)}
|
||||
startListing={onClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { sprinkles } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const chevron = style([
|
||||
sprinkles({
|
||||
height: '28',
|
||||
width: '28',
|
||||
transition: '250',
|
||||
marginLeft: 'auto',
|
||||
marginRight: '0',
|
||||
}),
|
||||
])
|
||||
|
||||
export const chevronDown = style({
|
||||
transform: 'rotate(180deg)',
|
||||
cursor: 'pointer',
|
||||
})
|
||||
|
||||
export const sectionDivider = style([
|
||||
sprinkles({
|
||||
borderRadius: '20',
|
||||
marginTop: '8',
|
||||
width: 'full',
|
||||
borderWidth: '0.5px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'backgroundOutline',
|
||||
}),
|
||||
])
|
||||
|
||||
export const button = style([
|
||||
sprinkles({
|
||||
paddingX: { sm: '12', md: '16' },
|
||||
paddingY: { sm: '10', md: '16' },
|
||||
textAlign: 'center',
|
||||
fontWeight: 'semibold',
|
||||
fontSize: { sm: '16', md: '20' },
|
||||
lineHeight: { sm: '20', md: '24' },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'flex-end',
|
||||
borderRadius: '12',
|
||||
}),
|
||||
])
|
||||
|
||||
export const listingModalIcon = style([
|
||||
sprinkles({
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'backgroundSurface',
|
||||
}),
|
||||
{
|
||||
boxSizing: 'border-box',
|
||||
marginLeft: '-2px',
|
||||
marginRight: '4px',
|
||||
},
|
||||
])
|
||||
|
||||
export const warningTooltip = style([
|
||||
sprinkles({
|
||||
paddingTop: '8',
|
||||
paddingRight: '8',
|
||||
paddingBottom: '8',
|
||||
paddingLeft: '12',
|
||||
}),
|
||||
{
|
||||
boxShadow: '0px 4px 16px rgba(10, 10, 59, 0.2)',
|
||||
},
|
||||
])
|
||||
|
||||
export const listingSectionBorder = style([
|
||||
sprinkles({
|
||||
padding: '8',
|
||||
borderRadius: '8',
|
||||
borderColor: 'backgroundOutline',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
}),
|
||||
])
|
@ -1,322 +0,0 @@
|
||||
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { ChevronLeftIcon, XMarkIcon } from 'nft/components/icons'
|
||||
import { caption, headlineSmall, subhead, subheadSmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useBag, useIsMobile, useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { logListing, looksRareNonceFetcher } from 'nft/queries'
|
||||
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { pluralize } from 'nft/utils/roundAndPluralize'
|
||||
import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { ListingButton } from './ListingButton'
|
||||
import * as styles from './ListingModal.css'
|
||||
import { ListingSection } from './ListingSection'
|
||||
import { approveCollectionRow, getTotalEthValue, pauseRow, resetRow, signListingRow, verifyStatus } from './utils'
|
||||
|
||||
const ListingModal = () => {
|
||||
const { provider } = useWeb3React()
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const {
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
setListingStatusAndCallback,
|
||||
setCollectionStatusAndCallback,
|
||||
looksRareNonce,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
} = useNFTList(
|
||||
({
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
setListingStatusAndCallback,
|
||||
setCollectionStatusAndCallback,
|
||||
looksRareNonce,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}) => ({
|
||||
listingStatus,
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
setListingStatusAndCallback,
|
||||
setCollectionStatusAndCallback,
|
||||
looksRareNonce,
|
||||
setLooksRareNonce,
|
||||
getLooksRareNonce,
|
||||
collectionsRequiringApproval,
|
||||
listings,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const signer = provider?.getSigner()
|
||||
const [openIndex, setOpenIndex] = useState(0)
|
||||
const [allCollectionsApproved, setAllCollectionsApproved] = useState(false)
|
||||
const toggleCart = useBag((state) => state.toggleBag)
|
||||
const looksRareNonceRef = useRef(looksRareNonce)
|
||||
const isMobile = useIsMobile()
|
||||
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
|
||||
|
||||
useEffect(() => {
|
||||
useNFTList.subscribe((state) => (looksRareNonceRef.current = state.looksRareNonce))
|
||||
}, [])
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrice().then((price) => {
|
||||
setEthPriceInUSD(price || 0)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const startListingEventProperties = {
|
||||
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
|
||||
token_ids: sellAssets.map((asset) => asset.tokenId),
|
||||
marketplaces: Array.from(new Set(listings.map((asset) => asset.marketplace.name))),
|
||||
list_quantity: listings.length,
|
||||
usd_value: ethPriceInUSD * totalEthListingValue,
|
||||
...trace,
|
||||
}
|
||||
|
||||
// when all collections have been approved, auto start the signing process
|
||||
useEffect(() => {
|
||||
collectionsRequiringApproval?.length &&
|
||||
setAllCollectionsApproved(
|
||||
collectionsRequiringApproval.every((collection: CollectionRow) => collection.status === ListingStatus.APPROVED)
|
||||
)
|
||||
if (
|
||||
allCollectionsApproved &&
|
||||
(listingStatus === ListingStatus.PENDING ||
|
||||
listingStatus === ListingStatus.CONTINUE ||
|
||||
listingStatus === ListingStatus.SIGNING)
|
||||
) {
|
||||
resetAllRows()
|
||||
signListings()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collectionsRequiringApproval, allCollectionsApproved])
|
||||
|
||||
const allCollectionsApprovedOrPaused = useMemo(
|
||||
() =>
|
||||
collectionsRequiringApproval.every(
|
||||
(collection: CollectionRow) =>
|
||||
collection.status === ListingStatus.APPROVED || collection.status === ListingStatus.PAUSED
|
||||
),
|
||||
[collectionsRequiringApproval]
|
||||
)
|
||||
const allListingsApprovedOrPaused = useMemo(
|
||||
() =>
|
||||
listings.every(
|
||||
(listing: ListingRow) => listing.status === ListingStatus.APPROVED || listing.status === ListingStatus.PAUSED
|
||||
),
|
||||
[listings]
|
||||
)
|
||||
|
||||
// go back to a ready state after a successful retry
|
||||
useEffect(() => {
|
||||
if (listingStatus === ListingStatus.SIGNING && allCollectionsApprovedOrPaused && allListingsApprovedOrPaused) {
|
||||
resetAllRows()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [allCollectionsApprovedOrPaused, allListingsApprovedOrPaused])
|
||||
|
||||
// handles the modal wide listing state based on conglomeration of the wallet, collection, and listing states
|
||||
const startListingFlow = async () => {
|
||||
if (!signer) return
|
||||
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
const signerAddress = await signer.getAddress()
|
||||
const nonce = await looksRareNonceFetcher(signerAddress)
|
||||
setLooksRareNonce(nonce ?? 0)
|
||||
|
||||
if (!collectionsRequiringApproval?.some((collection) => collection.status === ListingStatus.PAUSED)) {
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
setOpenIndex(1)
|
||||
}
|
||||
// for all unique collection, marketplace combos -> approve collections
|
||||
for (const collectionRow of collectionsRequiringApproval) {
|
||||
verifyStatus(collectionRow.status) &&
|
||||
(isMobile
|
||||
? await approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
|
||||
: approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows))
|
||||
}
|
||||
}
|
||||
|
||||
const signListings = async () => {
|
||||
if (!signer || !provider) return
|
||||
setListingStatus(ListingStatus.SIGNING)
|
||||
setOpenIndex(2)
|
||||
// sign listings
|
||||
for (const listing of listings) {
|
||||
verifyStatus(listing.status) &&
|
||||
(await signListingRow(
|
||||
listing,
|
||||
signer,
|
||||
provider,
|
||||
getLooksRareNonce,
|
||||
setLooksRareNonce,
|
||||
setListingStatusAndCallback,
|
||||
pauseAllRows
|
||||
))
|
||||
}
|
||||
const allListingsSigned = listings.every((listing: ListingRow) => listing.status === ListingStatus.APPROVED)
|
||||
const paused = listings.some((listing: ListingRow) => listing.status === ListingStatus.PAUSED)
|
||||
if (allListingsSigned) {
|
||||
setOpenIndex(0)
|
||||
setListingStatus(ListingStatus.APPROVED)
|
||||
} else if (!paused) {
|
||||
setListingStatus(ListingStatus.FAILED)
|
||||
}
|
||||
sendAnalyticsEvent(NFTEventName.NFT_LISTING_COMPLETED, {
|
||||
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
|
||||
list_quantity: listings.length,
|
||||
usd_value: ethPriceInUSD * totalEthListingValue,
|
||||
...trace,
|
||||
})
|
||||
await logListing(listings, (await signer?.getAddress()) ?? '')
|
||||
}
|
||||
|
||||
const pauseAllRows = () => {
|
||||
for (const collection of collectionsRequiringApproval) {
|
||||
pauseRow(collection, collectionsRequiringApproval, setCollectionsRequiringApproval as Dispatch<AssetRow[]>)
|
||||
}
|
||||
for (const listing of listings) {
|
||||
pauseRow(listing, listings, setListings as Dispatch<AssetRow[]>)
|
||||
}
|
||||
}
|
||||
|
||||
const resetAllRows = () => {
|
||||
for (const collection of collectionsRequiringApproval) {
|
||||
resetRow(collection, collectionsRequiringApproval, setCollectionsRequiringApproval as Dispatch<AssetRow[]>)
|
||||
}
|
||||
for (const listing of listings) {
|
||||
resetRow(listing, listings, setListings as Dispatch<AssetRow[]>)
|
||||
}
|
||||
}
|
||||
|
||||
const clickStopListing = () => {
|
||||
pauseAllRows()
|
||||
}
|
||||
|
||||
const clickStartListingFlow = () => {
|
||||
resetAllRows()
|
||||
allCollectionsApproved ? signListings() : startListingFlow()
|
||||
}
|
||||
|
||||
const showSuccessScreen = useMemo(() => listingStatus === ListingStatus.APPROVED, [listingStatus])
|
||||
|
||||
return (
|
||||
<Trace modal={InterfaceModalName.NFT_LISTING}>
|
||||
<Column paddingTop="20" paddingBottom="20" paddingLeft="12" paddingRight="12">
|
||||
<Row className={headlineSmall} marginBottom="10">
|
||||
{isMobile && !showSuccessScreen && (
|
||||
<Box paddingTop="4" marginRight="4" onClick={toggleCart}>
|
||||
<ChevronLeftIcon height={28} width={28} />
|
||||
</Box>
|
||||
)}
|
||||
{showSuccessScreen ? 'Success!' : `Listing ${sellAssets.length} NFTs`}
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
color="textSecondary"
|
||||
backgroundColor="backgroundSurface"
|
||||
marginLeft="auto"
|
||||
marginRight="0"
|
||||
paddingRight="0"
|
||||
display={{ sm: 'flex', md: 'none' }}
|
||||
cursor="pointer"
|
||||
onClick={toggleCart}
|
||||
>
|
||||
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
|
||||
</Box>
|
||||
</Row>
|
||||
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
|
||||
{showSuccessScreen ? (
|
||||
<Trace
|
||||
name={NFTEventName.NFT_LISTING_COMPLETED}
|
||||
properties={{ list_quantity: listings.length, usd_value: ethPriceInUSD * totalEthListingValue, ...trace }}
|
||||
shouldLogImpression
|
||||
>
|
||||
<ListingSection
|
||||
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
|
||||
rows={listings}
|
||||
index={0}
|
||||
openIndex={openIndex}
|
||||
isSuccessScreen={true}
|
||||
/>
|
||||
</Trace>
|
||||
) : (
|
||||
<>
|
||||
<ListingSection
|
||||
sectionTitle={`Approve ${collectionsRequiringApproval.length} collection${pluralize(
|
||||
collectionsRequiringApproval.length
|
||||
)}`}
|
||||
title="COLLECTIONS"
|
||||
rows={collectionsRequiringApproval}
|
||||
index={1}
|
||||
openIndex={openIndex}
|
||||
/>
|
||||
<ListingSection
|
||||
sectionTitle={`Confirm ${listings.length} listing${pluralize(listings.length)}`}
|
||||
caption="Now you can sign to list each item"
|
||||
title="NFTS"
|
||||
rows={listings}
|
||||
index={2}
|
||||
openIndex={openIndex}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
<hr className={styles.sectionDivider} />
|
||||
<Row className={subhead} marginTop="12" marginBottom={showSuccessScreen ? '8' : '20'}>
|
||||
Return if sold
|
||||
<Row className={subheadSmall} marginLeft="auto" marginRight="0">
|
||||
{totalEthListingValue}
|
||||
ETH
|
||||
</Row>
|
||||
</Row>
|
||||
{showSuccessScreen ? (
|
||||
<Box as="span" className={caption} color="textSecondary">
|
||||
Status:{' '}
|
||||
<Box as="span" color="accentSuccess">
|
||||
Confirmed
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<ListingButton onClick={clickStartListingFlow} buttonText="Start listing" showWarningOverride={isMobile} />
|
||||
)}
|
||||
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
|
||||
<Box
|
||||
as="button"
|
||||
border="none"
|
||||
backgroundColor="backgroundSurface"
|
||||
cursor="pointer"
|
||||
color="orange"
|
||||
className={styles.button}
|
||||
onClick={clickStopListing}
|
||||
type="button"
|
||||
>
|
||||
Stop listing
|
||||
</Box>
|
||||
)}
|
||||
</Column>
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListingModal
|
@ -1,187 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { ApprovedCheckmarkIcon, ChevronUpIcon, FailedListingIcon, LoadingIcon } from 'nft/components/icons'
|
||||
import { badge, bodySmall, buttonTextSmall, subhead } from 'nft/css/common.css'
|
||||
import { useSellAsset } from 'nft/hooks'
|
||||
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
|
||||
import { formatEthPrice, numberToWei } from 'nft/utils/currency'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import * as styles from './ListingModal.css'
|
||||
|
||||
export const ListingSection = ({
|
||||
sectionTitle,
|
||||
caption = undefined,
|
||||
title = undefined,
|
||||
rows,
|
||||
index,
|
||||
openIndex,
|
||||
isSuccessScreen = false,
|
||||
}: {
|
||||
sectionTitle: string
|
||||
caption?: string
|
||||
title?: string
|
||||
rows: AssetRow[]
|
||||
index: number
|
||||
openIndex: number
|
||||
isSuccessScreen?: boolean
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const notAllApproved = rows.some((row: AssetRow) => row.status !== ListingStatus.APPROVED)
|
||||
const sellAssets = useSellAsset((state) => state.sellAssets)
|
||||
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
|
||||
|
||||
const removeRow = (row: any) => {
|
||||
// collections
|
||||
if (index === 1) {
|
||||
for (const asset of sellAssets)
|
||||
if (asset.asset_contract.address === row.collectionAddress) removeAssetMarketplace(asset, row.marketplace)
|
||||
}
|
||||
// listings
|
||||
else removeAssetMarketplace(row.asset, row.marketplace)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(index === openIndex)
|
||||
}, [index, openIndex])
|
||||
|
||||
function getListingRowPrice(row: AssetRow): number | undefined {
|
||||
const listingRow = row as ListingRow
|
||||
const newListings = listingRow.asset.newListings
|
||||
return newListings?.find((listing) => listing.marketplace.name === listingRow.marketplace.name)?.price ?? 0
|
||||
}
|
||||
|
||||
const allApproved = !notAllApproved && rows.length > 0 && !isSuccessScreen
|
||||
|
||||
return (
|
||||
<Row
|
||||
flexWrap="wrap"
|
||||
className={subhead}
|
||||
marginTop="10"
|
||||
marginBottom="10"
|
||||
onClick={() => rows.length > 0 && setIsOpen(!isOpen)}
|
||||
color={allApproved ? 'accentSuccess' : 'textPrimary'}
|
||||
>
|
||||
{allApproved && <ApprovedCheckmarkIcon style={{ marginRight: '8px' }} />}
|
||||
{sectionTitle}
|
||||
{!isSuccessScreen && <ChevronUpIcon className={clsx(`${isOpen ? '' : styles.chevronDown} ${styles.chevron}`)} />}
|
||||
{(isOpen || isSuccessScreen) && (
|
||||
<Column
|
||||
gap="12"
|
||||
width="full"
|
||||
paddingTop={isSuccessScreen ? '28' : 'auto'}
|
||||
className={clsx(!isSuccessScreen && styles.listingSectionBorder)}
|
||||
>
|
||||
{caption && (
|
||||
<Box color="textPrimary" fontWeight="normal" className={caption}>
|
||||
{caption}
|
||||
</Box>
|
||||
)}
|
||||
{title && (
|
||||
<Box color="textSecondary" className={badge}>
|
||||
{title}
|
||||
</Box>
|
||||
)}
|
||||
<Column gap="8">
|
||||
{rows.map((row: AssetRow, index) => {
|
||||
return (
|
||||
<Column key={index} gap="8">
|
||||
<Row>
|
||||
{row.images?.map((image, index) => {
|
||||
return (
|
||||
<Box
|
||||
as="img"
|
||||
height="20"
|
||||
width="20"
|
||||
borderRadius={index === 0 && (row as CollectionRow).collectionAddress ? 'round' : '4'}
|
||||
style={{ zIndex: 2 - index }}
|
||||
className={styles.listingModalIcon}
|
||||
src={image}
|
||||
alt={row.name}
|
||||
key={index}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Box
|
||||
marginLeft="8"
|
||||
marginRight="auto"
|
||||
fontWeight="normal"
|
||||
color="textPrimary"
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
maxWidth={{
|
||||
sm: 'max',
|
||||
md:
|
||||
row.status === ListingStatus.REJECTED || row.status === ListingStatus.FAILED ? '120' : 'full',
|
||||
}}
|
||||
className={bodySmall}
|
||||
>
|
||||
{row.name}
|
||||
</Box>
|
||||
{isSuccessScreen ? (
|
||||
getListingRowPrice(row) &&
|
||||
`${formatEthPrice(numberToWei(getListingRowPrice(row) ?? 0).toString())} ETH`
|
||||
) : row.status === ListingStatus.APPROVED ? (
|
||||
<ApprovedCheckmarkIcon height="20" width="20" />
|
||||
) : row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED ? (
|
||||
<Row gap="4">
|
||||
<Box fontWeight="normal" fontSize="14" color="textSecondary">
|
||||
{row.status}
|
||||
</Box>
|
||||
<FailedListingIcon />
|
||||
</Row>
|
||||
) : (
|
||||
row.status === ListingStatus.SIGNING && <LoadingIcon height="20" width="20" stroke="#4673FA" />
|
||||
)}
|
||||
</Row>
|
||||
{(row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED) && (
|
||||
<Row gap="8" justifyContent="center">
|
||||
<Box
|
||||
width="120"
|
||||
as="button"
|
||||
className={buttonTextSmall}
|
||||
borderRadius="12"
|
||||
border="none"
|
||||
color="red400"
|
||||
height="32"
|
||||
cursor="pointer"
|
||||
style={{ backgroundColor: '#FA2B391A' }}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation()
|
||||
removeRow(row)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Box>
|
||||
<Box
|
||||
width="120"
|
||||
as="button"
|
||||
className={buttonTextSmall}
|
||||
borderRadius="12"
|
||||
border="none"
|
||||
color="accentAction"
|
||||
height="32"
|
||||
cursor="pointer"
|
||||
style={{ backgroundColor: '#4C82FB29' }}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation()
|
||||
if (row.callback) {
|
||||
await row.callback()
|
||||
}
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</Box>
|
||||
</Row>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
})}
|
||||
</Column>
|
||||
</Column>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
@ -2,30 +2,8 @@ import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
|
||||
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
|
||||
import { LOOKSRARE_MARKETPLACE_CONTRACT, X2Y2_TRANSFER_CONTRACT } from 'nft/queries'
|
||||
import { OPENSEA_CROSS_CHAIN_CONDUIT } from 'nft/queries/openSea'
|
||||
import { AssetRow, CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { approveCollection, LOOKS_RARE_CREATOR_BASIS_POINTS, signListing } from 'nft/utils/listNfts'
|
||||
import { Dispatch } from 'react'
|
||||
|
||||
const updateStatus = ({
|
||||
listing,
|
||||
newStatus,
|
||||
rows,
|
||||
setRows,
|
||||
callback,
|
||||
}: {
|
||||
listing: AssetRow
|
||||
newStatus: ListingStatus
|
||||
rows: AssetRow[]
|
||||
setRows: Dispatch<AssetRow[]>
|
||||
callback?: () => Promise<void>
|
||||
}) => {
|
||||
const rowsCopy = [...rows]
|
||||
const index = rows.findIndex((n) => n === listing)
|
||||
listing.status = newStatus
|
||||
if (callback) listing.callback = callback
|
||||
rowsCopy[index] = listing
|
||||
setRows(rowsCopy)
|
||||
}
|
||||
|
||||
export async function approveCollectionRow(
|
||||
collectionRow: CollectionRow,
|
||||
@ -211,27 +189,3 @@ export const getListingState = (
|
||||
export const verifyStatus = (status: ListingStatus) => {
|
||||
return status !== ListingStatus.PAUSED && status !== ListingStatus.APPROVED
|
||||
}
|
||||
|
||||
export const pauseRow = (row: AssetRow, rows: AssetRow[], setRows: Dispatch<AssetRow[]>) => {
|
||||
if (row.status === ListingStatus.PENDING || row.status === ListingStatus.DEFINED)
|
||||
updateStatus({
|
||||
listing: row,
|
||||
newStatus: ListingStatus.PAUSED,
|
||||
rows,
|
||||
setRows,
|
||||
})
|
||||
}
|
||||
|
||||
export const resetRow = (row: AssetRow, rows: AssetRow[], setRows: Dispatch<AssetRow[]>) => {
|
||||
if (
|
||||
row.status === ListingStatus.PAUSED ||
|
||||
row.status === ListingStatus.FAILED ||
|
||||
row.status === ListingStatus.REJECTED
|
||||
)
|
||||
updateStatus({
|
||||
listing: row,
|
||||
newStatus: ListingStatus.DEFINED,
|
||||
rows,
|
||||
setRows,
|
||||
})
|
||||
}
|
||||
|
@ -304,30 +304,6 @@ export const ClockIcon = () => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const LoadingIcon = (props: SVGProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
width="100px"
|
||||
height="100px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="50" cy="50" fill="none" strokeWidth="10" r="35" strokeDasharray="164.93361431346415 56.97787143782138">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
repeatCount="indefinite"
|
||||
dur="1s"
|
||||
values="0 50 50;360 50 50"
|
||||
keyTimes="0;1"
|
||||
{...props}
|
||||
></animateTransform>
|
||||
</circle>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ApprovedCheckmarkIcon = (props: SVGProps) => (
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
@ -337,15 +313,6 @@ export const ApprovedCheckmarkIcon = (props: SVGProps) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const FailedListingIcon = (props: SVGProps) => (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M9.9933 16.2444C13.5529 16.2444 16.4909 13.3064 16.4909 9.75307C16.4909 6.19978 13.5466 3.26172 9.98703 3.26172C6.43373 3.26172 3.50195 6.19978 3.50195 9.75307C3.50195 13.3064 6.44001 16.2444 9.9933 16.2444ZM8.12877 12.3207C7.78976 12.3207 7.62653 12.1324 7.62653 11.8624V7.63742C7.62653 7.36747 7.78976 7.17913 8.12877 7.17913H8.80678C9.14579 7.17913 9.30901 7.36747 9.30901 7.63742V11.8624C9.30901 12.1324 9.14579 12.3207 8.80678 12.3207H8.12877ZM11.1798 12.3207C10.8471 12.3207 10.6776 12.1324 10.6776 11.8624V7.63742C10.6776 7.36747 10.8471 7.17913 11.1798 7.17913H11.8641C12.1906 7.17913 12.3538 7.36747 12.3538 7.63742V11.8624C12.3538 12.1324 12.1906 12.3207 11.8641 12.3207H11.1798Z"
|
||||
fill={themeVars.colors.textSecondary}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const FilterIcon = (props: SVGProps) => (
|
||||
<svg width="20" height="20" viewBox="1 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
|
||||
import { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
|
||||
import { ListingButton } from 'nft/components/profile/list/ListingButton'
|
||||
import { useIsMobile, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE } from 'nft/pages/profile/shared'
|
||||
import { looksRareNonceFetcher } from 'nft/queries'
|
||||
@ -212,7 +212,6 @@ export const ListPage = () => {
|
||||
)
|
||||
|
||||
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
|
||||
const anyListingsMissingPrice = useMemo(() => !!listings.find((listing) => !listing.price), [listings])
|
||||
const [showListModal, toggleShowListModal] = useReducer((s) => !s, false)
|
||||
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
|
||||
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
|
||||
@ -269,7 +268,7 @@ export const ListPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleV2Click = () => {
|
||||
const showModalAndStartListing = () => {
|
||||
toggleShowListModal()
|
||||
startListingFlow()
|
||||
}
|
||||
@ -322,11 +321,7 @@ export const ListPage = () => {
|
||||
<UsdValue>{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}</UsdValue>
|
||||
)}
|
||||
</ProceedsWrapper>
|
||||
<ListingButton
|
||||
onClick={handleV2Click}
|
||||
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
|
||||
showWarningOverride={true}
|
||||
/>
|
||||
<ListingButton onClick={showModalAndStartListing} />
|
||||
</ProceedsAndButtonWrapper>
|
||||
</FloatingConfirmationBar>
|
||||
<Overlay />
|
||||
|
144
src/nft/components/profile/list/ListingButton.tsx
Normal file
144
src/nft/components/profile/list/ListingButton.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { Plural, t, Trans } from '@lingui/macro'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import ms from 'ms.macro'
|
||||
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
|
||||
import { useIsMobile, useNFTList, useSellAsset } from 'nft/hooks'
|
||||
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { getListings } from '../../bag/profile/utils'
|
||||
|
||||
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
|
||||
|
||||
const StyledListingButton = styled(BaseButton)<{ showResolveIssues: boolean; missingPrices: boolean }>`
|
||||
background: ${({ showResolveIssues, theme }) => (showResolveIssues ? theme.accentFailure : theme.accentAction)};
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
width: min-content;
|
||||
border: none;
|
||||
cursor: ${({ missingPrices }) => (missingPrices ? 'auto' : 'pointer')};
|
||||
opacity: ${({ showResolveIssues, missingPrices }) => !showResolveIssues && missingPrices && '0.3'};
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export const ListingButton = ({ onClick }: { onClick: () => void }) => {
|
||||
const { sellAssets, showResolveIssues, toggleShowResolveIssues, issues, setIssues } = useSellAsset(
|
||||
({ sellAssets, showResolveIssues, toggleShowResolveIssues, issues, setIssues }) => ({
|
||||
sellAssets,
|
||||
showResolveIssues,
|
||||
toggleShowResolveIssues,
|
||||
issues,
|
||||
setIssues,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const { setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
|
||||
({ setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
|
||||
setListingStatus,
|
||||
setListings,
|
||||
setCollectionsRequiringApproval,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const [showWarning, setShowWarning] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
// instantiate listings and collections to approve when user's modify input data
|
||||
useEffect(() => {
|
||||
const [newCollectionsToApprove, newListings] = getListings(sellAssets)
|
||||
setListings(newListings)
|
||||
setCollectionsRequiringApproval(newCollectionsToApprove)
|
||||
setListingStatus(ListingStatus.DEFINED)
|
||||
}, [sellAssets, setCollectionsRequiringApproval, setListingStatus, setListings])
|
||||
|
||||
// Find issues with item listing data
|
||||
const [listingsMissingPrice, listingsBelowFloor] = useMemo(() => {
|
||||
const missingExpiration = sellAssets.some((asset) => {
|
||||
return (
|
||||
asset.expirationTime != null &&
|
||||
(isNaN(asset.expirationTime) || asset.expirationTime * 1000 - Date.now() < ms`60 seconds`)
|
||||
)
|
||||
})
|
||||
const overMaxExpiration = sellAssets.some((asset) => {
|
||||
return asset.expirationTime != null && asset.expirationTime * 1000 - Date.now() > ms`180 days`
|
||||
})
|
||||
|
||||
const listingsMissingPrice: [WalletAsset, Listing][] = []
|
||||
const listingsBelowFloor: [WalletAsset, Listing][] = []
|
||||
const listingsAboveSellOrderFloor: [WalletAsset, Listing][] = []
|
||||
const invalidPrices: [WalletAsset, Listing][] = []
|
||||
for (const asset of sellAssets) {
|
||||
if (asset.newListings) {
|
||||
for (const listing of asset.newListings) {
|
||||
if (!listing.price) listingsMissingPrice.push([asset, listing])
|
||||
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
|
||||
else if (
|
||||
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
|
||||
!listing.overrideFloorPrice
|
||||
)
|
||||
listingsBelowFloor.push([asset, listing])
|
||||
else if (asset.floor_sell_order_price && listing.price >= asset.floor_sell_order_price)
|
||||
listingsAboveSellOrderFloor.push([asset, listing])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set number of issues
|
||||
const foundIssues =
|
||||
Number(missingExpiration) +
|
||||
Number(overMaxExpiration) +
|
||||
listingsMissingPrice.length +
|
||||
listingsAboveSellOrderFloor.length
|
||||
setIssues(foundIssues)
|
||||
!foundIssues && showResolveIssues && toggleShowResolveIssues()
|
||||
// Only show Resolve Issue text if there was a user submitted error (ie not when page loads with no prices set)
|
||||
if ((missingExpiration || overMaxExpiration || listingsAboveSellOrderFloor.length) && !showResolveIssues)
|
||||
toggleShowResolveIssues()
|
||||
|
||||
return [listingsMissingPrice, listingsBelowFloor]
|
||||
}, [sellAssets, setIssues, showResolveIssues, toggleShowResolveIssues])
|
||||
|
||||
const warningWrappedClick = () => {
|
||||
if (issues) !showResolveIssues && toggleShowResolveIssues()
|
||||
else if (listingsBelowFloor.length) setShowWarning(true)
|
||||
else onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledListingButton
|
||||
onClick={warningWrappedClick}
|
||||
missingPrices={!!listingsMissingPrice.length}
|
||||
showResolveIssues={showResolveIssues}
|
||||
>
|
||||
{showResolveIssues ? (
|
||||
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
|
||||
) : listingsMissingPrice.length && !isMobile ? (
|
||||
<Trans>Set prices to continue</Trans>
|
||||
) : (
|
||||
<Trans>Start listing</Trans>
|
||||
)}
|
||||
</StyledListingButton>
|
||||
|
||||
{showWarning && (
|
||||
<BelowFloorWarningModal
|
||||
listingsBelowFloor={listingsBelowFloor}
|
||||
closeModal={() => setShowWarning(false)}
|
||||
startListing={onClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -18,27 +18,10 @@ export const subheadSmall = sprinkles({ fontWeight: 'medium', fontSize: '14', li
|
||||
export const body = sprinkles({ fontWeight: 'normal', fontSize: '16', lineHeight: '24' })
|
||||
export const bodySmall = sprinkles({ fontWeight: 'normal', fontSize: '14', lineHeight: '20' })
|
||||
export const caption = sprinkles({ fontWeight: 'normal', fontSize: '12', lineHeight: '16' })
|
||||
export const badge = sprinkles({ fontWeight: 'semibold', fontSize: '10', lineHeight: '12' })
|
||||
|
||||
export const buttonTextMedium = sprinkles({ fontWeight: 'semibold', fontSize: '16', lineHeight: '20' })
|
||||
export const buttonTextSmall = sprinkles({ fontWeight: 'semibold', fontSize: '14', lineHeight: '16' })
|
||||
|
||||
export const commonButtonStyles = style([
|
||||
sprinkles({
|
||||
borderRadius: '12',
|
||||
transition: '250',
|
||||
}),
|
||||
{
|
||||
border: 'none',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
':disabled': {
|
||||
cursor: 'auto',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const magicalGradient = style({
|
||||
selectors: {
|
||||
'&::before': {
|
||||
|
Loading…
Reference in New Issue
Block a user