diff --git a/src/nft/components/bag/profile/ListingModal.tsx b/src/nft/components/bag/profile/ListingModal.tsx index 101e847b66..3ea700a6b5 100644 --- a/src/nft/components/bag/profile/ListingModal.tsx +++ b/src/nft/components/bag/profile/ListingModal.tsx @@ -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 )) } diff --git a/src/nft/components/bag/profile/utils.ts b/src/nft/components/bag/profile/utils.ts index 368c2d109a..6c3006a774 100644 --- a/src/nft/components/bag/profile/utils.ts +++ b/src/nft/components/bag/profile/utils.ts @@ -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, 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, }) )) - 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, }) ) - 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 diff --git a/src/nft/components/icons.tsx b/src/nft/components/icons.tsx index a979371051..389004b633 100644 --- a/src/nft/components/icons.tsx +++ b/src/nft/components/icons.tsx @@ -760,8 +760,8 @@ export const CancelListingIcon = (props: SVGProps) => ( export const ListingModalWindowActive = (props: SVGProps) => ( - - + + ) diff --git a/src/nft/components/profile/list/ListPage.tsx b/src/nft/components/profile/list/ListPage.tsx index a08e3cbdc3..bba60e6459 100644 --- a/src/nft/components/profile/list/ListPage.tsx +++ b/src/nft/components/profile/list/ListPage.tsx @@ -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 ? ( @@ -241,7 +311,7 @@ export const ListPage = () => { diff --git a/src/nft/components/profile/list/Modal/ListModal.tsx b/src/nft/components/profile/list/Modal/ListModal.tsx index 5432d5332c..7f3905d4d2 100644 --- a/src/nft/components/profile/list/Modal/ListModal.tsx +++ b/src/nft/components/profile/list/Modal/ListModal.tsx @@ -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 ( - - - List NFTs - - - - - + {listingStatus === ListingStatus.APPROVED ? ( + <>TODO Success Screen + ) : ( + <> + + + List NFTs + + + + + + + )} diff --git a/src/nft/components/profile/list/Modal/ListModalSection.tsx b/src/nft/components/profile/list/Modal/ListModalSection.tsx index 1128b3195d..8875d2047f 100644 --- a/src/nft/components/profile/list/Modal/ListModalSection.tsx +++ b/src/nft/components/profile/list/Modal/ListModalSection.tsx @@ -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 ( - {active ? : } - + {active || allContentApproved ? ( + + ) : ( + + )} + {isCollectionApprovalSection ? ( <> Approve {content.length}  @@ -165,7 +176,10 @@ export const ListModalSection = ({ sectionType, active, content, toggleSection } {content.map((row) => { return ( - + {isCollectionApprovalSection ? ( ) : ( @@ -174,7 +188,23 @@ export const ListModalSection = ({ sectionType, active, content, toggleSection } {row.name} {isCollectionApprovalSection && (row as CollectionRow).isVerified && } - + + {row.status === ListingStatus.DEFINED || row.status === ListingStatus.PENDING ? ( + + ) : row.status === ListingStatus.SIGNING ? ( + + Proceed in wallet + + ) : ( + row.status === ListingStatus.APPROVED && ( + + ) + )} + ) })} diff --git a/src/nft/hooks/useSellAsset.ts b/src/nft/hooks/useSellAsset.ts index 757a707e08..b9a87be859 100644 --- a/src/nft/hooks/useSellAsset.ts +++ b/src/nft/hooks/useSellAsset.ts @@ -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 diff --git a/src/nft/types/sell/sell.ts b/src/nft/types/sell/sell.ts index d30f1f7a2b..eb4d2ef30c 100644 --- a/src/nft/types/sell/sell.ts +++ b/src/nft/types/sell/sell.ts @@ -118,8 +118,3 @@ export enum ProfilePageStateType { VIEWING, LISTING, } - -export enum ListingResponse { - TRY_AGAIN, - SUCCESS, -} diff --git a/src/nft/utils/listNfts.ts b/src/nft/utils/listNfts.ts index a85fc4f096..c44b19f019 100644 --- a/src/nft/utils/listNfts.ts +++ b/src/nft/utils/listNfts.ts @@ -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