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

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
parent 783f42ab
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',
......
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useGqlRoutingFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.gqlRouting, BaseVariant.Enabled)
}
export { BaseVariant as GqlRoutingVariant }
This diff is collapsed.
......@@ -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>
......
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 }
......
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])
}
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,
])
}
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[]) => {
......
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])
}
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 }
}
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