feat: [ListV2] Success modal + Twitter Share (#5924)

* added success screen with images

* add listing proceeds

* working return

* working single tweet

* working tweet multiple

* update how we parse twitterName

* add scrollbar styles

* usestablecoinvalue

* math.min

* add collection name backup to tweet

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2023-02-06 12:45:13 -08:00 committed by GitHub
parent 8f922b665a
commit f26b09537d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 183 additions and 10 deletions

@ -1004,7 +1004,7 @@ export type NftBalanceQueryVariables = Exact<{
}>;
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export const TokenDocument = gql`
@ -1622,6 +1622,7 @@ export const NftBalanceDocument = gql`
url
}
name
twitterName
nftContracts {
address
chain

@ -34,6 +34,7 @@ gql`
url
}
name
twitterName
nftContracts {
address
chain
@ -166,7 +167,12 @@ export function useNftBalance(
image_url: asset?.collection?.image?.url,
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
},
collection: asset?.collection as unknown as GenieCollection,
collection: {
name: asset.collection?.name,
isVerified: asset.collection?.isVerified,
imageUrl: asset.collection?.image?.url,
twitterUrl: asset.collection?.twitterName ? `@${asset.collection?.twitterName}` : undefined,
} as GenieCollection,
collectionIsVerified: asset?.collection?.isVerified,
lastPrice: queryAsset.node.lastPrice?.value,
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,

@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
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'
@ -15,7 +14,9 @@ import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { TitleRow } from '../shared'
import { ListModalSection, Section } from './ListModalSection'
import { SuccessScreen } from './SuccessScreen'
const ListModalWrapper = styled.div`
position: fixed;
@ -39,11 +40,6 @@ const ListModalWrapper = styled.div`
}
`
const TitleRow = styled(Row)`
justify-content: space-between;
margin-bottom: 8px;
`
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
const { provider } = useWeb3React()
const signer = provider?.getSigner()
@ -101,7 +97,7 @@ export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
<Trace modal={InterfaceModalName.NFT_LISTING}>
<ListModalWrapper>
{listingStatus === ListingStatus.APPROVED ? (
<>TODO Success Screen</>
<SuccessScreen overlayClick={overlayClick} />
) : (
<>
<TitleRow>

@ -0,0 +1,129 @@
import { Trans } from '@lingui/macro'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import Row from 'components/Row'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { getTotalEthValue } from 'nft/components/bag/profile/utils'
import { useSellAsset } from 'nft/hooks'
import { formatEth, generateTweetForList, pluralize } from 'nft/utils'
import { useMemo } from 'react'
import { Twitter, X } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { TitleRow } from '../shared'
const SuccessImage = styled.img<{ numImages: number }>`
width: calc(${({ numImages }) => (numImages > 1 ? (numImages > 2 ? '33%' : '50%') : '100%')} - 12px);
border-radius: 12px;
`
const SuccessImageWrapper = styled(Row)`
flex-wrap: wrap;
gap: 12px;
justify-content: center;
overflow-y: scroll;
margin-bottom: 16px;
${ScrollBarStyles}
`
const ProceedsColumn = styled(Column)`
text-align: right;
`
const buttonStyle = css`
width: 182px;
cursor: pointer;
padding: 12px 0px;
text-align: center;
font-weight: 600;
font-size: 16px;
line-height: 20px;
border-radius: 12px;
border: none;
&:hover {
opacity: 0.6;
}
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: 100%;
margin-bottom: 8px;
}
`
const ReturnButton = styled.button`
background-color: ${({ theme }) => theme.backgroundInteractive};
color: ${({ theme }) => theme.textPrimary};
${buttonStyle}
`
const TweetButton = styled.a`
background-color: ${({ theme }) => theme.accentAction};
color: ${({ theme }) => theme.textPrimary};
text-decoration: none;
${buttonStyle}
`
const TweetRow = styled(Row)`
justify-content: center;
gap: 4px;
`
export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => {
const theme = useTheme()
const sellAssets = useSellAsset((state) => state.sellAssets)
const nativeCurrency = useNativeCurrency()
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
const usdcValue = useStablecoinValue(parsedAmount)
return (
<>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>Successfully listed</Trans>&nbsp;{sellAssets.length > 1 ? ` ${sellAssets.length} ` : ''}
NFT{pluralize(sellAssets.length)}!
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
</TitleRow>
<SuccessImageWrapper>
{sellAssets.map((asset) => (
<SuccessImage
src={asset.imageUrl}
key={asset?.asset_contract?.address ?? '' + asset?.tokenId}
numImages={sellAssets.length}
/>
))}
</SuccessImageWrapper>
<Row justify="space-between" align="flex-start" marginBottom="16px">
<ThemedText.SubHeader lineHeight="24px">
<Trans>Proceeds if sold</Trans>
</ThemedText.SubHeader>
<ProceedsColumn>
<ThemedText.SubHeader lineHeight="24px">{formatEth(totalEthListingValue)} ETH</ThemedText.SubHeader>
{usdcValue && (
<ThemedText.BodySmall lineHeight="20px" color="textSecondary">
{formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)}
</ThemedText.BodySmall>
)}
</ProceedsColumn>
</Row>
<Row justify="space-between" flexWrap="wrap">
<ReturnButton onClick={() => window.location.reload()}>
<Trans>Return to My NFTs</Trans>
</ReturnButton>
<TweetButton href={generateTweetForList(sellAssets)} target="_blank" rel="noreferrer">
<TweetRow>
<Twitter height={20} width={20} color={theme.textPrimary} fill={theme.textPrimary} />
<Trans>Share on Twitter</Trans>
</TweetRow>
</TweetButton>
</Row>
</>
)
}

@ -1,3 +1,4 @@
import Row from 'components/Row'
import styled from 'styled-components/macro'
export const RemoveIconWrap = styled.div<{ hovered: boolean }>`
@ -8,3 +9,8 @@ export const RemoveIconWrap = styled.div<{ hovered: boolean }>`
width: 32px;
visibility: ${({ hovered }) => (hovered ? 'visible' : 'hidden')};
`
export const TitleRow = styled(Row)`
justify-content: space-between;
margin-bottom: 8px;
`

@ -1,4 +1,4 @@
import { DetailsOrigin, GenieAsset, UpdatedGenieAsset, WalletAsset } from 'nft/types'
import { DetailsOrigin, GenieAsset, Listing, UpdatedGenieAsset, WalletAsset } from 'nft/types'
export function getRarityStatus(
rarityStatusCache: Map<string, boolean>,
@ -44,3 +44,38 @@ export const generateTweetForPurchase = (assets: UpdatedGenieAsset[], txHashUrl:
} with @Uniswap 🦄\n\nhttps://app.uniswap.org/#/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769\n${txHashUrl}`
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
}
function getMinListingPrice(listings: Listing[]): number {
return Math.min(...listings.map((listing) => listing.price ?? 0)) ?? 0
}
function mapAssetsToCollections(assets: WalletAsset[]): { collection: string; items: string[] }[] {
const collections = assets.map((asset) => asset.collection?.twitterUrl ?? asset.collection?.name ?? '')
const uniqueCollections = [...new Set(collections)]
return uniqueCollections.map((collection) => {
return {
collection,
items: assets
.filter((asset) => asset.collection?.twitterUrl === collection || asset.collection?.name === collection)
.map((asset) => asset.name ?? ''),
}
})
}
export const generateTweetForList = (assets: WalletAsset[]): string => {
const tweetText =
assets.length == 1
? `I just listed ${
assets[0].collection?.twitterUrl
? `${assets[0].collection?.twitterUrl} `
: `${assets[0].collection?.name} ` ?? ''
}${assets[0].name} for ${getMinListingPrice(assets[0].newListings ?? [])} ETH on ${assets[0].marketplaces
?.map((market) => market.name)
.join(', ')}. Buy it on @Uniswap at https://app.uniswap.org/#${getAssetHref(assets[0])}`
: `I just listed ${
assets.length
} items on @Uniswap at https://app.uniswap.org/#/nfts/profile\n\nCollections: ${mapAssetsToCollections(assets)
.map(({ collection, items }) => `${collection} ${items.map((item) => item).join(', ')}`)
.join(', ')} \n\nMarketplaces: ${assets[0].marketplaces?.map((market) => market.name).join(', ')}`
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
}