chore: refactoring bag (#6039)
* chore: refactoring bag component to sub components (#6027) * moving totalEthPrice to hook * moving everything from bag to bag footer * moving transaction state to sep hook * explicit type * itemsInBag * fixing transaction tracking * chore: refactor useFetchAssets to make it more readable (#6043) * chore: refactoring useFetchAssets to make it more readable * remvoing eslint stuff * extracting what can be * comments * removing feature flag * changing return type of useUsd hook * zustand shallow
This commit is contained in:
parent
783f42abcc
commit
a362f8797a
@ -1,5 +1,4 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
@ -218,12 +217,6 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.swapWidget}
|
||||
label="Swap Widget"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={GqlRoutingVariant}
|
||||
value={useGqlRoutingFlag()}
|
||||
featureFlag={FeatureFlag.gqlRouting}
|
||||
label="GraphQL NFT Routing"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={NftGraphqlVariant}
|
||||
value={useNftGraphqlFlag()}
|
||||
|
@ -6,7 +6,6 @@ export enum FeatureFlag {
|
||||
permit2 = 'permit2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
swapWidget = 'swap_widget_replacement_enabled',
|
||||
gqlRouting = 'gqlRouting',
|
||||
statsigDummy = 'web_dummy_gate_amplitude_id',
|
||||
nftGraphql = 'nft_graphql_migration',
|
||||
taxService = 'tax_service_banner',
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useGqlRoutingFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.gqlRouting, BaseVariant.Enabled)
|
||||
}
|
||||
|
||||
export { BaseVariant as GqlRoutingVariant }
|
@ -1,38 +1,16 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { NFTEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
|
||||
import { BagFooter } from 'nft/components/bag/BagFooter'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Column } from 'nft/components/Flex'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import {
|
||||
useBag,
|
||||
useIsMobile,
|
||||
useProfilePageState,
|
||||
useSellAsset,
|
||||
useSendTransaction,
|
||||
useTransactionResponse,
|
||||
} from 'nft/hooks'
|
||||
import { useTokenInput } from 'nft/hooks/useTokenInput'
|
||||
import { fetchRoute } from 'nft/queries'
|
||||
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
|
||||
import {
|
||||
buildNftTradeInputFromBagItems,
|
||||
buildSellObject,
|
||||
formatAssetEventProperties,
|
||||
recalculateBagUsingPooledAssets,
|
||||
sortUpdatedAssets,
|
||||
} from 'nft/utils'
|
||||
import { buildRouteResponse } from 'nft/utils/nftRoute'
|
||||
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useBag, useIsMobile, useProfilePageState, useSellAsset } from 'nft/hooks'
|
||||
import { BagStatus, ProfilePageStateType } from 'nft/types'
|
||||
import { formatAssetEventProperties, recalculateBagUsingPooledAssets } from 'nft/utils'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
@ -120,8 +98,6 @@ const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
|
||||
)
|
||||
|
||||
const Bag = () => {
|
||||
const { account, provider } = useWeb3React()
|
||||
|
||||
const { resetSellAssets, sellAssets } = useSellAsset(
|
||||
({ reset, sellAssets }) => ({
|
||||
resetSellAssets: reset,
|
||||
@ -132,36 +108,16 @@ const Bag = () => {
|
||||
|
||||
const { setProfilePageState } = useProfilePageState(({ setProfilePageState }) => ({ setProfilePageState }))
|
||||
|
||||
const {
|
||||
bagStatus,
|
||||
setBagStatus,
|
||||
didOpenUnavailableAssets,
|
||||
setDidOpenUnavailableAssets,
|
||||
bagIsLocked,
|
||||
setLocked,
|
||||
reset,
|
||||
setItemsInBag,
|
||||
bagExpanded,
|
||||
toggleBag,
|
||||
setTotalEthPrice,
|
||||
setBagExpanded,
|
||||
} = useBag((state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }), shallow)
|
||||
const { bagStatus, bagIsLocked, reset, bagExpanded, toggleBag, setBagExpanded } = useBag(
|
||||
(state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }),
|
||||
shallow
|
||||
)
|
||||
const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag }))
|
||||
|
||||
const isProfilePage = useIsNftProfilePage()
|
||||
const isDetailsPage = useIsNftDetailsPage()
|
||||
const isNFTPage = useIsNftPage()
|
||||
const isMobile = useIsMobile()
|
||||
const usingGqlRouting = useGqlRoutingFlag() === GqlRoutingVariant.Enabled
|
||||
|
||||
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
|
||||
const transactionState = useSendTransaction((state) => state.state)
|
||||
const setTransactionState = useSendTransaction((state) => state.setState)
|
||||
const transactionStateRef = useRef(transactionState)
|
||||
const [setTransactionResponse] = useTransactionResponse((state) => [state.setTransactionResponse])
|
||||
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
|
||||
|
||||
@ -175,210 +131,14 @@ const Bag = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const { totalEthPrice } = useMemo(() => {
|
||||
const totalEthPrice = itemsInBag.reduce(
|
||||
(total, item) =>
|
||||
item.status !== BagItemStatus.UNAVAILABLE
|
||||
? total.add(
|
||||
BigNumber.from(
|
||||
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
|
||||
)
|
||||
)
|
||||
: total,
|
||||
BigNumber.from(0)
|
||||
)
|
||||
|
||||
return { totalEthPrice }
|
||||
}, [itemsInBag])
|
||||
|
||||
const purchaseAssets = async (routingData: RouteResponse, purchasingWithErc20: boolean) => {
|
||||
if (!provider || !routingData) return
|
||||
const purchaseResponse = await sendTransaction(
|
||||
provider?.getSigner(),
|
||||
itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset),
|
||||
routingData,
|
||||
purchasingWithErc20
|
||||
)
|
||||
if (
|
||||
purchaseResponse &&
|
||||
(transactionStateRef.current === TxStateType.Success || transactionStateRef.current === TxStateType.Failed)
|
||||
) {
|
||||
setLocked(false)
|
||||
setModalIsOpen(false)
|
||||
setTransactionResponse(purchaseResponse)
|
||||
setBagExpanded({ bagExpanded: false })
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseBag = useCallback(() => {
|
||||
setBagExpanded({ bagExpanded: false, manualClose: true })
|
||||
}, [setBagExpanded])
|
||||
|
||||
const [fetchGqlRoute] = useNftRouteLazyQuery()
|
||||
|
||||
const fetchAssets = async () => {
|
||||
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
|
||||
const ethSellObject = buildSellObject(
|
||||
itemsToBuy
|
||||
.reduce((ethTotal, asset) => ethTotal.add(BigNumber.from(asset.priceInfo.ETHPrice)), BigNumber.from(0))
|
||||
.toString()
|
||||
)
|
||||
|
||||
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
|
||||
!bagIsLocked && setLocked(true)
|
||||
setBagStatus(BagStatus.FETCHING_ROUTE)
|
||||
try {
|
||||
if (usingGqlRouting) {
|
||||
fetchGqlRoute({
|
||||
variables: {
|
||||
senderAddress: usingGqlRouting && account ? account : '',
|
||||
nftTrades: usingGqlRouting ? buildNftTradeInputFromBagItems(itemsInBag) : [],
|
||||
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (!data.nftRoute || !data.nftRoute.route) {
|
||||
setBagStatus(BagStatus.ADDING_TO_BAG)
|
||||
setLocked(false)
|
||||
return
|
||||
}
|
||||
|
||||
const purchasingWithErc20 = !!tokenTradeInput
|
||||
const { route, routeResponse } = buildRouteResponse(data.nftRoute, purchasingWithErc20)
|
||||
|
||||
const { hasPriceAdjustment, updatedAssets } = combineBuyItemsWithTxRoute(itemsToBuy, route)
|
||||
const shouldRefetchCalldata = hasPriceAdjustment && purchasingWithErc20
|
||||
|
||||
const fetchedPriceChangedAssets = updatedAssets
|
||||
.filter((asset) => asset.updatedPriceInfo)
|
||||
.sort(sortUpdatedAssets)
|
||||
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
|
||||
const fetchedUnchangedAssets = updatedAssets.filter(
|
||||
(asset) => !asset.updatedPriceInfo && !asset.isUnavailable
|
||||
)
|
||||
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
|
||||
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
|
||||
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
|
||||
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
|
||||
const shouldReview = hasAssetsInReview || hasUnavailableAssets
|
||||
|
||||
setItemsInBag([
|
||||
...fetchedUnavailableAssets.map((unavailableAsset) => ({
|
||||
asset: unavailableAsset,
|
||||
status: BagItemStatus.UNAVAILABLE,
|
||||
})),
|
||||
...fetchedPriceChangedAssets.map((changedAsset) => ({
|
||||
asset: changedAsset,
|
||||
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
|
||||
})),
|
||||
...fetchedUnchangedAssets.map((unchangedAsset) => ({
|
||||
asset: unchangedAsset,
|
||||
status: BagItemStatus.REVIEWED,
|
||||
})),
|
||||
])
|
||||
|
||||
let shouldLock = false
|
||||
|
||||
if (hasAssets) {
|
||||
if (!shouldReview) {
|
||||
if (shouldRefetchCalldata) {
|
||||
setBagStatus(BagStatus.CONFIRM_QUOTE)
|
||||
} else {
|
||||
purchaseAssets(routeResponse, purchasingWithErc20)
|
||||
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
|
||||
shouldLock = true
|
||||
}
|
||||
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
|
||||
else {
|
||||
setBagStatus(BagStatus.IN_REVIEW)
|
||||
}
|
||||
} else {
|
||||
setBagStatus(BagStatus.ADDING_TO_BAG)
|
||||
}
|
||||
|
||||
setLocked(shouldLock)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const routeData = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
|
||||
fetchRoute({
|
||||
toSell: [ethSellObject],
|
||||
toBuy: itemsToBuy,
|
||||
senderAddress: account ?? '',
|
||||
})
|
||||
)
|
||||
|
||||
const { updatedAssets } = combineBuyItemsWithTxRoute(itemsToBuy, routeData.route)
|
||||
|
||||
const fetchedPriceChangedAssets = updatedAssets
|
||||
.filter((asset) => asset.updatedPriceInfo)
|
||||
.sort(sortUpdatedAssets)
|
||||
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
|
||||
const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
|
||||
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
|
||||
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
|
||||
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
|
||||
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
|
||||
const shouldReview = hasAssetsInReview || hasUnavailableAssets
|
||||
|
||||
setItemsInBag([
|
||||
...fetchedUnavailableAssets.map((unavailableAsset) => ({
|
||||
asset: unavailableAsset,
|
||||
status: BagItemStatus.UNAVAILABLE,
|
||||
})),
|
||||
...fetchedPriceChangedAssets.map((changedAsset) => ({
|
||||
asset: changedAsset,
|
||||
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
|
||||
})),
|
||||
...fetchedUnchangedAssets.map((unchangedAsset) => ({
|
||||
asset: unchangedAsset,
|
||||
status: BagItemStatus.REVIEWED,
|
||||
})),
|
||||
])
|
||||
setLocked(false)
|
||||
|
||||
if (hasAssets) {
|
||||
if (!shouldReview) {
|
||||
purchaseAssets(routeData, false)
|
||||
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
|
||||
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
|
||||
else {
|
||||
setBagStatus(BagStatus.IN_REVIEW)
|
||||
}
|
||||
} else {
|
||||
setBagStatus(BagStatus.ADDING_TO_BAG)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setBagStatus(BagStatus.ADDING_TO_BAG)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (bagIsLocked && !isModalOpen) setModalIsOpen(true)
|
||||
}, [bagIsLocked, isModalOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
|
||||
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
|
||||
if (transactionStateRef.current === TxStateType.Invalid) setBagStatus(BagStatus.WARNING)
|
||||
else setBagStatus(BagStatus.CONFIRM_REVIEW)
|
||||
setTransactionState(TxStateType.New)
|
||||
|
||||
setLocked(false)
|
||||
setModalIsOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [transactionStateRef.current])
|
||||
|
||||
useEffect(() => {
|
||||
setTotalEthPrice(totalEthPrice)
|
||||
}, [totalEthPrice, setTotalEthPrice])
|
||||
|
||||
const hasAssetsToShow = itemsInBag.length > 0
|
||||
|
||||
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
|
||||
@ -422,7 +182,7 @@ const Bag = () => {
|
||||
{isProfilePage ? <ProfileBagContent /> : <BagContent />}
|
||||
</Column>
|
||||
{hasAssetsToShow && !isProfilePage && (
|
||||
<BagFooter totalEthPrice={totalEthPrice} fetchAssets={fetchAssets} eventProperties={eventProperties} />
|
||||
<BagFooter setModalIsOpen={setModalIsOpen} eventProperties={eventProperties} />
|
||||
)}
|
||||
{isSellingAssets && isProfilePage && (
|
||||
<ContinueButton
|
||||
|
@ -20,10 +20,13 @@ import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useBag } from 'nft/hooks/useBag'
|
||||
import { useBagTotalEthPrice } from 'nft/hooks/useBagTotalEthPrice'
|
||||
import useDerivedPayWithAnyTokenSwapInfo from 'nft/hooks/useDerivedPayWithAnyTokenSwapInfo'
|
||||
import { useFetchAssets } from 'nft/hooks/useFetchAssets'
|
||||
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
|
||||
import usePermit2Approval from 'nft/hooks/usePermit2Approval'
|
||||
import { PriceImpact, usePriceImpact } from 'nft/hooks/usePriceImpact'
|
||||
import { useSubscribeTransactionState } from 'nft/hooks/useSubscribeTransactionState'
|
||||
import { useTokenInput } from 'nft/hooks/useTokenInput'
|
||||
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
|
||||
import { BagStatus } from 'nft/types'
|
||||
@ -272,8 +275,7 @@ const FiatValue = ({
|
||||
}
|
||||
|
||||
interface BagFooterProps {
|
||||
totalEthPrice: BigNumber
|
||||
fetchAssets: () => void
|
||||
setModalIsOpen: (open: boolean) => void
|
||||
eventProperties: Record<string, unknown>
|
||||
}
|
||||
|
||||
@ -284,11 +286,12 @@ const PENDING_BAG_STATUSES = [
|
||||
BagStatus.PROCESSING_TRANSACTION,
|
||||
]
|
||||
|
||||
export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFooterProps) => {
|
||||
export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) => {
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
const totalEthPrice = useBagTotalEthPrice()
|
||||
const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled()
|
||||
const inputCurrency = useTokenInput((state) => state.inputCurrency)
|
||||
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
|
||||
@ -297,7 +300,6 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
|
||||
account ?? undefined,
|
||||
!!inputCurrency && inputCurrency.isToken ? inputCurrency : undefined
|
||||
)
|
||||
|
||||
const {
|
||||
isLocked: bagIsLocked,
|
||||
bagStatus,
|
||||
@ -312,13 +314,14 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
|
||||
|
||||
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
|
||||
const activeCurrency = inputCurrency ?? defaultCurrency
|
||||
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken && chainId === SupportedChainId.MAINNET
|
||||
|
||||
useSubscribeTransactionState(setModalIsOpen)
|
||||
const fetchAssets = useFetchAssets()
|
||||
|
||||
const parsedOutputAmount = useMemo(() => {
|
||||
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
|
||||
}, [defaultCurrency, totalEthPrice])
|
||||
@ -374,7 +377,7 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
|
||||
handleClick,
|
||||
buttonColor,
|
||||
} = useMemo(() => {
|
||||
let handleClick = fetchAssets
|
||||
let handleClick: (() => void) | (() => Promise<void>) = fetchAssets
|
||||
let buttonText = <Trans>Something went wrong</Trans>
|
||||
let disabled = true
|
||||
let warningText = undefined
|
||||
|
@ -2,14 +2,15 @@ import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { useBagTotalEthPrice, useBagTotalUsdPrice } from 'nft/hooks/useBagTotalEthPrice'
|
||||
import { ethNumberStandardFormatter, formatWeiToDecimal, roundAndPluralize } from 'nft/utils'
|
||||
|
||||
import * as styles from './MobileHoverBag.css'
|
||||
export const MobileHoverBag = () => {
|
||||
const itemsInBag = useBag((state) => state.itemsInBag)
|
||||
const toggleBag = useBag((state) => state.toggleBag)
|
||||
const totalEthPrice = useBag((state) => state.totalEthPrice)
|
||||
const totalUsdPrice = useBag((state) => state.totalUsdPrice)
|
||||
const totalEthPrice = useBagTotalEthPrice()
|
||||
const totalUsdPrice = useBagTotalUsdPrice()
|
||||
|
||||
const shouldShowBag = itemsInBag.length > 0
|
||||
|
||||
@ -47,11 +48,10 @@ export const MobileHoverBag = () => {
|
||||
{roundAndPluralize(itemsInBag.length, 'NFT')}
|
||||
</Box>
|
||||
<Row gap="8">
|
||||
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`}</Box>
|
||||
<Box color="textSecondary" className={bodySmall}>{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</Box>
|
||||
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`} ETH</Box>
|
||||
<Box color="textSecondary" className={bodySmall}>
|
||||
{ethNumberStandardFormatter(totalUsdPrice, true)}
|
||||
</Box>
|
||||
</Row>
|
||||
</Column>
|
||||
</Row>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@ -12,10 +11,6 @@ interface BagState {
|
||||
setBagStatus: (state: BagStatus) => void
|
||||
itemsInBag: BagItem[]
|
||||
setItemsInBag: (items: BagItem[]) => void
|
||||
totalEthPrice: BigNumber
|
||||
setTotalEthPrice: (totalEthPrice: BigNumber) => void
|
||||
totalUsdPrice: number | undefined
|
||||
setTotalUsdPrice: (totalUsdPrice: number | undefined) => void
|
||||
addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void
|
||||
removeAssetsFromBag: (assets: UpdatedGenieAsset[], fromSweep?: boolean) => void
|
||||
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
|
||||
@ -72,16 +67,6 @@ export const useBag = create<BagState>()(
|
||||
set(() => ({
|
||||
itemsInBag: items,
|
||||
})),
|
||||
totalEthPrice: BigNumber.from(0),
|
||||
setTotalEthPrice: (totalEthPrice) =>
|
||||
set(() => ({
|
||||
totalEthPrice,
|
||||
})),
|
||||
totalUsdPrice: undefined,
|
||||
setTotalUsdPrice: (totalUsdPrice) =>
|
||||
set(() => ({
|
||||
totalUsdPrice,
|
||||
})),
|
||||
addAssetsToBag: (assets, fromSweep = false) =>
|
||||
set(({ itemsInBag }) => {
|
||||
if (get().isLocked) return { itemsInBag: get().itemsInBag }
|
||||
|
44
src/nft/hooks/useBagTotalEthPrice.ts
Normal file
44
src/nft/hooks/useBagTotalEthPrice.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { BagItemStatus } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useBag } from './useBag'
|
||||
|
||||
export function useBagTotalEthPrice(): BigNumber {
|
||||
const itemsInBag = useBag((state) => state.itemsInBag)
|
||||
|
||||
return useMemo(() => {
|
||||
const totalEthPrice = itemsInBag.reduce(
|
||||
(total, item) =>
|
||||
item.status !== BagItemStatus.UNAVAILABLE
|
||||
? total.add(
|
||||
BigNumber.from(
|
||||
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
|
||||
)
|
||||
)
|
||||
: total,
|
||||
BigNumber.from(0)
|
||||
)
|
||||
|
||||
return totalEthPrice
|
||||
}, [itemsInBag])
|
||||
}
|
||||
|
||||
export function useBagTotalUsdPrice(): string | undefined {
|
||||
const totalEthPrice = useBagTotalEthPrice()
|
||||
const defaultCurrency = useCurrency('ETH')
|
||||
|
||||
const parsedOutputAmount = useMemo(() => {
|
||||
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
|
||||
}, [defaultCurrency, totalEthPrice])
|
||||
|
||||
const usdcValue = useStablecoinValue(parsedOutputAmount)
|
||||
|
||||
return useMemo(() => {
|
||||
return usdcValue?.toExact()
|
||||
}, [usdcValue])
|
||||
}
|
102
src/nft/hooks/useFetchAssets.ts
Normal file
102
src/nft/hooks/useFetchAssets.ts
Normal file
@ -0,0 +1,102 @@
|
||||
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 { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
|
||||
import { buildRouteResponse } from 'nft/utils/nftRoute'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
|
||||
import { useBag } from './useBag'
|
||||
import { usePurchaseAssets } from './usePurchaseAssets'
|
||||
import { useTokenInput } from './useTokenInput'
|
||||
|
||||
export function useFetchAssets(): () => Promise<void> {
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const {
|
||||
itemsInBag: uncheckedItemsInBag,
|
||||
setBagStatus,
|
||||
didOpenUnavailableAssets,
|
||||
setDidOpenUnavailableAssets,
|
||||
isLocked: bagIsLocked,
|
||||
setLocked: setBagLocked,
|
||||
setItemsInBag,
|
||||
} = useBag(
|
||||
({
|
||||
itemsInBag,
|
||||
setBagStatus,
|
||||
didOpenUnavailableAssets,
|
||||
setDidOpenUnavailableAssets,
|
||||
isLocked,
|
||||
setLocked,
|
||||
setItemsInBag,
|
||||
}) => ({
|
||||
itemsInBag,
|
||||
setBagStatus,
|
||||
didOpenUnavailableAssets,
|
||||
setDidOpenUnavailableAssets,
|
||||
isLocked,
|
||||
setLocked,
|
||||
setItemsInBag,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
|
||||
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
|
||||
|
||||
const [fetchGqlRoute] = useNftRouteLazyQuery()
|
||||
const purchaseAssets = usePurchaseAssets()
|
||||
|
||||
const resetStateBeforeFetch = useCallback(() => {
|
||||
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
|
||||
!bagIsLocked && setBagLocked(true)
|
||||
setBagStatus(BagStatus.FETCHING_ROUTE)
|
||||
}, [bagIsLocked, didOpenUnavailableAssets, setBagLocked, setBagStatus, setDidOpenUnavailableAssets])
|
||||
|
||||
return useCallback(async () => {
|
||||
resetStateBeforeFetch()
|
||||
|
||||
fetchGqlRoute({
|
||||
variables: {
|
||||
senderAddress: account ? account : '',
|
||||
nftTrades: buildNftTradeInputFromBagItems(itemsInBag),
|
||||
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (!data.nftRoute || !data.nftRoute.route) {
|
||||
setBagStatus(BagStatus.ADDING_TO_BAG)
|
||||
setBagLocked(false)
|
||||
return
|
||||
}
|
||||
|
||||
const wishAssetsToBuy = getPurchasableAssets(itemsInBag)
|
||||
const purchasingWithErc20 = !!tokenTradeInput
|
||||
const { route, routeResponse } = buildRouteResponse(data.nftRoute, purchasingWithErc20)
|
||||
|
||||
const { newBagItems, nextBagStatus } = getNextBagState(wishAssetsToBuy, route, purchasingWithErc20)
|
||||
|
||||
setItemsInBag(newBagItems)
|
||||
setBagStatus(nextBagStatus)
|
||||
|
||||
if (nextBagStatus === BagStatus.CONFIRMING_IN_WALLET) {
|
||||
purchaseAssets(routeResponse, wishAssetsToBuy, purchasingWithErc20)
|
||||
setBagLocked(true)
|
||||
return
|
||||
}
|
||||
|
||||
setBagLocked(false)
|
||||
},
|
||||
})
|
||||
}, [
|
||||
account,
|
||||
fetchGqlRoute,
|
||||
itemsInBag,
|
||||
purchaseAssets,
|
||||
resetStateBeforeFetch,
|
||||
setBagLocked,
|
||||
setBagStatus,
|
||||
setItemsInBag,
|
||||
tokenTradeInput,
|
||||
])
|
||||
}
|
52
src/nft/hooks/usePurchaseAssets.ts
Normal file
52
src/nft/hooks/usePurchaseAssets.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { RouteResponse, UpdatedGenieAsset } from 'nft/types'
|
||||
import { useCallback } from 'react'
|
||||
import shallow from 'zustand/shallow'
|
||||
|
||||
import { useBag } from './useBag'
|
||||
import { useSendTransaction } from './useSendTransaction'
|
||||
import { useTransactionResponse } from './useTransactionResponse'
|
||||
|
||||
export function usePurchaseAssets(): (
|
||||
routingData: RouteResponse,
|
||||
assetsToBuy: UpdatedGenieAsset[],
|
||||
purchasingWithErc20?: boolean
|
||||
) => Promise<void> {
|
||||
const { provider } = useWeb3React()
|
||||
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
|
||||
const setTransactionResponse = useTransactionResponse((state) => state.setTransactionResponse)
|
||||
|
||||
const {
|
||||
setLocked: setBagLocked,
|
||||
setBagExpanded,
|
||||
reset: resetBag,
|
||||
} = useBag(
|
||||
({ setLocked, setBagExpanded, reset }) => ({
|
||||
setLocked,
|
||||
setBagExpanded,
|
||||
reset,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async (routingData: RouteResponse, assetsToBuy: UpdatedGenieAsset[], purchasingWithErc20 = false) => {
|
||||
if (!provider) return
|
||||
|
||||
const purchaseResponse = await sendTransaction(
|
||||
provider.getSigner(),
|
||||
assetsToBuy,
|
||||
routingData,
|
||||
purchasingWithErc20
|
||||
)
|
||||
|
||||
if (purchaseResponse) {
|
||||
setBagLocked(false)
|
||||
setTransactionResponse(purchaseResponse)
|
||||
setBagExpanded({ bagExpanded: false })
|
||||
resetBag()
|
||||
}
|
||||
},
|
||||
[provider, resetBag, sendTransaction, setBagExpanded, setBagLocked, setTransactionResponse]
|
||||
)
|
||||
}
|
@ -12,7 +12,7 @@ import ERC721 from '../../abis/erc721.json'
|
||||
import ERC1155 from '../../abis/erc1155.json'
|
||||
import CryptoPunksMarket from '../abis/CryptoPunksMarket.json'
|
||||
import { GenieAsset, RouteResponse, RoutingItem, TxResponse, TxStateType, UpdatedGenieAsset } from '../types'
|
||||
import { combineBuyItemsWithTxRoute } from '../utils/txRoute/combineItemsWithTxRoute'
|
||||
import { compareAssetsWithTransactionRoute } from '../utils/txRoute/combineItemsWithTxRoute'
|
||||
|
||||
interface TxState {
|
||||
state: TxStateType
|
||||
@ -147,7 +147,7 @@ const findNFTsPurchased = (
|
||||
)
|
||||
})
|
||||
|
||||
return combineBuyItemsWithTxRoute(transferredItems, txRoute).updatedAssets
|
||||
return compareAssetsWithTransactionRoute(transferredItems, txRoute).updatedAssets
|
||||
}
|
||||
|
||||
const findNFTsNotPurchased = (toBuy: GenieAsset[], nftsPurchased: UpdatedGenieAsset[]) => {
|
||||
|
38
src/nft/hooks/useSubscribeTransactionState.ts
Normal file
38
src/nft/hooks/useSubscribeTransactionState.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { BagStatus, TxStateType } from 'nft/types'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
|
||||
import { useBag } from './useBag'
|
||||
import { useSendTransaction } from './useSendTransaction'
|
||||
|
||||
export function useSubscribeTransactionState(setModalIsOpen: (isOpen: boolean) => void) {
|
||||
const transactionState = useSendTransaction((state) => state.state)
|
||||
const setTransactionState = useSendTransaction((state) => state.setState)
|
||||
const transactionStateRef = useRef(transactionState)
|
||||
const { setBagStatus, setLocked: setBagLocked } = useBag(
|
||||
({ setBagExpanded, setBagStatus, setLocked }) => ({
|
||||
setBagExpanded,
|
||||
setBagStatus,
|
||||
setLocked,
|
||||
}),
|
||||
shallow
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
|
||||
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
|
||||
if (transactionStateRef.current === TxStateType.Invalid) {
|
||||
setBagStatus(BagStatus.WARNING)
|
||||
} else setBagStatus(BagStatus.CONFIRM_REVIEW)
|
||||
setTransactionState(TxStateType.New)
|
||||
|
||||
setBagLocked(false)
|
||||
setModalIsOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setBagLocked, setBagStatus, setModalIsOpen, setTransactionState, transactionStateRef.current])
|
||||
}
|
75
src/nft/utils/bag.ts
Normal file
75
src/nft/utils/bag.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { BagItem, BagItemStatus, BagStatus, RoutingItem, UpdatedGenieAsset } from 'nft/types'
|
||||
|
||||
import { compareAssetsWithTransactionRoute } from './txRoute/combineItemsWithTxRoute'
|
||||
import { filterUpdatedAssetsByState } from './updatedAssets'
|
||||
|
||||
export function getPurchasableAssets(itemsInBag: BagItem[]): UpdatedGenieAsset[] {
|
||||
return itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
|
||||
}
|
||||
|
||||
function createBagFromUpdatedAssets(
|
||||
unavailable: UpdatedGenieAsset[],
|
||||
priceChanged: UpdatedGenieAsset[],
|
||||
unchanged: UpdatedGenieAsset[]
|
||||
): BagItem[] {
|
||||
return [
|
||||
...unavailable.map((unavailableAsset) => ({
|
||||
asset: unavailableAsset,
|
||||
status: BagItemStatus.UNAVAILABLE,
|
||||
})),
|
||||
...priceChanged.map((changedAsset) => ({
|
||||
asset: changedAsset,
|
||||
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
|
||||
})),
|
||||
...unchanged.map((unchangedAsset) => ({
|
||||
asset: unchangedAsset,
|
||||
status: BagItemStatus.REVIEWED,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
function evaluateNextBagState(
|
||||
hasAssets: boolean,
|
||||
shouldReview: boolean,
|
||||
hasAssetsInReview: boolean,
|
||||
shouldRefetchCalldata: boolean
|
||||
): BagStatus {
|
||||
if (!hasAssets) {
|
||||
return BagStatus.ADDING_TO_BAG
|
||||
}
|
||||
|
||||
if (shouldReview) {
|
||||
if (hasAssetsInReview) {
|
||||
return BagStatus.IN_REVIEW
|
||||
}
|
||||
|
||||
return BagStatus.CONFIRM_REVIEW
|
||||
}
|
||||
|
||||
if (shouldRefetchCalldata) {
|
||||
return BagStatus.CONFIRM_QUOTE
|
||||
}
|
||||
|
||||
return BagStatus.CONFIRMING_IN_WALLET
|
||||
}
|
||||
|
||||
export function getNextBagState(
|
||||
wishAssetsToBuy: UpdatedGenieAsset[],
|
||||
route: RoutingItem[],
|
||||
purchasingWithErc20: boolean
|
||||
): { newBagItems: BagItem[]; nextBagStatus: BagStatus } {
|
||||
const { hasPriceAdjustment, updatedAssets } = compareAssetsWithTransactionRoute(wishAssetsToBuy, route)
|
||||
const shouldRefetchCalldata = hasPriceAdjustment && purchasingWithErc20
|
||||
|
||||
const { unchanged, priceChanged, unavailable } = filterUpdatedAssetsByState(updatedAssets)
|
||||
|
||||
const hasAssets = updatedAssets.length > 0
|
||||
const hasAssetsInReview = priceChanged.length > 0
|
||||
const hasUnavailableAssets = unavailable.length > 0
|
||||
const shouldReview = hasAssetsInReview || hasUnavailableAssets
|
||||
|
||||
const newBagItems = createBagFromUpdatedAssets(unavailable, priceChanged, unchanged)
|
||||
const nextBagStatus = evaluateNextBagState(hasAssets, shouldReview, hasAssetsInReview, shouldRefetchCalldata)
|
||||
|
||||
return { newBagItems, nextBagStatus }
|
||||
}
|
@ -74,7 +74,7 @@ const itemInRouteAndSamePool = (
|
||||
)
|
||||
}
|
||||
|
||||
export const combineBuyItemsWithTxRoute = (
|
||||
export const compareAssetsWithTransactionRoute = (
|
||||
items: UpdatedGenieAsset[],
|
||||
txRoute?: RoutingItem[]
|
||||
): { hasPriceAdjustment: boolean; updatedAssets: UpdatedGenieAsset[] } => {
|
||||
|
@ -20,3 +20,15 @@ export const getTotalNftValue = (nfts: UpdatedGenieAsset[]): BigNumber => {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function filterUpdatedAssetsByState(assets: UpdatedGenieAsset[]): {
|
||||
unchanged: UpdatedGenieAsset[]
|
||||
priceChanged: UpdatedGenieAsset[]
|
||||
unavailable: UpdatedGenieAsset[]
|
||||
} {
|
||||
const unchanged = assets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
|
||||
const priceChanged = assets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets)
|
||||
const unavailable = assets.filter((asset) => asset.isUnavailable)
|
||||
|
||||
return { unchanged, priceChanged, unavailable }
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user