Commit caa2524e authored by Jack Short's avatar Jack Short Committed by GitHub

feat: [DetailsV2] instant buy (#6599)

* initial impl

* removing isopen change

* stopping refetching

* shared button

* pending animiation

* updating shared

* updating snapshots

* adding disabled state

* isLoading in hook

* pulling out ternary

* removing fragment

* separate file for offer button

* fixing price diff check

* remove unnecessary export

* changing name to useBuyAssetCallback
parent d28a4b34
import { Trans } from '@lingui/macro'
import { formatNumber } from '@uniswap/conedison/format'
import { ButtonPrimary } from 'components/Button'
import Loader from 'components/Icons/LoadingSpinner'
import { useBuyAssetCallback } from 'nft/hooks/useFetchAssets'
import { GenieAsset } from 'nft/types'
import styled from 'styled-components/macro'
import { OfferButton } from './OfferButton'
import { ButtonStyles } from './shared'
const StyledBuyButton = styled(ButtonPrimary)`
display: flex;
flex-direction: row;
padding: 16px 24px;
gap: 8px;
line-height: 24px;
white-space: nowrap;
${ButtonStyles}
`
const Price = styled.div`
color: ${({ theme }) => theme.accentTextLightSecondary};
`
export const BuyButton = ({ asset, onDataPage }: { asset: GenieAsset; onDataPage?: boolean }) => {
const { fetchAndPurchaseSingleAsset, isLoading: isLoadingRoute } = useBuyAssetCallback()
const price = asset.sellorders?.[0]?.price.value
if (!price) {
return <OfferButton />
}
return (
<>
<StyledBuyButton disabled={isLoadingRoute} onClick={() => fetchAndPurchaseSingleAsset(asset)}>
{isLoadingRoute ? (
<>
<Trans>Fetching Route</Trans>
<Loader size="24px" stroke="white" />
</>
) : (
<>
<Trans>Buy</Trans>
<Price>{formatNumber(price)} ETH</Price>
</>
)}
</StyledBuyButton>
{onDataPage && <OfferButton smallVersion />}
</>
)
}
import { Trans } from '@lingui/macro'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import Column from 'components/Column'
import Row from 'components/Row'
import { HandHoldingDollarIcon, VerifiedIcon } from 'nft/components/icons'
import { VerifiedIcon } from 'nft/components/icons'
import { GenieAsset } from 'nft/types'
import { formatEth } from 'nft/utils'
import styled, { css } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { BuyButton } from './BuyButton'
const HeaderContainer = styled(Row)`
gap: 24px;
`
......@@ -28,38 +27,7 @@ const AssetText = styled(Column)`
margin-right: auto;
`
const ButtonStyles = css`
width: min-content;
flex-shrink: 0;
border-radius: 16px;
`
const BuyButton = styled(ButtonPrimary)`
display: flex;
flex-direction: row;
padding: 16px 24px;
gap: 8px;
line-height: 24px;
white-space: nowrap;
${ButtonStyles}
`
const Price = styled.div`
color: ${({ theme }) => theme.accentTextLightSecondary};
`
const MakeOfferButtonSmall = styled(ButtonPrimary)`
padding: 16px;
${ButtonStyles}
`
const MakeOfferButtonLarge = styled(ButtonGray)`
white-space: nowrap;
${ButtonStyles}
`
export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
const price = asset.sellorders?.[0]?.price.value
return (
<HeaderContainer>
<AssetImage src={asset.imageUrl} />
......@@ -73,21 +41,7 @@ export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
</ThemedText.HeadlineMedium>
</AssetText>
<Row justifySelf="flex-end" width="min-content" gap="12px">
{price ? (
<>
<BuyButton>
<Trans>Buy</Trans>
<Price>{formatEth(price)} ETH</Price>
</BuyButton>
<MakeOfferButtonSmall>
<HandHoldingDollarIcon />
</MakeOfferButtonSmall>
</>
) : (
<MakeOfferButtonLarge>
<Trans>Make an offer</Trans>
</MakeOfferButtonLarge>
)}
<BuyButton asset={asset} onDataPage />
</Row>
</HeaderContainer>
)
......
......@@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { BuyButton } from './BuyButton'
import { InfoChips } from './InfoChips'
import { MediaRenderer } from './MediaRenderer'
......@@ -144,6 +145,7 @@ export const LandingPage = ({ asset, collection, setShowDataHeader }: LandingPag
<StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText>
</InfoDetailsContainer>
<InfoChips asset={asset} />
<BuyButton asset={asset} />
</InfoContainer>
</LandingPageContainer>
)
......
import { Trans } from '@lingui/macro'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import { HandHoldingDollarIcon } from 'nft/components/icons'
import styled from 'styled-components/macro'
import { ButtonStyles } from './shared'
const MakeOfferButtonSmall = styled(ButtonPrimary)`
padding: 16px;
${ButtonStyles}
`
const MakeOfferButtonLarge = styled(ButtonGray)`
white-space: nowrap;
${ButtonStyles}
`
export const OfferButton = ({ smallVersion }: { smallVersion?: boolean }) => {
if (smallVersion) {
return (
<MakeOfferButtonSmall>
<HandHoldingDollarIcon />
</MakeOfferButtonSmall>
)
}
return (
<MakeOfferButtonLarge>
<Trans>Make an offer</Trans>
</MakeOfferButtonLarge>
)
}
......@@ -260,6 +260,17 @@ exports[`data page loads with header showing 1`] = `
background-color: #bdc8f3;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c6 {
gap: 24px;
}
......@@ -276,17 +287,6 @@ exports[`data page loads with header showing 1`] = `
margin-right: auto;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c0 {
padding: 24px 64px;
height: 100vh;
......@@ -776,6 +776,17 @@ exports[`data page loads without header showing 1`] = `
background-color: #bdc8f3;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c6 {
gap: 24px;
}
......@@ -792,17 +803,6 @@ exports[`data page loads without header showing 1`] = `
margin-right: auto;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c0 {
padding: 24px 64px;
height: 100vh;
......
......@@ -104,6 +104,20 @@ exports[`Header loads with asset with a sell order 1`] = `
color: #0D111C;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c12 {
padding: 16px;
width: 100%;
......@@ -191,34 +205,15 @@ exports[`Header loads with asset with a sell order 1`] = `
outline: none;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
gap: 24px;
}
.c3 {
width: 96px;
height: 96px;
border-radius: 20px;
object-fit: cover;
}
.c5 {
gap: 4px;
margin-right: auto;
.c16 {
padding: 16px;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c14 {
......@@ -246,15 +241,20 @@ exports[`Header loads with asset with a sell order 1`] = `
color: #F5F6FCb8;
}
.c16 {
padding: 16px;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
.c2 {
gap: 24px;
}
.c3 {
width: 96px;
height: 96px;
border-radius: 20px;
object-fit: cover;
}
.c5 {
gap: 4px;
margin-right: auto;
}
@media screen and (max-width:1024px) {
......@@ -315,7 +315,7 @@ exports[`Header loads with asset with a sell order 1`] = `
<div
class="c15"
>
100M ETH
100.00M ETH
</div>
</button>
<button
......@@ -443,6 +443,20 @@ exports[`Header loads with asset with no sell orders 1`] = `
color: #0D111C;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c12 {
padding: 16px;
width: 100%;
......@@ -514,18 +528,15 @@ exports[`Header loads with asset with no sell orders 1`] = `
background-color: #bdc8f3;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
.c14 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c2 {
......@@ -544,17 +555,6 @@ exports[`Header loads with asset with no sell orders 1`] = `
margin-right: auto;
}
.c14 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
@media screen and (max-width:1024px) {
.c3 {
display: none;
......
......@@ -8,6 +8,29 @@ exports[`LandingPage renders it correctly 1`] = `
min-width: 0;
}
.c25 {
box-sizing: border-box;
margin: 0;
min-width: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
font-size: inherit;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: white;
background-color: primary;
border: 0;
border-radius: 4px;
}
.c10 {
width: 100%;
display: -webkit-box;
......@@ -138,6 +161,88 @@ exports[`LandingPage renders it correctly 1`] = `
align-items: center;
}
.c26 {
padding: 16px;
width: 100%;
font-weight: 500;
text-align: center;
border-radius: 20px;
outline: none;
border: 1px solid transparent;
color: #0D111C;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
will-change: transform;
-webkit-transition: -webkit-transform 450ms ease;
-webkit-transition: transform 450ms ease;
transition: transform 450ms ease;
-webkit-transform: perspective(1px) translateZ(0);
-ms-transform: perspective(1px) translateZ(0);
transform: perspective(1px) translateZ(0);
}
.c26:disabled {
opacity: 50%;
cursor: auto;
pointer-events: none;
}
.c26 > * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c26 > a {
-webkit-text-decoration: none;
text-decoration: none;
}
.c27 {
background-color: #F5F6FC;
color: #7780A0;
font-size: 16px;
font-weight: 500;
}
.c27:hover {
background-color: #d2daf7;
}
.c27:active {
background-color: #bdc8f3;
}
.c28 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c21 {
background-color: #FFFFFF;
padding: 10px 12px 10px 8px;
......@@ -442,6 +547,11 @@ exports[`LandingPage renders it correctly 1`] = `
</div>
</div>
</div>
<button
class="c25 c26 c27 c28"
>
Make an offer
</button>
</div>
</div>
</DocumentFragment>
......
......@@ -34,3 +34,9 @@ export const Scrim = styled.div<{ isBottom?: boolean }>`
`linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`};
display: flex;
`
export const ButtonStyles = css`
width: min-content;
flex-shrink: 0;
border-radius: 16px;
`
import { useWeb3React } from '@web3-react/core'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { BagStatus } from 'nft/types'
import { buildNftTradeInputFromBagItems, recalculateBagUsingPooledAssets } from 'nft/utils'
import { BagStatus, GenieAsset } from 'nft/types'
import {
buildNftTradeInput,
buildNftTradeInputFromBagItems,
filterUpdatedAssetsByState,
recalculateBagUsingPooledAssets,
} from 'nft/utils'
import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { useCallback, useMemo } from 'react'
import { compareAssetsWithTransactionRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useMemo, useState } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
......@@ -100,3 +106,48 @@ export function useFetchAssets(): () => Promise<void> {
tokenTradeInput,
])
}
export const useBuyAssetCallback = () => {
const { account } = useWeb3React()
const [fetchGqlRoute] = useNftRouteLazyQuery()
const purchaseAssets = usePurchaseAssets()
const [isLoading, setIsLoading] = useState(false)
const fetchAndPurchaseSingleAsset = useCallback(
async (asset: GenieAsset) => {
setIsLoading(true)
fetchGqlRoute({
variables: {
senderAddress: account ? account : '',
nftTrades: buildNftTradeInput([asset]),
tokenTrades: undefined,
},
pollInterval: 0,
fetchPolicy: 'no-cache',
onCompleted: (data) => {
setIsLoading(false)
if (!data.nftRoute || !data.nftRoute.route) {
return
}
const { route, routeResponse } = buildRouteResponse(data.nftRoute, false)
const { updatedAssets } = compareAssetsWithTransactionRoute([asset], route)
const { priceChanged, unavailable } = filterUpdatedAssetsByState(updatedAssets)
const invalidData = priceChanged.length > 0 || unavailable.length > 0
if (invalidData) {
return
}
purchaseAssets(routeResponse, updatedAssets, false)
},
})
},
[account, fetchGqlRoute, purchaseAssets]
)
return useMemo(() => ({ fetchAndPurchaseSingleAsset, isLoading }), [fetchAndPurchaseSingleAsset, isLoading])
}
import { useWeb3React } from '@web3-react/core'
import { RouteResponse, UpdatedGenieAsset } from 'nft/types'
import { useCallback } from 'react'
import shallow from 'zustand/shallow'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction'
......
......@@ -23,7 +23,7 @@ export const buildNftTradeInputFromBagItems = (itemsInBag: BagItem[]): NftTradeI
return buildNftTradeInput(assetsToBuy)
}
const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => {
export const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => {
return assets.flatMap((asset) => {
const { id, address, marketplace, priceInfo, tokenId, tokenType } = asset
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment