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 Column from 'components/Column'
import Row from 'components/Row' import Row from 'components/Row'
import { HandHoldingDollarIcon, VerifiedIcon } from 'nft/components/icons' import { VerifiedIcon } from 'nft/components/icons'
import { GenieAsset } from 'nft/types' import { GenieAsset } from 'nft/types'
import { formatEth } from 'nft/utils' import styled from 'styled-components/macro'
import styled, { css } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme' import { BREAKPOINTS, ThemedText } from 'theme'
import { BuyButton } from './BuyButton'
const HeaderContainer = styled(Row)` const HeaderContainer = styled(Row)`
gap: 24px; gap: 24px;
` `
...@@ -28,38 +27,7 @@ const AssetText = styled(Column)` ...@@ -28,38 +27,7 @@ const AssetText = styled(Column)`
margin-right: auto; 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 }) => { export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
const price = asset.sellorders?.[0]?.price.value
return ( return (
<HeaderContainer> <HeaderContainer>
<AssetImage src={asset.imageUrl} /> <AssetImage src={asset.imageUrl} />
...@@ -73,21 +41,7 @@ export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => { ...@@ -73,21 +41,7 @@ export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
</ThemedText.HeadlineMedium> </ThemedText.HeadlineMedium>
</AssetText> </AssetText>
<Row justifySelf="flex-end" width="min-content" gap="12px"> <Row justifySelf="flex-end" width="min-content" gap="12px">
{price ? ( <BuyButton asset={asset} onDataPage />
<>
<BuyButton>
<Trans>Buy</Trans>
<Price>{formatEth(price)} ETH</Price>
</BuyButton>
<MakeOfferButtonSmall>
<HandHoldingDollarIcon />
</MakeOfferButtonSmall>
</>
) : (
<MakeOfferButtonLarge>
<Trans>Make an offer</Trans>
</MakeOfferButtonLarge>
)}
</Row> </Row>
</HeaderContainer> </HeaderContainer>
) )
......
...@@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react' ...@@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
import { BuyButton } from './BuyButton'
import { InfoChips } from './InfoChips' import { InfoChips } from './InfoChips'
import { MediaRenderer } from './MediaRenderer' import { MediaRenderer } from './MediaRenderer'
...@@ -144,6 +145,7 @@ export const LandingPage = ({ asset, collection, setShowDataHeader }: LandingPag ...@@ -144,6 +145,7 @@ export const LandingPage = ({ asset, collection, setShowDataHeader }: LandingPag
<StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText> <StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText>
</InfoDetailsContainer> </InfoDetailsContainer>
<InfoChips asset={asset} /> <InfoChips asset={asset} />
<BuyButton asset={asset} />
</InfoContainer> </InfoContainer>
</LandingPageContainer> </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`] = ` ...@@ -260,6 +260,17 @@ exports[`data page loads with header showing 1`] = `
background-color: #bdc8f3; 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 { .c6 {
gap: 24px; gap: 24px;
} }
...@@ -276,17 +287,6 @@ exports[`data page loads with header showing 1`] = ` ...@@ -276,17 +287,6 @@ exports[`data page loads with header showing 1`] = `
margin-right: auto; 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 { .c0 {
padding: 24px 64px; padding: 24px 64px;
height: 100vh; height: 100vh;
...@@ -776,6 +776,17 @@ exports[`data page loads without header showing 1`] = ` ...@@ -776,6 +776,17 @@ exports[`data page loads without header showing 1`] = `
background-color: #bdc8f3; 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 { .c6 {
gap: 24px; gap: 24px;
} }
...@@ -792,17 +803,6 @@ exports[`data page loads without header showing 1`] = ` ...@@ -792,17 +803,6 @@ exports[`data page loads without header showing 1`] = `
margin-right: auto; 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 { .c0 {
padding: 24px 64px; padding: 24px 64px;
height: 100vh; height: 100vh;
......
...@@ -104,6 +104,20 @@ exports[`Header loads with asset with a sell order 1`] = ` ...@@ -104,6 +104,20 @@ exports[`Header loads with asset with a sell order 1`] = `
color: #0D111C; 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 { .c12 {
padding: 16px; padding: 16px;
width: 100%; width: 100%;
...@@ -191,34 +205,15 @@ exports[`Header loads with asset with a sell order 1`] = ` ...@@ -191,34 +205,15 @@ exports[`Header loads with asset with a sell order 1`] = `
outline: none; outline: none;
} }
.c4 { .c16 {
display: -webkit-box; padding: 16px;
display: -webkit-flex; width: -webkit-min-content;
display: -ms-flexbox; width: -moz-min-content;
display: flex; width: min-content;
-webkit-flex-direction: column; -webkit-flex-shrink: 0;
-ms-flex-direction: column; -ms-flex-negative: 0;
flex-direction: column; flex-shrink: 0;
-webkit-box-pack: start; border-radius: 16px;
-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;
} }
.c14 { .c14 {
...@@ -246,15 +241,20 @@ exports[`Header loads with asset with a sell order 1`] = ` ...@@ -246,15 +241,20 @@ exports[`Header loads with asset with a sell order 1`] = `
color: #F5F6FCb8; color: #F5F6FCb8;
} }
.c16 { .c2 {
padding: 16px; gap: 24px;
width: -webkit-min-content; }
width: -moz-min-content;
width: min-content; .c3 {
-webkit-flex-shrink: 0; width: 96px;
-ms-flex-negative: 0; height: 96px;
flex-shrink: 0; border-radius: 20px;
border-radius: 16px; object-fit: cover;
}
.c5 {
gap: 4px;
margin-right: auto;
} }
@media screen and (max-width:1024px) { @media screen and (max-width:1024px) {
...@@ -315,7 +315,7 @@ exports[`Header loads with asset with a sell order 1`] = ` ...@@ -315,7 +315,7 @@ exports[`Header loads with asset with a sell order 1`] = `
<div <div
class="c15" class="c15"
> >
100M ETH 100.00M ETH
</div> </div>
</button> </button>
<button <button
...@@ -443,6 +443,20 @@ exports[`Header loads with asset with no sell orders 1`] = ` ...@@ -443,6 +443,20 @@ exports[`Header loads with asset with no sell orders 1`] = `
color: #0D111C; 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 { .c12 {
padding: 16px; padding: 16px;
width: 100%; width: 100%;
...@@ -514,18 +528,15 @@ exports[`Header loads with asset with no sell orders 1`] = ` ...@@ -514,18 +528,15 @@ exports[`Header loads with asset with no sell orders 1`] = `
background-color: #bdc8f3; background-color: #bdc8f3;
} }
.c4 { .c14 {
display: -webkit-box; white-space: nowrap;
display: -webkit-flex; width: -webkit-min-content;
display: -ms-flexbox; width: -moz-min-content;
display: flex; width: min-content;
-webkit-flex-direction: column; -webkit-flex-shrink: 0;
-ms-flex-direction: column; -ms-flex-negative: 0;
flex-direction: column; flex-shrink: 0;
-webkit-box-pack: start; border-radius: 16px;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
} }
.c2 { .c2 {
...@@ -544,17 +555,6 @@ exports[`Header loads with asset with no sell orders 1`] = ` ...@@ -544,17 +555,6 @@ exports[`Header loads with asset with no sell orders 1`] = `
margin-right: auto; 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) { @media screen and (max-width:1024px) {
.c3 { .c3 {
display: none; display: none;
......
...@@ -8,6 +8,29 @@ exports[`LandingPage renders it correctly 1`] = ` ...@@ -8,6 +8,29 @@ exports[`LandingPage renders it correctly 1`] = `
min-width: 0; 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 { .c10 {
width: 100%; width: 100%;
display: -webkit-box; display: -webkit-box;
...@@ -138,6 +161,88 @@ exports[`LandingPage renders it correctly 1`] = ` ...@@ -138,6 +161,88 @@ exports[`LandingPage renders it correctly 1`] = `
align-items: center; 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 { .c21 {
background-color: #FFFFFF; background-color: #FFFFFF;
padding: 10px 12px 10px 8px; padding: 10px 12px 10px 8px;
...@@ -442,6 +547,11 @@ exports[`LandingPage renders it correctly 1`] = ` ...@@ -442,6 +547,11 @@ exports[`LandingPage renders it correctly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<button
class="c25 c26 c27 c28"
>
Make an offer
</button>
</div> </div>
</div> </div>
</DocumentFragment> </DocumentFragment>
......
...@@ -34,3 +34,9 @@ export const Scrim = styled.div<{ isBottom?: boolean }>` ...@@ -34,3 +34,9 @@ export const Scrim = styled.div<{ isBottom?: boolean }>`
`linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`}; `linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`};
display: flex; display: flex;
` `
export const ButtonStyles = css`
width: min-content;
flex-shrink: 0;
border-radius: 16px;
`
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks' import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { BagStatus } from 'nft/types' import { BagStatus, GenieAsset } from 'nft/types'
import { buildNftTradeInputFromBagItems, recalculateBagUsingPooledAssets } from 'nft/utils' import {
buildNftTradeInput,
buildNftTradeInputFromBagItems,
filterUpdatedAssetsByState,
recalculateBagUsingPooledAssets,
} from 'nft/utils'
import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag' import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
import { buildRouteResponse } from 'nft/utils/nftRoute' 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 { shallow } from 'zustand/shallow'
import { useBag } from './useBag' import { useBag } from './useBag'
...@@ -100,3 +106,48 @@ export function useFetchAssets(): () => Promise<void> { ...@@ -100,3 +106,48 @@ export function useFetchAssets(): () => Promise<void> {
tokenTradeInput, 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 { useWeb3React } from '@web3-react/core'
import { RouteResponse, UpdatedGenieAsset } from 'nft/types' import { RouteResponse, UpdatedGenieAsset } from 'nft/types'
import { useCallback } from 'react' import { useCallback } from 'react'
import shallow from 'zustand/shallow' import { shallow } from 'zustand/shallow'
import { useBag } from './useBag' import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction' import { useSendTransaction } from './useSendTransaction'
......
...@@ -23,7 +23,7 @@ export const buildNftTradeInputFromBagItems = (itemsInBag: BagItem[]): NftTradeI ...@@ -23,7 +23,7 @@ export const buildNftTradeInputFromBagItems = (itemsInBag: BagItem[]): NftTradeI
return buildNftTradeInput(assetsToBuy) return buildNftTradeInput(assetsToBuy)
} }
const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => { export const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => {
return assets.flatMap((asset) => { return assets.flatMap((asset) => {
const { id, address, marketplace, priceInfo, tokenId, tokenType } = 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