diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 1763f62f254..d3cf3be56c9 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2833,7 +2833,10 @@ "potentialEarningsAmount": "%{amount}/yr at %{apy}% APY", "depositNow": "Deposit Now", "strategyInfo": "Strategy Info", - "overview": "Overview" + "overview": "Overview", + "availableToAdd": "%{amount} %{symbol} available", + "earnUpTo": "Earn up to", + "addMore": "Add More" }, "earn": { "enterFrom": "Enter from", diff --git a/src/components/AccountSelector/AccountSelector.tsx b/src/components/AccountSelector/AccountSelector.tsx index c8f5834ffe3..b59b9e669a3 100644 --- a/src/components/AccountSelector/AccountSelector.tsx +++ b/src/components/AccountSelector/AccountSelector.tsx @@ -32,6 +32,8 @@ export type AccountSelectorProps = { disabled?: boolean buttonProps?: ButtonProps boxProps?: BoxProps + showBalance?: boolean + showIcon?: boolean } const chevronIconSx = { @@ -39,7 +41,16 @@ const chevronIconSx = { } export const AccountSelector: FC = memo( - ({ assetId, accountId: selectedAccountId, onChange, disabled, buttonProps, boxProps }) => { + ({ + assetId, + accountId: selectedAccountId, + onChange, + disabled, + buttonProps, + boxProps, + showBalance = true, + showIcon = true, + }) => { const translate = useTranslate() const { isOpen, onOpen, onClose } = useDisclosure() const { @@ -120,7 +131,7 @@ export const AccountSelector: FC = memo( {...buttonProps} > - + {showIcon && } {accountNumber !== undefined && ( @@ -132,7 +143,7 @@ export const AccountSelector: FC = memo( )} - {selectedAccountDetails && ( + {selectedAccountDetails && showBalance && ( { const { colorScheme: c } = props + const isGreen = c === 'green' if (c === 'gray') { return { bg: 'background.button.secondary.base', @@ -79,8 +80,11 @@ export const ButtonStyle: ComponentStyleConfig = { return { bg: `${c}.500`, color: 'white', + _dark: { + color: isGreen ? 'black' : 'white', + }, _hover: { - bg: mode(`${c}.600`, `${c}.300`)(props), + bg: mode(`${c}.500`, `${c}.300`)(props), _disabled: { bg: `${c}.500`, }, diff --git a/src/components/Card/Card.theme.ts b/src/components/Card/Card.theme.ts index 714890a60fe..d12705163c6 100644 --- a/src/components/Card/Card.theme.ts +++ b/src/components/Card/Card.theme.ts @@ -44,7 +44,8 @@ export const CardStyle = { elevated: () => ({ container: { bg: 'background.surface.raised.base', - borderColor: 'border.base', + boxShadow: + '0 1px 0 var(--chakra-colors-border-base) inset, 0 0 0 1px var(--chakra-colors-border-base) inset', }, }), unstyled: { diff --git a/src/components/LazyLoadAvatar.tsx b/src/components/LazyLoadAvatar.tsx index 1a8298ea442..894ec3d706f 100644 --- a/src/components/LazyLoadAvatar.tsx +++ b/src/components/LazyLoadAvatar.tsx @@ -1,11 +1,13 @@ import type { AvatarProps, SkeletonProps } from '@chakra-ui/react' import { Avatar, SkeletonCircle } from '@chakra-ui/react' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' +import type { AvatarSize } from '@/components/Avatar/Avatar.theme' +import { AVATAR_SIZES } from '@/components/Avatar/Avatar.theme' import { imageLongPressSx } from '@/constants/longPress' export type LazyLoadAvatarProps = SkeletonProps & - Pick + Pick export const LazyLoadAvatar: React.FC = ({ src, @@ -13,7 +15,6 @@ export const LazyLoadAvatar: React.FC = ({ borderRadius, name, icon, - boxSize, bg, ...rest }) => { @@ -22,12 +23,14 @@ export const LazyLoadAvatar: React.FC = ({ const handleImageLoaded = useCallback(() => setImageLoaded(true), []) const handleImageError = useCallback(() => setImageError(true), []) + const skeletonSize = useMemo(() => { + return AVATAR_SIZES[size as AvatarSize] ?? AVATAR_SIZES.md + }, [size]) + return ( @@ -36,9 +39,8 @@ export const LazyLoadAvatar: React.FC = ({ onLoad={handleImageLoaded} onError={handleImageError} src={src} - size={size} icon={icon} - boxSize={boxSize} + boxSize='100%' name={name} borderRadius={borderRadius} sx={imageLongPressSx} diff --git a/src/pages/Yields/YieldAssetDetails.tsx b/src/pages/Yields/YieldAssetDetails.tsx index fe2c39fbf79..eb79ac86c61 100644 --- a/src/pages/Yields/YieldAssetDetails.tsx +++ b/src/pages/Yields/YieldAssetDetails.tsx @@ -3,7 +3,6 @@ import { Avatar, Box, Button, - Container, Flex, Heading, HStack, @@ -23,6 +22,7 @@ import { Amount } from '@/components/Amount/Amount' import { AssetIcon } from '@/components/AssetIcon' import { ChainIcon } from '@/components/ChainMenu' import { Display } from '@/components/Display' +import { Main } from '@/components/Layout/Main' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { bnOrZero } from '@/lib/bignumber/bignumber' import { @@ -531,42 +531,54 @@ export const YieldAssetDetails = memo(() => { effectiveViewMode, ]) + const headerComponent = useMemo( + () => ( + <> + + {assetHeaderElement} + + ), + [assetHeaderElement, navigate, translate], + ) + + const containerPaddingX = useMemo(() => ({ base: 4, xl: 0 }), []) + return ( - - - {assetHeaderElement} - - - - - {contentElement} - +
+ + + + + + {contentElement} + +
) }) diff --git a/src/pages/Yields/YieldDetail.tsx b/src/pages/Yields/YieldDetail.tsx index 884871fcd87..378b80249a4 100644 --- a/src/pages/Yields/YieldDetail.tsx +++ b/src/pages/Yields/YieldDetail.tsx @@ -1,6 +1,5 @@ -import { ArrowBackIcon } from '@chakra-ui/icons' import type { ResponsiveValue } from '@chakra-ui/react' -import { Box, Button, Container, Flex, Heading, IconButton, Stack, Text } from '@chakra-ui/react' +import { Box, Button, Flex, Heading, Stack, Text } from '@chakra-ui/react' import type { Property } from 'csstype' import { memo, useCallback, useEffect, useMemo } from 'react' import { useTranslate } from 'react-polyglot' @@ -8,6 +7,8 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { AccountSelector } from '@/components/AccountSelector/AccountSelector' import { Display } from '@/components/Display' +import { PageBackButton, PageHeader } from '@/components/Layout/Header/PageHeader' +import { Main } from '@/components/Layout/Main' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { bnOrZero } from '@/lib/bignumber/bignumber' import { @@ -43,14 +44,13 @@ import { } from '@/state/slices/selectors' import { useAppSelector } from '@/state/store' -const backIcon = - const layoutDirection: ResponsiveValue = { base: 'column', lg: 'row', } const actionColumnMaxWidth = { base: '100%', lg: '500px' } +const containerPaddingX = { base: 4, xl: 0 } export const YieldDetail = memo(() => { const { yieldId } = useParams<{ yieldId: string }>() @@ -113,7 +113,7 @@ export const YieldDetail = memo(() => { return defaultValidator } return validatorParam || defaultValidator - }, [yieldId, validatorParam, defaultValidator]) + }, [yieldId, defaultValidator, validatorParam]) const isStaking = yieldItem?.mechanics.type === 'staking' const shouldFetchValidators = isStaking && yieldItem?.mechanics.requiresValidatorSelection @@ -191,20 +191,20 @@ export const YieldDetail = memo(() => { const loadingElement = useMemo( () => ( - + {translate('common.loadingText')} - + ), [translate], ) const errorElement = useMemo( () => ( - + {translate('common.error')} @@ -216,69 +216,76 @@ export const YieldDetail = memo(() => { {translate('common.back')} - + ), [error, navigate, translate], ) - if (isLoading) return loadingElement - if (error || !yieldItem) return errorElement - - return ( - - - - + const headerComponent = useMemo( + () => ( + + + + + + + {showAccountSelector && selectorAssetId && ( + + + + )} + {showAccountSelector && selectorAssetId && ( - + + + )} - + + + ), + [handleBack, showAccountSelector, selectorAssetId, selectedAccountId, handleAccountChange], + ) + + if (isLoading) + return ( +
+ {loadingElement} +
+ ) + if (error || !yieldItem) + return ( +
+ {errorElement} +
+ ) + return ( +
+ - {showAccountSelector && selectorAssetId && ( - - - - )} - - {!isStaking && validatorOrProvider && ( { yieldItem={yieldItem} validatorOrProvider={validatorOrProvider} titleOverride={titleOverride} + userBalanceUserCurrency={userBalances.userCurrency} + userBalanceCrypto={userBalances.crypto} /> - + {!isStaking && validatorOrProvider && ( )} { balances={balances} isBalancesLoading={isBalancesLoading} selectedValidatorAddress={selectedValidatorAddress} + inputTokenMarketData={inputTokenMarketData} /> + - - {isModalOpen && } - + {isModalOpen && } + +
) }) diff --git a/src/pages/Yields/components/YieldAddMore.tsx b/src/pages/Yields/components/YieldAddMore.tsx new file mode 100644 index 00000000000..c125a354a59 --- /dev/null +++ b/src/pages/Yields/components/YieldAddMore.tsx @@ -0,0 +1,126 @@ +import { AddIcon } from '@chakra-ui/icons' +import { Alert, Box, Button, Flex, Text } from '@chakra-ui/react' +import qs from 'qs' +import { memo, useCallback, useMemo } from 'react' +import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' + +import { Amount } from '@/components/Amount/Amount' +import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' +import { bnOrZero } from '@/lib/bignumber/bignumber' +import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' +import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' + +const addIcon = + +type YieldAddMoreProps = { + yieldItem: AugmentedYieldDto + inputTokenMarketData: { price?: string } | undefined + hasPosition?: boolean +} + +export const YieldAddMore = memo( + ({ yieldItem, inputTokenMarketData, hasPosition }: YieldAddMoreProps) => { + const translate = useTranslate() + const navigate = useNavigate() + const { location } = useBrowserRouter() + + const inputToken = yieldItem.inputTokens[0] + const inputTokenAssetId = inputToken?.assetId ?? '' + const inputTokenPrecision = inputToken?.decimals + + const availableBalanceBaseUnit = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: inputTokenAssetId }), + ) + + const availableBalance = useMemo( + () => + typeof inputTokenPrecision === 'number' + ? bnOrZero(availableBalanceBaseUnit).shiftedBy(-inputTokenPrecision) + : bnOrZero(0), + [availableBalanceBaseUnit, inputTokenPrecision], + ) + + const availableBalanceFiat = useMemo( + () => availableBalance.times(bnOrZero(inputTokenMarketData?.price)), + [availableBalance, inputTokenMarketData?.price], + ) + + const potentialYearlyEarningsFiat = useMemo( + () => availableBalanceFiat.times(yieldItem.rewardRate.total), + [availableBalanceFiat, yieldItem.rewardRate.total], + ) + + const hasAvailableBalance = availableBalance.gt(0) + + const handleEnter = useCallback(() => { + const existingParams = qs.parse(location.search, { ignoreQueryPrefix: true }) + navigate({ + pathname: location.pathname, + search: qs.stringify({ ...existingParams, action: 'enter', modal: 'yield' }), + }) + }, [navigate, location.pathname, location.search]) + + // Only show when user has a position AND has available balance to add + if (!hasPosition || !hasAvailableBalance || typeof inputTokenPrecision !== 'number') return null + + return ( + + + + + + {translate('yieldXYZ.availableToAdd', { + amount: availableBalance.toFixed(), + symbol: yieldItem.token.symbol, + })} + + {potentialYearlyEarningsFiat.gt(0) && ( + + {translate('yieldXYZ.earnUpTo')}{' '} + + + )} + + + + + ) + }, +) diff --git a/src/pages/Yields/components/YieldAvailableToDeposit.tsx b/src/pages/Yields/components/YieldAvailableToDeposit.tsx index cf998872508..da885f334eb 100644 --- a/src/pages/Yields/components/YieldAvailableToDeposit.tsx +++ b/src/pages/Yields/components/YieldAvailableToDeposit.tsx @@ -1,9 +1,24 @@ import { InfoOutlineIcon } from '@chakra-ui/icons' -import { Box, Card, CardBody, Flex, Heading, HStack, Text, Tooltip, VStack } from '@chakra-ui/react' -import { memo, useMemo } from 'react' +import { + Alert, + Box, + Button, + Card, + CardBody, + Flex, + Heading, + HStack, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react' +import qs from 'qs' +import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useNavigate } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' +import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' @@ -12,11 +27,14 @@ import { useAppSelector } from '@/state/store' type YieldAvailableToDepositProps = { yieldItem: AugmentedYieldDto inputTokenMarketData: { price?: string } | undefined + hasPosition?: boolean } export const YieldAvailableToDeposit = memo( - ({ yieldItem, inputTokenMarketData }: YieldAvailableToDepositProps) => { + ({ yieldItem, inputTokenMarketData, hasPosition }: YieldAvailableToDepositProps) => { const translate = useTranslate() + const navigate = useNavigate() + const { location } = useBrowserRouter() const inputToken = yieldItem.inputTokens[0] const inputTokenAssetId = inputToken?.assetId ?? '' @@ -46,27 +64,39 @@ export const YieldAvailableToDeposit = memo( const hasAvailableBalance = availableBalance.gt(0) + const handleEnter = useCallback(() => { + navigate({ + pathname: location.pathname, + search: qs.stringify({ action: 'enter', modal: 'yield' }), + }) + }, [navigate, location.pathname]) + if (!inputTokenPrecision) return null const tooltipLabel = translate('yieldXYZ.availableToDepositTooltip', { symbol: yieldItem.token.symbol, }) - if (!hasAvailableBalance) return null + if (!hasAvailableBalance || hasPosition) return null return ( - - + + + - + {translate('yieldXYZ.availableToDeposit')} @@ -76,7 +106,7 @@ export const YieldAvailableToDeposit = memo( - + @@ -89,18 +119,24 @@ export const YieldAvailableToDeposit = memo( {potentialYearlyEarningsFiat.gt(0) && ( - - - {translate('yieldXYZ.potentialEarnings')} - - - + + + + {translate('yieldXYZ.potentialEarnings')} + + + + )} + diff --git a/src/pages/Yields/components/YieldCompareItem.tsx b/src/pages/Yields/components/YieldCompareItem.tsx new file mode 100644 index 00000000000..730c8019996 --- /dev/null +++ b/src/pages/Yields/components/YieldCompareItem.tsx @@ -0,0 +1,100 @@ +import { Avatar, Box, Card, CardBody, Flex, Tag, Text } from '@chakra-ui/react' +import { memo, useCallback, useMemo } from 'react' + +import { LazyLoadAvatar } from '@/components/LazyLoadAvatar' +import { chainIdToFeeAssetId } from '@/lib/utils' +import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' +import { GradientApy } from '@/pages/Yields/components/GradientApy' +import { selectAssetById } from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' + +type YieldCompareItemProps = { + yieldItem: AugmentedYieldDto + providerName?: string + providerIcon?: string + onClick?: () => void +} + +export const YieldCompareItem = memo( + ({ yieldItem, providerName, providerIcon, onClick }: YieldCompareItemProps) => { + const handleClick = useCallback(() => { + onClick?.() + }, [onClick]) + + const feeAssetId = useMemo( + () => (yieldItem.chainId ? chainIdToFeeAssetId(yieldItem.chainId) : undefined), + [yieldItem.chainId], + ) + + const feeAsset = useAppSelector(state => selectAssetById(state, feeAssetId ?? '')) + + const chainIcon = useMemo( + () => feeAsset?.networkIcon ?? feeAsset?.icon, + [feeAsset?.networkIcon, feeAsset?.icon], + ) + + const chainName = useMemo( + () => feeAsset?.networkName ?? feeAsset?.name, + [feeAsset?.networkName, feeAsset?.name], + ) + + const displayProviderName = useMemo( + () => providerName ?? yieldItem.providerId, + [providerName, yieldItem.providerId], + ) + + const apyFormatted = useMemo( + () => `${(yieldItem.rewardRate.total * 100).toFixed(2)}%`, + [yieldItem.rewardRate.total], + ) + + return ( + + + + + {chainIcon && ( + + )} + + + + {displayProviderName} + + {chainName && ( + + {chainName} + + )} + + + {apyFormatted} + + + + ) + }, +) diff --git a/src/pages/Yields/components/YieldHero.tsx b/src/pages/Yields/components/YieldHero.tsx index 0c4c4d4244b..472a6b8ad69 100644 --- a/src/pages/Yields/components/YieldHero.tsx +++ b/src/pages/Yields/components/YieldHero.tsx @@ -5,9 +5,14 @@ import { AlertIcon, Avatar, Badge, + Box, Button, + Card, + Flex, HStack, Link, + Tag, + TagLeftIcon, Text, VStack, } from '@chakra-ui/react' @@ -23,6 +28,9 @@ import { useBrowserRouter } from '@/hooks/useBrowserRouter/useBrowserRouter' import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { getYieldActionLabelKeys, resolveYieldInputAssetIcon } from '@/lib/yieldxyz/utils' +import { GradientApy } from '@/pages/Yields/components/GradientApy' +import { selectPortfolioCryptoBalanceBaseUnitByFilter } from '@/state/slices/selectors' +import { useAppSelector } from '@/state/store' const enterIcon = const exitIcon = @@ -40,6 +48,7 @@ type YieldHeroProps = { userBalanceCrypto: string validatorOrProvider: ValidatorOrProviderInfo titleOverride?: string + inputTokenMarketData: { price?: string } | undefined } export const YieldHero = memo( @@ -49,6 +58,7 @@ export const YieldHero = memo( userBalanceCrypto, validatorOrProvider, titleOverride, + inputTokenMarketData, }: YieldHeroProps) => { const navigate = useNavigate() const translate = useTranslate() @@ -56,7 +66,44 @@ export const YieldHero = memo( const iconSource = resolveYieldInputAssetIcon(yieldItem) const apy = bnOrZero(yieldItem.rewardRate.total).times(100).toFixed(2) - const hasExitBalance = bnOrZero(userBalanceCrypto).gt(0) + const hasPosition = bnOrZero(userBalanceCrypto).gt(0) + + // Available to deposit logic + const inputToken = yieldItem.inputTokens[0] + const inputTokenAssetId = inputToken?.assetId ?? '' + const inputTokenPrecision = inputToken?.decimals + + const assetIcon = useMemo(() => { + if (iconSource.assetId) { + return + } + return + }, [iconSource]) + + const availableBalanceBaseUnit = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { assetId: inputTokenAssetId }), + ) + + const availableBalance = useMemo( + () => + inputTokenPrecision != null + ? bnOrZero(availableBalanceBaseUnit).shiftedBy(-inputTokenPrecision) + : bnOrZero(0), + [availableBalanceBaseUnit, inputTokenPrecision], + ) + + const availableBalanceFiat = useMemo( + () => availableBalance.times(bnOrZero(inputTokenMarketData?.price)), + [availableBalance, inputTokenMarketData?.price], + ) + + const potentialYearlyEarningsFiat = useMemo( + () => availableBalanceFiat.times(bnOrZero(yieldItem?.rewardRate?.total)), + [availableBalanceFiat, yieldItem?.rewardRate?.total], + ) + + const hasAvailableBalance = availableBalance.gt(0) + const showAvailableToDeposit = !hasPosition && hasAvailableBalance const [searchParams] = useSearchParams() const validator = searchParams.get('validator') @@ -116,125 +163,194 @@ export const YieldHero = memo( ]) return ( - - - {yieldTitle} - - - {yieldItem.metadata.deprecated && ( - - - - {translate('yieldXYZ.deprecatedDescription')} - - - )} - - {yieldItem.metadata.underMaintenance && !yieldItem.metadata.deprecated && ( - - - - {translate('yieldXYZ.underMaintenanceDescription')} - - - )} - - - - {iconSource.assetId ? ( - - ) : ( - - )} - - {yieldItem.token.symbol} - - - {yieldItem.chainId && ( - - - - {yieldItem.network} - - + + + {assetIcon} + + + {assetIcon} + + {yieldTitle} + + + {yieldItem.metadata.deprecated && ( + + + + {translate('yieldXYZ.deprecatedDescription')} + + )} - {validatorOrProvider?.name && ( - - {validatorOrProvider.logoURI && ( - - )} - - {validatorOrProvider.name} - - + + {yieldItem.metadata.underMaintenance && !yieldItem.metadata.deprecated && ( + + + + {translate('yieldXYZ.underMaintenanceDescription')} + + )} - - - {apy}% {translate('common.apy')} - + + {yieldItem.chainId && ( + + + + {yieldItem.network} + + + )} + {validatorOrProvider?.name && ( + + {validatorOrProvider.logoURI && ( + + )} + + {validatorOrProvider.name} + + + )} + - {descriptionSection} + {hasPosition ? ( + <> + + + {translate('yieldXYZ.myPosition')} + + + + + + + {apy}% {translate('common.apy')} + + + + + + - - - - - - - - + + + + + + ) : ( + <> + + {apy}% {translate('common.apy')} + - - - {hasExitBalance && ( - + {!hasPosition && descriptionSection} + + {showAvailableToDeposit && ( + + + + + + {translate('yieldXYZ.availableToDeposit')} + + + + + + {potentialYearlyEarningsFiat.gt(0) && ( + + + {translate('yieldXYZ.potentialEarnings')} + + + + )} + + + )} + + + )} - - +
+ ) }, ) diff --git a/src/pages/Yields/components/YieldInfoCard.tsx b/src/pages/Yields/components/YieldInfoCard.tsx index 760c704f18e..d995a1dc041 100644 --- a/src/pages/Yields/components/YieldInfoCard.tsx +++ b/src/pages/Yields/components/YieldInfoCard.tsx @@ -10,12 +10,16 @@ import { Flex, Heading, HStack, + Stack, + Tag, + TagLeftIcon, Text, VStack, } from '@chakra-ui/react' -import { memo } from 'react' +import { memo, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { Amount } from '@/components/Amount/Amount' import { AssetIcon } from '@/components/AssetIcon' import { ChainIcon } from '@/components/ChainMenu' import { bnOrZero } from '@/lib/bignumber/bignumber' @@ -35,10 +39,18 @@ type YieldInfoCardProps = { yieldItem: AugmentedYieldDto validatorOrProvider: ValidatorOrProviderInfo titleOverride?: string + userBalanceUserCurrency?: string + userBalanceCrypto?: string } export const YieldInfoCard = memo( - ({ yieldItem, validatorOrProvider, titleOverride }: YieldInfoCardProps) => { + ({ + yieldItem, + validatorOrProvider, + titleOverride, + userBalanceUserCurrency, + userBalanceCrypto, + }: YieldInfoCardProps) => { const translate = useTranslate() const iconSource = resolveYieldInputAssetIcon(yieldItem) @@ -47,6 +59,8 @@ export const YieldInfoCard = memo( const type = yieldItem.mechanics.type const description = yieldItem.metadata.description + const hasPosition = useMemo(() => bnOrZero(userBalanceCrypto).gt(0), [userBalanceCrypto]) + const assetIcon = iconSource.assetId ? ( ) : ( @@ -55,40 +69,24 @@ export const YieldInfoCard = memo( const hasOverlay = validatorOrProvider?.logoURI || yieldItem.chainId - const stackedIconElement = !hasOverlay ? ( - assetIcon - ) : ( - - {assetIcon} - {validatorOrProvider?.logoURI ? ( - - ) : yieldItem.chainId ? ( - - - - ) : null} - - ) + const stackedIconElement = !hasOverlay ? assetIcon : {assetIcon} return ( - - + + + {assetIcon} + + {yieldItem.metadata.deprecated && ( @@ -110,74 +108,103 @@ export const YieldInfoCard = memo( {stackedIconElement} - - {yieldTitle} - + + + {yieldTitle} + + + {[yieldItem.network, validatorOrProvider?.name].filter(Boolean).join(' • ')} + + + + + {yieldItem.chainId && ( + + + + {yieldItem.network} + + + )} + {validatorOrProvider?.name && ( + + {validatorOrProvider.logoURI && ( + + )} + + {validatorOrProvider.name} + + + )} + + {type} + + - - - + {hasPosition ? ( + + + {translate('yieldXYZ.myPosition')} + + + + + {apy}% {translate('common.apy')} + + + + + + + ) : ( + + {apy}% {translate('common.apy')} - - - {type} - - - - - - {iconSource.assetId ? ( - - ) : ( - - )} - - {yieldItem.token.symbol} - - {yieldItem.chainId && ( - - - - {yieldItem.network} - - - )} - {validatorOrProvider?.name && ( - - {validatorOrProvider.logoURI && ( - - )} - - {validatorOrProvider.name} - - - )} - + )} - {description && ( + {description && !hasPosition && ( {description} diff --git a/src/pages/Yields/components/YieldPositionCard.tsx b/src/pages/Yields/components/YieldPositionCard.tsx index e468aae7977..aacd66c173f 100644 --- a/src/pages/Yields/components/YieldPositionCard.tsx +++ b/src/pages/Yields/components/YieldPositionCard.tsx @@ -14,7 +14,6 @@ import { Text, VStack, } from '@chakra-ui/react' -import { fromAccountId } from '@shapeshiftoss/caip' import dayjs from 'dayjs' import qs from 'qs' import { memo, useCallback, useMemo } from 'react' @@ -28,16 +27,13 @@ import { bnOrZero } from '@/lib/bignumber/bignumber' import type { AugmentedYieldDto } from '@/lib/yieldxyz/types' import { YieldBalanceType } from '@/lib/yieldxyz/types' import { getYieldActionLabelKeys } from '@/lib/yieldxyz/utils' +import { YieldAddMore } from '@/pages/Yields/components/YieldAddMore' import { useYieldAccount } from '@/pages/Yields/YieldAccountContext' import type { AggregatedBalance, NormalizedYieldBalances, } from '@/react-queries/queries/yieldxyz/useAllYieldBalances' -import { useYieldValidators } from '@/react-queries/queries/yieldxyz/useYieldValidators' -import { - selectAccountIdByAccountNumberAndChainId, - selectUserCurrencyToUsdRate, -} from '@/state/slices/selectors' +import { selectAccountIdByAccountNumberAndChainId } from '@/state/slices/selectors' import { useAppSelector } from '@/state/store' const enterIcon = @@ -55,6 +51,7 @@ type YieldPositionCardProps = { balances: NormalizedYieldBalances | undefined isBalancesLoading: boolean selectedValidatorAddress: string | undefined + inputTokenMarketData: { price?: string } | undefined } export const YieldPositionCard = memo( @@ -63,6 +60,7 @@ export const YieldPositionCard = memo( balances, isBalancesLoading, selectedValidatorAddress, + inputTokenMarketData, }: YieldPositionCardProps) => { const translate = useTranslate() const navigate = useNavigate() @@ -77,12 +75,6 @@ export const YieldPositionCard = memo( const accountIdsByNumberAndChain = selectAccountIdByAccountNumberAndChainId(state) return accountIdsByNumberAndChain[accountNumber]?.[chainId] }) - const userCurrencyToUsdRate = useAppSelector(selectUserCurrencyToUsdRate) - - const address = useMemo( - () => (accountId ? fromAccountId(accountId).account : undefined), - [accountId], - ) const balancesByType = useMemo(() => { if (!balances) return undefined @@ -137,20 +129,6 @@ export const YieldPositionCard = memo( claimableBalance && bnOrZero(claimableBalance.aggregatedAmount).gt(0), ) - const totalValueUsd = useMemo( - () => - [activeBalance, enteringBalance, exitingBalance, withdrawableBalance].reduce( - (sum, b) => sum.plus(bnOrZero(b?.aggregatedAmountUsd)), - bnOrZero(0), - ), - [activeBalance, enteringBalance, exitingBalance, withdrawableBalance], - ) - - const totalValueUserCurrency = useMemo( - () => totalValueUsd.times(userCurrencyToUsdRate).toFixed(), - [totalValueUsd, userCurrencyToUsdRate], - ) - const totalAmount = useMemo( () => [activeBalance, enteringBalance, exitingBalance, withdrawableBalance].reduce( @@ -162,26 +140,6 @@ export const YieldPositionCard = memo( const hasAnyPosition = totalAmount.gt(0) - const { data: validators } = useYieldValidators(yieldItem.id) - - const selectedValidatorName = useMemo(() => { - if (!selectedValidatorAddress) return undefined - const found = validators?.find(v => v.address === selectedValidatorAddress) - if (found) return found.name - const foundInBalances = balances?.raw.find( - b => b.validator?.address === selectedValidatorAddress, - ) - return foundInBalances?.validator?.name - }, [validators, selectedValidatorAddress, balances]) - - const headingText = selectedValidatorName - ? translate('yieldXYZ.myValidatorPosition', { validator: selectedValidatorName }) - : translate('yieldXYZ.myPosition') - - const addressBadgeText = address ? `${address.slice(0, 4)}...${address.slice(-4)}` : '' - - const totalAmountFixed = totalAmount.toFixed() - const navigateToAction = useCallback( (action: 'claim' | 'enter' | 'exit') => { navigate({ @@ -330,15 +288,6 @@ export const YieldPositionCard = memo( canClaim, ]) - const addressBadge = useMemo(() => { - if (!address) return null - return ( - - {addressBadgeText} - - ) - }, [address, addressBadgeText]) - const pendingActionsSection = useMemo(() => { if (!showPendingActions) return null return ( @@ -361,6 +310,7 @@ export const YieldPositionCard = memo( ]) if (!accountId) return null + if (!isBalancesLoading && !hasAnyPosition) return null if (isBalancesLoading) { return ( @@ -384,39 +334,12 @@ export const YieldPositionCard = memo( } return ( - + - - - {headingText} - - {addressBadge} - - - - {translate('yieldXYZ.totalValue')} - - - - - - - - {pendingActionsSection} - + - - )} + + + + + + {providerLogoURI && ( + + )} + + {translate('yieldXYZ.aboutProvider', { provider: providerName })} + + {providerWebsite && ( + + + + )} + + + {description} + + - - + + - - - {providerLogoURI && } - - {translate('yieldXYZ.aboutProvider', { provider: providerName })} - - - - {description} - - {providerWebsite && ( - - - - )} - + + + + {providerLogoURI && } + + {translate('yieldXYZ.aboutProvider', { provider: providerName })} + + + + {description} + + {providerWebsite && ( + + + + )} + + ) diff --git a/src/pages/Yields/components/YieldRelatedMarkets.tsx b/src/pages/Yields/components/YieldRelatedMarkets.tsx index a4fa1da5b94..6ad26e00507 100644 --- a/src/pages/Yields/components/YieldRelatedMarkets.tsx +++ b/src/pages/Yields/components/YieldRelatedMarkets.tsx @@ -1,9 +1,9 @@ -import { Box, Heading, SimpleGrid, useMediaQuery } from '@chakra-ui/react' +import { Box, Card, CardBody, Heading, VStack } from '@chakra-ui/react' import { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { useNavigate } from 'react-router-dom' -import { YieldItem } from '@/pages/Yields/components/YieldItem' +import { YieldCompareItem } from '@/pages/Yields/components/YieldCompareItem' import { useYieldProviders } from '@/react-queries/queries/yieldxyz/useYieldProviders' import { useYields } from '@/react-queries/queries/yieldxyz/useYields' @@ -16,7 +16,6 @@ export const YieldRelatedMarkets = memo( ({ currentYieldId, tokenSymbol }: YieldRelatedMarketsProps) => { const translate = useTranslate() const navigate = useNavigate() - const [isMobile] = useMediaQuery('(max-width: 768px)') const { data: yields } = useYields() const { data: yieldProviders } = useYieldProviders() @@ -43,7 +42,7 @@ export const YieldRelatedMarkets = memo( const getProviderInfo = useCallback( (providerId: string) => { const provider = yieldProviders?.[providerId] - return { name: provider?.name, logo: provider?.logoURI } + return { name: provider?.name, icon: provider?.logoURI } }, [yieldProviders], ) @@ -55,24 +54,24 @@ export const YieldRelatedMarkets = memo( {translate('yieldXYZ.otherYields', { symbol: tokenSymbol })} - - {relatedYields.map(y => { - const providerInfo = getProviderInfo(y.providerId) - return ( - handleYieldClick(y.id)} - /> - ) - })} - + + + + {relatedYields.map(y => { + const providerInfo = getProviderInfo(y.providerId) + return ( + handleYieldClick(y.id)} + /> + ) + })} + + + ) }, diff --git a/src/pages/Yields/components/YieldStats.tsx b/src/pages/Yields/components/YieldStats.tsx index ff6693763b5..71687103c92 100644 --- a/src/pages/Yields/components/YieldStats.tsx +++ b/src/pages/Yields/components/YieldStats.tsx @@ -1,9 +1,19 @@ -import { Avatar, Box, Flex, SimpleGrid, Text } from '@chakra-ui/react' +import { + Avatar, + Card, + CardBody, + Flex, + SimpleGrid, + Stat, + StatLabel, + StatNumber, +} from '@chakra-ui/react' import { memo, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { useSearchParams } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' +import { Row } from '@/components/Row/Row' import { bnOrZero } from '@/lib/bignumber/bignumber' import { COSMOS_ATOM_NATIVE_STAKING_YIELD_ID, @@ -23,9 +33,10 @@ import { useAppSelector } from '@/state/store' type YieldStatsProps = { yieldItem: AugmentedYieldDto balances?: NormalizedYieldBalances + variant?: 'list' | 'card' } -export const YieldStats = memo(({ yieldItem, balances }: YieldStatsProps) => { +export const YieldStats = memo(({ yieldItem, balances, variant = 'card' }: YieldStatsProps) => { const translate = useTranslate() const userCurrencyToUsdRate = useAppSelector(selectUserCurrencyToUsdRate) const inputTokenAssetId = yieldItem.inputTokens[0]?.assetId ?? '' @@ -57,7 +68,7 @@ export const YieldStats = memo(({ yieldItem, balances }: YieldStatsProps) => { return defaultValidator } return validatorParam || defaultValidator - }, [yieldItem.id, validatorParam, defaultValidator]) + }, [yieldItem.id, defaultValidator, validatorParam]) const selectedValidator = useMemo(() => { if (!selectedValidatorAddress) return undefined @@ -69,11 +80,13 @@ export const YieldStats = memo(({ yieldItem, balances }: YieldStatsProps) => { return undefined }, [selectedValidatorAddress, validators, balances?.raw]) + const validatorTvl = useMemo(() => { + return selectedValidator && 'tvl' in selectedValidator ? selectedValidator.tvl : undefined + }, [selectedValidator]) + const tvl = useMemo(() => { - const validatorTvl = - selectedValidator && 'tvl' in selectedValidator ? selectedValidator.tvl : undefined return bnOrZero(yieldItem.statistics?.tvl ?? validatorTvl).toNumber() - }, [selectedValidator, yieldItem.statistics?.tvl]) + }, [yieldItem.statistics?.tvl, validatorTvl]) const tvlUserCurrency = useMemo( () => @@ -93,47 +106,97 @@ export const YieldStats = memo(({ yieldItem, balances }: YieldStatsProps) => { [isStaking, selectedValidator], ) + if (variant === 'list') { + return ( + + + + {translate('yieldXYZ.tvl')} + + + + + + {translate('yieldXYZ.rewardSchedule')} + {yieldItem.mechanics.rewardSchedule} + + + {translate('yieldXYZ.type')} + {yieldItem.mechanics.type} + + {validatorMetadata && ( + + {translate('yieldXYZ.validator')} + {validatorMetadata?.name} + + )} + + + ) + } + return ( - - - - {translate('yieldXYZ.tvl')} - - - - - - - {translate('yieldXYZ.rewardSchedule')} - - - {yieldItem.mechanics.rewardSchedule} - - - - - - {translate('yieldXYZ.type')} - - - {yieldItem.mechanics.type} - - + + + + + + {translate('yieldXYZ.tvl')} + + + + + + + + + + + + + {translate('yieldXYZ.rewardSchedule')} + + + {yieldItem.mechanics.rewardSchedule} + + + + + + + + + {translate('yieldXYZ.type')} + + + {yieldItem.mechanics.type} + + + + {validatorMetadata && ( - - - {translate('yieldXYZ.validator')} - - - {validatorMetadata.logoURI && ( - - )} - - {validatorMetadata.name} - - - + + + + + {translate('yieldXYZ.validator')} + + + + {validatorMetadata.logoURI && ( + + )} + {validatorMetadata.name} + + + + + )} ) diff --git a/src/pages/Yields/components/YieldsList.tsx b/src/pages/Yields/components/YieldsList.tsx index 16f5a947c20..a77994bb096 100644 --- a/src/pages/Yields/components/YieldsList.tsx +++ b/src/pages/Yields/components/YieldsList.tsx @@ -2,7 +2,6 @@ import { SearchIcon } from '@chakra-ui/icons' import { Avatar, Box, - Container, Flex, Heading, HStack, @@ -29,6 +28,9 @@ import { useNavigate, useSearchParams } from 'react-router-dom' import { Amount } from '@/components/Amount/Amount' import { AssetIcon } from '@/components/AssetIcon' import { ChainIcon } from '@/components/ChainMenu' +import { Display } from '@/components/Display' +import { PageHeader } from '@/components/Layout/Header/PageHeader' +import { Main } from '@/components/Layout/Main' import { ResultsEmptyNoWallet } from '@/components/ResultsEmptyNoWallet' import { useWallet } from '@/hooks/useWallet/useWallet' import { bnOrZero } from '@/lib/bignumber/bignumber' @@ -387,9 +389,11 @@ export const YieldsList = memo(() => { ]) const recommendedYields = useMemo(() => { - if (!unfilteredAvailableYields.length || !userCurrencyBalances) return [] + if (!isConnected || !yields?.unfiltered || !userCurrencyBalances || !assetBalancesBaseUnit) + return [] - const yieldsByAssetId = unfilteredAvailableYields.reduce>( + // Build unfiltered byInputAssetId lookup so recommendations are independent of filters + const allYieldsByInputAssetId = yields.unfiltered.reduce>( (acc, item) => { const assetId = item.inputTokens?.[0]?.assetId if (assetId) { @@ -407,8 +411,11 @@ export const YieldsList = memo(() => { potentialEarnings: ReturnType }[] = [] - for (const [assetId, yieldsForAsset] of Object.entries(yieldsByAssetId)) { - const balance = bnOrZero(userCurrencyBalances[assetId]) + for (const [assetId, balanceFiat] of Object.entries(userCurrencyBalances)) { + const yieldsForAsset = allYieldsByInputAssetId[assetId] + if (!yieldsForAsset?.length) continue + + const balance = bnOrZero(balanceFiat) if (balance.lte(0)) continue const bestYield = yieldsForAsset.reduce((best, current) => @@ -425,7 +432,7 @@ export const YieldsList = memo(() => { return recommendations .sort((a, b) => b.potentialEarnings.minus(a.potentialEarnings).toNumber()) .slice(0, 3) - }, [unfilteredAvailableYields, userCurrencyBalances]) + }, [isConnected, yields?.unfiltered, userCurrencyBalances, assetBalancesBaseUnit]) const availableYields = useMemo(() => { if (!unfilteredAvailableYields.length || !userCurrencyBalances) return [] @@ -1141,102 +1148,115 @@ export const YieldsList = memo(() => { isMobile, ]) + const headerComponent = useMemo( + () => ( + + + + {translate('yieldXYZ.pageTitle')} + + {null} + + + + + + {translate('yieldXYZ.pageTitle')} + + {translate('yieldXYZ.pageSubtitle')} + + + + + ), + [translate], + ) + + const containerPaddingX = useMemo(() => ({ base: 4, xl: 0 }), []) + return ( - - - - - - {translate('yieldXYZ.pageTitle')} - - {!isMobile && {translate('yieldXYZ.pageSubtitle')}} - - - - {errorElement} - {isConnected && ( - - )} - {recommendedStripElement} - - - {translate('common.all')} - {translate('yieldXYZ.availableToEarn')} - - {translate('yieldXYZ.myPositions')} ({myPositions.length}) - - - + + {errorElement} + {isConnected && ( + + )} + {recommendedStripElement} + - - - - - - + + {translate('common.all')} + {translate('yieldXYZ.availableToEarn')} + + {translate('yieldXYZ.myPositions')} ({myPositions.length}) + + - {!isMobile && ( - <> - - - - )} + + + + + + + + {!isMobile && ( + <> + + + + )} + - - - {allYieldsContentElement} - {availableToEarnContentElement} - {positionsContentElement} - - - + + {allYieldsContentElement} + {availableToEarnContentElement} + {positionsContentElement} + + + + ) })