feat: [ListV2] Connect modal styling to functionality (#5905)

* dynamic styles based on listing status

* connect collection approval to v2 modal

* import cleanup

* add comments

* connect sign listing logic

* correct pending styles

* correct pending status for collection approval

* correct pending status for os listings

* use check from react-feather

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2023-02-01 13:41:50 -08:00 committed by GitHub
parent 35a03e2681
commit 48833f27e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 219 additions and 68 deletions

@ -1,4 +1,3 @@
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
@ -109,7 +108,6 @@ const ListingModal = () => {
if (!signer) return
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
setListingStatus(ListingStatus.SIGNING)
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const signerAddress = await signer.getAddress()
const nonce = await looksRareNonceFetcher(signerAddress)
setLooksRareNonce(nonce ?? 0)
@ -118,7 +116,6 @@ const ListingModal = () => {
setListingStatus(ListingStatus.SIGNING)
setOpenIndex(1)
}
const looksRareAddress = addresses.TRANSFER_MANAGER_ERC721
// for all unique collection, marketplace combos -> approve collections
for (const collectionRow of collectionsRequiringApproval) {
verifyStatus(collectionRow.status) &&
@ -128,7 +125,6 @@ const ListingModal = () => {
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer,
looksRareAddress,
pauseAllRows
)
: approveCollectionRow(
@ -136,7 +132,6 @@ const ListingModal = () => {
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer,
looksRareAddress,
pauseAllRows
))
}

@ -1,4 +1,5 @@
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'
@ -31,8 +32,7 @@ export async function approveCollectionRow(
collectionsRequiringApproval: CollectionRow[],
setCollectionsRequiringApproval: Dispatch<CollectionRow[]>,
signer: JsonRpcSigner,
looksRareAddress: string,
pauseAllRows: () => void
pauseAllRows?: () => void
) {
updateStatus({
listing: collectionRow,
@ -45,11 +45,11 @@ export async function approveCollectionRow(
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer,
looksRareAddress,
pauseAllRows
),
})
const { marketplace, collectionAddress } = collectionRow
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const spender =
marketplace.name === 'OpenSea'
? OPENSEA_CROSS_CHAIN_CONDUIT
@ -57,7 +57,7 @@ export async function approveCollectionRow(
? LOOKSRARE_MARKETPLACE_CONTRACT
: marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT
: looksRareAddress
: addresses.TRANSFER_MANAGER_ERC721
!!collectionAddress &&
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
updateStatus({
@ -67,7 +67,11 @@ export async function approveCollectionRow(
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
})
))
if (collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) pauseAllRows()
if (
(collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) &&
pauseAllRows
)
pauseAllRows()
}
export async function signListingRow(
@ -78,7 +82,7 @@ export async function signListingRow(
provider: Web3Provider,
getLooksRareNonce: () => number,
setLooksRareNonce: (nonce: number) => void,
pauseAllRows: () => void
pauseAllRows?: () => void
) {
const looksRareNonce = getLooksRareNonce()
updateStatus({
@ -108,7 +112,7 @@ export async function signListingRow(
setRows: setListings as Dispatch<AssetRow[]>,
})
)
if (listing.status === ListingStatus.REJECTED) pauseAllRows()
if (listing.status === ListingStatus.REJECTED && pauseAllRows) pauseAllRows()
else {
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
const newStatus = res ? ListingStatus.APPROVED : ListingStatus.FAILED

@ -760,8 +760,8 @@ export const CancelListingIcon = (props: SVGProps) => (
export const ListingModalWindowActive = (props: SVGProps) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="8" cy="8" r="8" fill={themeVars.colors.accentAction} fillOpacity="0.24" />
<circle cx="8" cy="8" r="5" fill={themeVars.colors.accentAction} />
<circle cx="8" cy="8" r="8" fill={props.fill ? props.fill : themeVars.colors.accentAction} fillOpacity="0.24" />
<circle cx="8" cy="8" r="5" fill={props.fill ? props.fill : themeVars.colors.accentAction} />
</svg>
)

@ -1,15 +1,19 @@
import { t, 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 { SMALL_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
import { getListingState, getTotalEthValue } from 'nft/components/bag/profile/utils'
import { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
import { BackArrowIcon } from 'nft/components/icons'
import { headlineLarge, headlineSmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, 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'
import { ListingStatus, ProfilePageStateType } from 'nft/types'
import { fetchPrice, formatEth, formatUsdPrice } from 'nft/utils'
import { ListingMarkets } from 'nft/utils/listNfts'
@ -17,6 +21,7 @@ import { useEffect, useMemo, useReducer, useState } from 'react'
import styled, { css } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import shallow from 'zustand/shallow'
import { ListModal } from './Modal/ListModal'
import { NFTListingsGrid } from './NFTListingsGrid'
@ -150,21 +155,50 @@ const ListingButtonWrapper = styled.div`
export const ListPage = () => {
const { setProfilePageState: setSellPageState } = useProfilePageState()
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
const { provider } = useWeb3React()
const toggleBag = useBag((s) => s.toggleBag)
const listings = useNFTList((state) => state.listings)
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
const listingStatus = useNFTList((state) => state.listingStatus)
const setListingStatus = useNFTList((state) => state.setListingStatus)
const sellAssets = useSellAsset((state) => state.sellAssets)
const isMobile = useIsMobile()
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const { setGlobalMarketplaces, sellAssets } = useSellAsset(
({ setGlobalMarketplaces, sellAssets }) => ({
setGlobalMarketplaces,
sellAssets,
}),
shallow
)
const {
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionsRequiringApproval,
} = useNFTList(
({
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionsRequiringApproval,
}) => ({
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionsRequiringApproval,
}),
shallow
)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const anyListingsMissingPrice = useMemo(() => !!listings.find((listing) => !listing.price), [listings])
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
const [showListModal, toggleShowListModal] = useReducer((s) => !s, false)
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
const signer = provider?.getSigner()
useEffect(() => {
fetchPrice().then((price) => {
@ -172,6 +206,7 @@ export const ListPage = () => {
})
}, [])
// TODO with removal of list v1 see if this logic can be removed
useEffect(() => {
const state = getListingState(collectionsRequiringApproval, listings)
@ -188,8 +223,43 @@ export const ListPage = () => {
useEffect(() => {
setGlobalMarketplaces(selectedMarkets)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMarkets])
}, [selectedMarkets, setGlobalMarketplaces])
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,
}
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)
// for all unique collection, marketplace combos -> approve collections
for (const collectionRow of collectionsRequiringApproval) {
verifyStatus(collectionRow.status) &&
(isMobile
? await approveCollectionRow(
collectionRow,
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer
)
: approveCollectionRow(collectionRow, collectionsRequiringApproval, setCollectionsRequiringApproval, signer))
}
}
const handleV2Click = () => {
toggleShowListModal()
startListingFlow()
}
const BannerText = isMobile ? (
<ThemedText.SubHeader lineHeight="24px">
@ -241,7 +311,7 @@ export const ListPage = () => {
</ProceedsWrapper>
<ListingButtonWrapper>
<ListingButton
onClick={isNftListV2 ? toggleShowListModal : toggleBag}
onClick={handleV2Click}
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
/>
</ListingButtonWrapper>

@ -1,11 +1,15 @@
import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { InterfaceModalName } from '@uniswap/analytics-events'
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import Row from 'components/Row'
import { getTotalEthValue, signListingRow } from 'nft/components/bag/profile/utils'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import { useNFTList } from 'nft/hooks'
import { useReducer } from 'react'
import { useNFTList, useSellAsset } from 'nft/hooks'
import { ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { X } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
@ -41,34 +45,85 @@ const TitleRow = styled(Row)`
`
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
const { provider } = useWeb3React()
const signer = provider?.getSigner()
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const sellAssets = useSellAsset((state) => state.sellAssets)
const listings = useNFTList((state) => state.listings)
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
const listingStatus = useNFTList((state) => state.listingStatus)
const setListings = useNFTList((state) => state.setListings)
const setLooksRareNonce = useNFTList((state) => state.setLooksRareNonce)
const getLooksRareNonce = useNFTList((state) => state.getLooksRareNonce)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const [openSection, toggleOpenSection] = useReducer(
(s) => (s === Section.APPROVE ? Section.SIGN : Section.APPROVE),
Section.APPROVE
)
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
useEffect(() => {
fetchPrice().then((price) => {
setEthPriceInUSD(price || 0)
})
}, [])
const allCollectionsApproved = useMemo(
() => collectionsRequiringApproval.every((collection) => collection.status === ListingStatus.APPROVED),
[collectionsRequiringApproval]
)
const signListings = async () => {
if (!signer || !provider) return
// sign listings
for (const listing of listings) {
await signListingRow(listing, listings, setListings, signer, provider, getLooksRareNonce, setLooksRareNonce)
}
sendAnalyticsEvent(NFTEventName.NFT_LISTING_COMPLETED, {
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
})
}
useEffect(() => {
if (allCollectionsApproved) {
signListings()
openSection === Section.APPROVE && toggleOpenSection()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allCollectionsApproved])
return (
<Portal>
<Trace modal={InterfaceModalName.NFT_LISTING}>
<ListModalWrapper>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>List NFTs</Trans>
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
</TitleRow>
<ListModalSection
sectionType={Section.APPROVE}
active={openSection === Section.APPROVE}
content={collectionsRequiringApproval}
toggleSection={toggleOpenSection}
/>
<ListModalSection
sectionType={Section.SIGN}
active={openSection === Section.SIGN}
content={listings}
toggleSection={toggleOpenSection}
/>
{listingStatus === ListingStatus.APPROVED ? (
<>TODO Success Screen</>
) : (
<>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>List NFTs</Trans>
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
</TitleRow>
<ListModalSection
sectionType={Section.APPROVE}
active={openSection === Section.APPROVE}
content={collectionsRequiringApproval}
toggleSection={toggleOpenSection}
/>
<ListModalSection
sectionType={Section.SIGN}
active={openSection === Section.SIGN}
content={listings}
toggleSection={toggleOpenSection}
/>
</>
)}
</ListModalWrapper>
</Trace>
<Overlay onClick={overlayClick} />

@ -10,8 +10,9 @@ import {
LoadingIcon,
VerifiedIcon,
} from 'nft/components/icons'
import { AssetRow, CollectionRow } from 'nft/types'
import { Info } from 'react-feather'
import { AssetRow, CollectionRow, ListingStatus } from 'nft/types'
import { useMemo } from 'react'
import { Check, Info } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { colors } from 'theme/colors'
@ -21,9 +22,10 @@ const SectionHeader = styled(Row)`
justify-content: space-between;
`
const SectionTitle = styled(ThemedText.SubHeader)<{ active: boolean }>`
const SectionTitle = styled(ThemedText.SubHeader)<{ active: boolean; approved: boolean }>`
line-height: 24px;
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
color: ${({ theme, active, approved }) =>
approved ? theme.accentSuccess : active ? theme.textPrimary : theme.textSecondary};
`
const SectionArrow = styled(ChevronUpIcon)<{ active: boolean }>`
@ -56,11 +58,11 @@ const ContentRowContainer = styled(Column)`
gap: 8px;
`
const ContentRow = styled(Row)`
const ContentRow = styled(Row)<{ active: boolean }>`
padding: 16px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 12px;
opacity: 0.6;
opacity: ${({ active }) => (active ? '1' : '0.6')};
`
const CollectionIcon = styled.img`
@ -94,16 +96,20 @@ const ContentName = styled(ThemedText.SubHeaderSmall)`
max-width: 50%;
`
const ProceedText = styled.span`
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const StyledVerifiedIcon = styled(VerifiedIcon)`
height: 16px;
width: 16px;
margin-left: 4px;
`
const StyledLoadingIconBackground = styled(LoadingIcon)`
height: 14px;
width: 14px;
stroke: ${({ theme }) => theme.textTertiary};
const IconWrapper = styled.div`
margin-left: auto;
margin-right: 0px;
`
@ -122,13 +128,18 @@ interface ListModalSectionProps {
export const ListModalSection = ({ sectionType, active, content, toggleSection }: ListModalSectionProps) => {
const theme = useTheme()
const allContentApproved = useMemo(() => !content.some((row) => row.status !== ListingStatus.APPROVED), [content])
const isCollectionApprovalSection = sectionType === Section.APPROVE
return (
<Column>
<SectionHeader>
<Row>
{active ? <ListingModalWindowActive /> : <ListingModalWindowClosed />}
<SectionTitle active={active} marginLeft="12px">
{active || allContentApproved ? (
<ListingModalWindowActive fill={allContentApproved ? theme.accentSuccess : theme.accentAction} />
) : (
<ListingModalWindowClosed />
)}
<SectionTitle active={active} marginLeft="12px" approved={allContentApproved}>
{isCollectionApprovalSection ? (
<>
<Trans>Approve</Trans>&nbsp;{content.length}&nbsp;
@ -165,7 +176,10 @@ export const ListModalSection = ({ sectionType, active, content, toggleSection }
<ContentRowContainer>
{content.map((row) => {
return (
<ContentRow key={row.name}>
<ContentRow
key={row.name}
active={row.status === ListingStatus.SIGNING || row.status === ListingStatus.APPROVED}
>
{isCollectionApprovalSection ? (
<CollectionIcon src={row.images[0]} />
) : (
@ -174,7 +188,23 @@ export const ListModalSection = ({ sectionType, active, content, toggleSection }
<MarketplaceIcon src={row.images[1]} />
<ContentName>{row.name}</ContentName>
{isCollectionApprovalSection && (row as CollectionRow).isVerified && <StyledVerifiedIcon />}
<StyledLoadingIconBackground />
<IconWrapper>
{row.status === ListingStatus.DEFINED || row.status === ListingStatus.PENDING ? (
<LoadingIcon
height="14px"
width="14px"
stroke={row.status === ListingStatus.PENDING ? theme.accentAction : theme.textTertiary}
/>
) : row.status === ListingStatus.SIGNING ? (
<ProceedText>
<Trans>Proceed in wallet</Trans>
</ProceedText>
) : (
row.status === ListingStatus.APPROVED && (
<Check height="20" width="20" stroke={theme.accentSuccess} />
)
)}
</IconWrapper>
</ContentRow>
)
})}

@ -12,6 +12,7 @@ interface SellAssetState {
setAssetListPrice: (asset: WalletAsset, price?: number, marketplace?: ListingMarket) => void
setGlobalMarketplaces: (marketplaces: ListingMarket[]) => void
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
// TODO: After merging v2, see if this marketplace logic can be removed
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
removeAllMarketplaceWarnings: () => void

@ -118,8 +118,3 @@ export enum ProfilePageStateType {
VIEWING,
LISTING,
}
export enum ListingResponse {
TRY_AGAIN,
SUCCESS,
}

@ -92,7 +92,7 @@ export async function approveCollection(
// setApprovalForAll() method
const ERC721Contract = new Contract(collectionAddress, ERC721, signer)
const signerAddress = await signer.getAddress()
setStatus(ListingStatus.PENDING)
try {
const approved = await ERC721Contract.isApprovedForAll(signerAddress, operator)
if (approved) {
@ -160,6 +160,7 @@ export async function signListing(
)
const order = await executeAllActions()
setStatus(ListingStatus.PENDING)
const res = await PostOpenSeaSellOrder(order)
if (res) setStatus(ListingStatus.APPROVED)
return res