From fe44c45991d1a1de47ac81e0ee5e7fa511b1ea83 Mon Sep 17 00:00:00 2001 From: Maxime Julian <44675210+therealemjy@users.noreply.github.com> Date: Tue, 26 Sep 2023 17:28:05 +0200 Subject: [PATCH] feat: add Prime status banner (#1464) --- src/assets/img/primeLogo.svg | 11 + src/components/ProgressBar/styles.ts | 3 +- src/containers/PrimeStatusBanner/index.tsx | 313 ++++++++++++++++++ src/containers/PrimeStatusBanner/styles.ts | 172 ++++++++++ .../useFormatPercentageToReadableValue.ts | 12 + .../AccountPlaceholder/index.tsx | 0 .../AccountPlaceholder/styles.ts | 0 .../AccountPlaceholder/wallet.png | Bin .../PoolsBreakdown/Tables/index.tsx | 0 .../PoolsBreakdown/Tables/styles.ts | 0 .../__snapshots__/index.spec.tsx.snap | 0 .../PoolsBreakdown/index.spec.tsx | 0 .../PoolsBreakdown/index.stories.tsx | 0 .../PoolsBreakdown/index.tsx | 0 .../PoolsBreakdown/styles.ts | 0 .../PoolsBreakdown/testIds.ts | 0 .../{ => AccountBreakdown}/Section/index.tsx | 0 .../{ => AccountBreakdown}/Section/styles.ts | 0 .../Summary/__snapshots__/index.spec.tsx.snap | 0 .../Summary/calculateNetApy/index.spec.ts | 0 .../Summary/calculateNetApy/index.ts | 0 .../Summary/index.spec.tsx | 0 .../{ => AccountBreakdown}/Summary/index.tsx | 93 +++--- .../{ => AccountBreakdown}/Summary/styles.ts | 0 .../{ => AccountBreakdown}/Summary/testIds.ts | 0 .../Summary/useExtractData.ts | 0 .../VaultsBreakdown/Table/index.tsx | 0 .../VaultsBreakdown/Table/styles.ts | 0 .../__snapshots__/index.spec.tsx.snap | 0 .../VaultsBreakdown/index.spec.tsx | 0 .../VaultsBreakdown/index.stories.tsx | 0 .../VaultsBreakdown/index.tsx | 0 .../{ => AccountBreakdown}/index.spec.tsx | 0 .../{ => AccountBreakdown}/index.stories.tsx | 0 src/pages/Account/AccountBreakdown/index.tsx | 122 +++++++ src/pages/Account/index.tsx | 114 +------ src/pages/Account/styles.ts | 27 -- .../UploadOrManualProposal/index.tsx | 1 + src/translation/translations/en.json | 41 ++- src/utilities/isFeatureEnabled.ts | 2 +- 40 files changed, 721 insertions(+), 190 deletions(-) create mode 100644 src/assets/img/primeLogo.svg create mode 100644 src/containers/PrimeStatusBanner/index.tsx create mode 100644 src/containers/PrimeStatusBanner/styles.ts create mode 100644 src/hooks/useFormatPercentageToReadableValue.ts rename src/pages/Account/{ => AccountBreakdown}/AccountPlaceholder/index.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/AccountPlaceholder/styles.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/AccountPlaceholder/wallet.png (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/Tables/index.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/Tables/styles.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/__snapshots__/index.spec.tsx.snap (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/index.spec.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/index.stories.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/index.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/styles.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/PoolsBreakdown/testIds.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/Section/index.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/Section/styles.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/__snapshots__/index.spec.tsx.snap (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/calculateNetApy/index.spec.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/calculateNetApy/index.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/index.spec.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/index.tsx (51%) rename src/pages/Account/{ => AccountBreakdown}/Summary/styles.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/testIds.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/Summary/useExtractData.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/VaultsBreakdown/Table/index.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/VaultsBreakdown/Table/styles.ts (100%) rename src/pages/Account/{ => AccountBreakdown}/VaultsBreakdown/__snapshots__/index.spec.tsx.snap (100%) rename src/pages/Account/{ => AccountBreakdown}/VaultsBreakdown/index.spec.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/VaultsBreakdown/index.stories.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/VaultsBreakdown/index.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/index.spec.tsx (100%) rename src/pages/Account/{ => AccountBreakdown}/index.stories.tsx (100%) create mode 100644 src/pages/Account/AccountBreakdown/index.tsx diff --git a/src/assets/img/primeLogo.svg b/src/assets/img/primeLogo.svg new file mode 100644 index 0000000000..c9fef17576 --- /dev/null +++ b/src/assets/img/primeLogo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/ProgressBar/styles.ts b/src/components/ProgressBar/styles.ts index 4603a54cc8..1c76a4ae7e 100644 --- a/src/components/ProgressBar/styles.ts +++ b/src/components/ProgressBar/styles.ts @@ -29,7 +29,8 @@ export const useStyles = ({ } .MuiSlider-rail { height: ${theme.spacing(2)}; - color: ${theme.palette.background.default}; + color: ${theme.palette.secondary.light}; + opacity: 1; } `, trackWrapper: css` diff --git a/src/containers/PrimeStatusBanner/index.tsx b/src/containers/PrimeStatusBanner/index.tsx new file mode 100644 index 0000000000..6c8d2f6e42 --- /dev/null +++ b/src/containers/PrimeStatusBanner/index.tsx @@ -0,0 +1,313 @@ +/** @jsxImportSource @emotion/react */ +import { Typography } from '@mui/material'; +import Paper from '@mui/material/Paper'; +import BigNumber from 'bignumber.js'; +import formatDistanceStrict from 'date-fns/formatDistanceStrict'; +import { ContractReceipt } from 'ethers'; +import React, { useMemo } from 'react'; +import { useNavigate } from 'react-router'; +import { useTranslation } from 'translation'; +import { Token } from 'types'; + +import fakeContractReceipt from '__mocks__/models/contractReceipt'; +import { ReactComponent as PrimeLogo } from 'assets/img/primeLogo.svg'; +import { PrimaryButton } from 'components/Button'; +import { Icon } from 'components/Icon'; +import { ProgressBar } from 'components/ProgressBar'; +import { Tooltip } from 'components/Tooltip'; +import { routes } from 'constants/routing'; +import useFormatPercentageToReadableValue from 'hooks/useFormatPercentageToReadableValue'; +import useConvertWeiToReadableTokenString from 'hooks/useFormatTokensToReadableValue'; +import useGetToken from 'hooks/useGetToken'; +import useHandleTransactionMutation from 'hooks/useHandleTransactionMutation'; + +import { useStyles } from './styles'; + +export interface PrimeStatusBannerUiProps { + xvs: Token; + claimedPrimeTokenCount: number; + primeTokenLimit: number; + isClaimPrimeTokenLoading: boolean; + onClaimPrimeToken: () => Promise; + onRedirectToXvsVaultPage: () => void; + userStakedXvsTokens: BigNumber; + minXvsToStakeForPrimeTokens: BigNumber; + highestHypotheticalPrimeApyBoostPercentage: BigNumber; + primeClaimWaitingPeriodSeconds: number; + hidePromotionalTitle?: boolean; + className?: string; +} + +export const PrimeStatusBannerUi: React.FC = ({ + className, + xvs, + claimedPrimeTokenCount, + primeTokenLimit, + isClaimPrimeTokenLoading, + highestHypotheticalPrimeApyBoostPercentage, + primeClaimWaitingPeriodSeconds, + minXvsToStakeForPrimeTokens, + userStakedXvsTokens, + hidePromotionalTitle = false, + onClaimPrimeToken, + onRedirectToXvsVaultPage, +}) => { + const styles = useStyles(); + const { Trans, t } = useTranslation(); + const handleTransactionMutation = useHandleTransactionMutation(); + + const handleClaimPrimeToken = () => + handleTransactionMutation({ + mutate: onClaimPrimeToken, + successTransactionModalProps: contractReceipt => ({ + title: t('primeStatusBanner.successfulTransactionModal.title'), + content: t('primeStatusBanner.successfulTransactionModal.message'), + transactionHash: contractReceipt.transactionHash, + }), + }); + + const stakeDeltaTokens = useMemo( + () => minXvsToStakeForPrimeTokens.minus(userStakedXvsTokens), + [minXvsToStakeForPrimeTokens, userStakedXvsTokens], + ); + const isUserXvsStakeHighEnoughForPrime = !!stakeDeltaTokens?.isEqualTo(0); + + const haveAllPrimeTokensBeenClaimed = useMemo( + () => claimedPrimeTokenCount >= primeTokenLimit, + [primeTokenLimit, claimedPrimeTokenCount], + ); + + const readableStakeDeltaTokens = useConvertWeiToReadableTokenString({ + value: stakeDeltaTokens, + token: xvs, + }); + + const readableApyBoostPercentage = useFormatPercentageToReadableValue({ + value: highestHypotheticalPrimeApyBoostPercentage, + }); + + const readableClaimWaitingPeriod = useMemo( + () => + formatDistanceStrict( + new Date(), + new Date().getTime() + primeClaimWaitingPeriodSeconds * 1000, + ), + [primeClaimWaitingPeriodSeconds], + ); + + const readableMinXvsToStakeForPrimeTokens = useConvertWeiToReadableTokenString({ + value: minXvsToStakeForPrimeTokens, + token: xvs, + }); + + const readableUserStakedXvsTokens = useConvertWeiToReadableTokenString({ + value: userStakedXvsTokens, + token: xvs, + }); + + const title = useMemo(() => { + if (isUserXvsStakeHighEnoughForPrime && primeClaimWaitingPeriodSeconds > 0) { + return t('primeStatusBanner.waitForPrimeTitle', { + claimWaitingPeriod: readableClaimWaitingPeriod, + }); + } + + if (isUserXvsStakeHighEnoughForPrime && primeClaimWaitingPeriodSeconds === 0) { + return t('primeStatusBanner.becomePrimeTitle'); + } + + if (!hidePromotionalTitle) { + return ( + , + }} + values={{ + percentage: readableApyBoostPercentage, + }} + /> + ); + } + }, [hidePromotionalTitle, readableApyBoostPercentage, isUserXvsStakeHighEnoughForPrime]); + + const ctaButton = useMemo(() => { + if (haveAllPrimeTokensBeenClaimed) { + return undefined; + } + + if (isUserXvsStakeHighEnoughForPrime) { + return ( + + {t('primeStatusBanner.claimButtonLabel')} + + ); + } + + return ( + + {t('primeStatusBanner.stakeButtonLabel')} + + ); + }, [isUserXvsStakeHighEnoughForPrime, haveAllPrimeTokensBeenClaimed]); + + return ( + +
+
+
+ +
+ +
+ {!!title && ( + + {title} + + )} + + {!isUserXvsStakeHighEnoughForPrime && ( + + , + Link: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ), + }} + values={{ + stakeDelta: readableStakeDeltaTokens, + claimWaitingPeriod: readableClaimWaitingPeriod, + }} + /> + + )} +
+
+ + {!isUserXvsStakeHighEnoughForPrime && ( +
+ + + + , + }} + values={{ + minXvsToStakeForPrimeTokens: readableMinXvsToStakeForPrimeTokens, + userStakedXvsTokens: readableUserStakedXvsTokens, + }} + /> + +
+ )} +
+ +
+ {haveAllPrimeTokensBeenClaimed ? ( +
+ + {t('primeStatusBanner.noPrimeTokenWarning.text')} + + + + + +
+ ) : ( + ctaButton + )} +
+
+ ); +}; + +export type PrimeStatusBannerProps = Pick< + PrimeStatusBannerUiProps, + 'className' | 'hidePromotionalTitle' +>; + +const PrimeStatusBanner: React.FC = props => { + const navigate = useNavigate(); + const redirectToXvsPage = () => navigate(routes.vaults.path); + + const xvs = useGetToken({ + symbol: 'XVS', + }); + + // TODO: wire up + const isLoading = false; + const isUserPrime = false; + const primeClaimWaitingPeriodSeconds = 90 * 24 * 60 * 60; // 9 days in seconds + const userStakedXvsTokens = new BigNumber('100'); + const minXvsToStakeForPrimeTokens = new BigNumber('1000'); + const highestHypotheticalPrimeApyBoostPercentage = new BigNumber('3.14'); + const claimedPrimeTokenCount = 1000; + const primeTokenLimit = 1000; + + const claimPrimeToken = async () => fakeContractReceipt; + const isClaimPrimeTokenLoading = false; + + // Hide component while loading or if user is Prime already + if (isLoading || isUserPrime) { + return null; + } + + return ( + + ); +}; + +export default PrimeStatusBanner; diff --git a/src/containers/PrimeStatusBanner/styles.ts b/src/containers/PrimeStatusBanner/styles.ts new file mode 100644 index 0000000000..774cae401b --- /dev/null +++ b/src/containers/PrimeStatusBanner/styles.ts @@ -0,0 +1,172 @@ +import { css } from '@emotion/react'; +import { useTheme } from '@mui/material'; + +export const useStyles = () => { + const theme = useTheme(); + + return { + getContainer: ({ isProgressDisplayed }: { isProgressDisplayed: boolean }) => css` + display: flex; + justify-content: space-between; + + ${!isProgressDisplayed && + css` + align-items: center; + `} + + ${theme.breakpoints.down('md')} { + flex-direction: column; + padding: ${theme.spacing(4)}; + + ${!isProgressDisplayed && + css` + align-items: flex-start; + `} + } + `, + getContentColumn: ({ isWarningDisplayed }: { isWarningDisplayed: boolean }) => css` + :not(:last-child) { + margin-right: ${theme.spacing(6)}; + } + + ${theme.breakpoints.down('md')} { + ${isWarningDisplayed + ? css` + order: 2; + ` + : css` + margin-bottom: ${theme.spacing(6)}; + `} + + :not(:last-child) { + margin-right: 0; + } + } + + ${theme.breakpoints.down('sm')} { + width: 100%; + } + `, + getCtaColumn: ({ + isWarningDisplayed, + isTitleDisplayed, + }: { + isWarningDisplayed: boolean; + isTitleDisplayed: boolean; + }) => css` + ${theme.breakpoints.down('md')} { + ${isWarningDisplayed && + css` + order: 1; + `} + ${!isWarningDisplayed && + isTitleDisplayed && + css` + padding-left: ${theme.spacing(14)}; + `} + } + + ${theme.breakpoints.down('sm')} { + padding-left: 0; + width: 100%; + } + `, + getHeader: ({ isProgressDisplayed }: { isProgressDisplayed: boolean }) => css` + display: flex; + + ${isProgressDisplayed + ? css` + margin-bottom: ${theme.spacing(6)}; + ` + : css` + align-items: center; + `}; + + ${theme.breakpoints.down('sm')} { + flex-direction: column; + + ${!isProgressDisplayed && + css` + align-items: flex-start; + `} + } + `, + getPrimeLogo: ({ isProgressDisplayed }: { isProgressDisplayed: boolean }) => css` + display: inline-flex; + width: ${theme.spacing(10)}; + margin-right: ${theme.spacing(4)}; + + ${!isProgressDisplayed && + css` + margin-top: ${theme.spacing(1)}; + `}; + + ${theme.breakpoints.down('sm')} { + margin-top: ${theme.spacing(0)}; + margin-bottom: ${theme.spacing(4)}; + } + `, + getTitle: ({ isDescriptionDisplayed }: { isDescriptionDisplayed: boolean }) => css` + ${isDescriptionDisplayed && + css` + margin-bottom: ${theme.spacing(2)}; + `}; + `, + greenText: css` + color: ${theme.palette.interactive.success}; + `, + whiteText: css` + color: ${theme.palette.text.primary}; + `, + button: css` + white-space: nowrap; + + ${theme.breakpoints.down('sm')} { + width: 100%; + } + `, + getProgress: ({ addLeftPadding }: { addLeftPadding: boolean }) => css` + ${addLeftPadding && + css` + max-width: ${theme.spacing(125)}; + padding-left: ${theme.spacing(14)}; + + ${theme.breakpoints.down('md')} { + max-width: none; + } + + ${theme.breakpoints.down('sm')} { + padding-left: 0; + } + `} + `, + progressBar: css` + margin-bottom: ${theme.spacing(2)}; + `, + getNoPrimeTokenWarning: ({ isProgressDisplayed }: { isProgressDisplayed: boolean }) => css` + display: flex; + align-items: center; + text-align: right; + + ${isProgressDisplayed && + css` + margin-top: ${theme.spacing(2)}; + `}; + + ${theme.breakpoints.down('md')} { + margin-top: 0; + margin-bottom: ${theme.spacing(4)}; + } + `, + warningText: css` + color: ${theme.palette.interactive.warning}; + margin-right: ${theme.spacing(2)}; + `, + tooltip: css` + display: inline-flex; + `, + tooltipIcon: css` + color: ${theme.palette.interactive.warning}; + `, + }; +}; diff --git a/src/hooks/useFormatPercentageToReadableValue.ts b/src/hooks/useFormatPercentageToReadableValue.ts new file mode 100644 index 0000000000..cc6e8fea2a --- /dev/null +++ b/src/hooks/useFormatPercentageToReadableValue.ts @@ -0,0 +1,12 @@ +import BigNumber from 'bignumber.js'; +import { useMemo } from 'react'; +import { formatPercentageToReadableValue } from 'utilities'; + +export interface UseFormatPercentageToReadableValueInput { + value: number | string | BigNumber | undefined; +} + +const useFormatPercentageToReadableValue = (params: UseFormatPercentageToReadableValueInput) => + useMemo(() => formatPercentageToReadableValue(params.value), [params.value]); + +export default useFormatPercentageToReadableValue; diff --git a/src/pages/Account/AccountPlaceholder/index.tsx b/src/pages/Account/AccountBreakdown/AccountPlaceholder/index.tsx similarity index 100% rename from src/pages/Account/AccountPlaceholder/index.tsx rename to src/pages/Account/AccountBreakdown/AccountPlaceholder/index.tsx diff --git a/src/pages/Account/AccountPlaceholder/styles.ts b/src/pages/Account/AccountBreakdown/AccountPlaceholder/styles.ts similarity index 100% rename from src/pages/Account/AccountPlaceholder/styles.ts rename to src/pages/Account/AccountBreakdown/AccountPlaceholder/styles.ts diff --git a/src/pages/Account/AccountPlaceholder/wallet.png b/src/pages/Account/AccountBreakdown/AccountPlaceholder/wallet.png similarity index 100% rename from src/pages/Account/AccountPlaceholder/wallet.png rename to src/pages/Account/AccountBreakdown/AccountPlaceholder/wallet.png diff --git a/src/pages/Account/PoolsBreakdown/Tables/index.tsx b/src/pages/Account/AccountBreakdown/PoolsBreakdown/Tables/index.tsx similarity index 100% rename from src/pages/Account/PoolsBreakdown/Tables/index.tsx rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/Tables/index.tsx diff --git a/src/pages/Account/PoolsBreakdown/Tables/styles.ts b/src/pages/Account/AccountBreakdown/PoolsBreakdown/Tables/styles.ts similarity index 100% rename from src/pages/Account/PoolsBreakdown/Tables/styles.ts rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/Tables/styles.ts diff --git a/src/pages/Account/PoolsBreakdown/__snapshots__/index.spec.tsx.snap b/src/pages/Account/AccountBreakdown/PoolsBreakdown/__snapshots__/index.spec.tsx.snap similarity index 100% rename from src/pages/Account/PoolsBreakdown/__snapshots__/index.spec.tsx.snap rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/__snapshots__/index.spec.tsx.snap diff --git a/src/pages/Account/PoolsBreakdown/index.spec.tsx b/src/pages/Account/AccountBreakdown/PoolsBreakdown/index.spec.tsx similarity index 100% rename from src/pages/Account/PoolsBreakdown/index.spec.tsx rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/index.spec.tsx diff --git a/src/pages/Account/PoolsBreakdown/index.stories.tsx b/src/pages/Account/AccountBreakdown/PoolsBreakdown/index.stories.tsx similarity index 100% rename from src/pages/Account/PoolsBreakdown/index.stories.tsx rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/index.stories.tsx diff --git a/src/pages/Account/PoolsBreakdown/index.tsx b/src/pages/Account/AccountBreakdown/PoolsBreakdown/index.tsx similarity index 100% rename from src/pages/Account/PoolsBreakdown/index.tsx rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/index.tsx diff --git a/src/pages/Account/PoolsBreakdown/styles.ts b/src/pages/Account/AccountBreakdown/PoolsBreakdown/styles.ts similarity index 100% rename from src/pages/Account/PoolsBreakdown/styles.ts rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/styles.ts diff --git a/src/pages/Account/PoolsBreakdown/testIds.ts b/src/pages/Account/AccountBreakdown/PoolsBreakdown/testIds.ts similarity index 100% rename from src/pages/Account/PoolsBreakdown/testIds.ts rename to src/pages/Account/AccountBreakdown/PoolsBreakdown/testIds.ts diff --git a/src/pages/Account/Section/index.tsx b/src/pages/Account/AccountBreakdown/Section/index.tsx similarity index 100% rename from src/pages/Account/Section/index.tsx rename to src/pages/Account/AccountBreakdown/Section/index.tsx diff --git a/src/pages/Account/Section/styles.ts b/src/pages/Account/AccountBreakdown/Section/styles.ts similarity index 100% rename from src/pages/Account/Section/styles.ts rename to src/pages/Account/AccountBreakdown/Section/styles.ts diff --git a/src/pages/Account/Summary/__snapshots__/index.spec.tsx.snap b/src/pages/Account/AccountBreakdown/Summary/__snapshots__/index.spec.tsx.snap similarity index 100% rename from src/pages/Account/Summary/__snapshots__/index.spec.tsx.snap rename to src/pages/Account/AccountBreakdown/Summary/__snapshots__/index.spec.tsx.snap diff --git a/src/pages/Account/Summary/calculateNetApy/index.spec.ts b/src/pages/Account/AccountBreakdown/Summary/calculateNetApy/index.spec.ts similarity index 100% rename from src/pages/Account/Summary/calculateNetApy/index.spec.ts rename to src/pages/Account/AccountBreakdown/Summary/calculateNetApy/index.spec.ts diff --git a/src/pages/Account/Summary/calculateNetApy/index.ts b/src/pages/Account/AccountBreakdown/Summary/calculateNetApy/index.ts similarity index 100% rename from src/pages/Account/Summary/calculateNetApy/index.ts rename to src/pages/Account/AccountBreakdown/Summary/calculateNetApy/index.ts diff --git a/src/pages/Account/Summary/index.spec.tsx b/src/pages/Account/AccountBreakdown/Summary/index.spec.tsx similarity index 100% rename from src/pages/Account/Summary/index.spec.tsx rename to src/pages/Account/AccountBreakdown/Summary/index.spec.tsx diff --git a/src/pages/Account/Summary/index.tsx b/src/pages/Account/AccountBreakdown/Summary/index.tsx similarity index 51% rename from src/pages/Account/Summary/index.tsx rename to src/pages/Account/AccountBreakdown/Summary/index.tsx index c4ac13b4b3..ad38241b3a 100644 --- a/src/pages/Account/Summary/index.tsx +++ b/src/pages/Account/AccountBreakdown/Summary/index.tsx @@ -9,6 +9,7 @@ import { formatCentsToReadableValue, formatPercentageToReadableValue } from 'uti import { SAFE_BORROW_LIMIT_PERCENTAGE } from 'constants/safeBorrowLimitPercentage'; +import Section from '../Section'; import { useStyles } from './styles'; import TEST_IDS from './testIds'; import useExtractData from './useExtractData'; @@ -53,79 +54,81 @@ export const Summary: React.FC = ({ const cells: Cell[] = [ { - label: t('account.marketBreakdown.cellGroup.netApy'), + label: t('account.summary.cellGroup.netApy'), value: formatPercentageToReadableValue(netApyPercentage), tooltip: displayTotalVaultStake - ? t('account.marketBreakdown.cellGroup.netApyWithVaultStakeTooltip') - : t('account.marketBreakdown.cellGroup.netApyTooltip'), + ? t('account.summary.cellGroup.netApyWithVaultStakeTooltip') + : t('account.summary.cellGroup.netApyTooltip'), color: styles.getNetApyColor({ netApyPercentage: netApyPercentage || 0 }), }, { - label: t('account.marketBreakdown.cellGroup.dailyEarnings'), + label: t('account.summary.cellGroup.dailyEarnings'), value: formatCentsToReadableValue({ value: dailyEarningsCents }), }, { - label: t('account.marketBreakdown.cellGroup.totalSupply'), + label: t('account.summary.cellGroup.totalSupply'), value: formatCentsToReadableValue({ value: totalSupplyCents }), }, { - label: t('account.marketBreakdown.cellGroup.totalBorrow'), + label: t('account.summary.cellGroup.totalBorrow'), value: formatCentsToReadableValue({ value: totalBorrowCents }), }, ]; if (displayTotalVaultStake) { cells.push({ - label: t('account.marketBreakdown.cellGroup.totalVaultStake'), + label: t('account.summary.cellGroup.totalVaultStake'), value: formatCentsToReadableValue({ value: totalVaultStakeCents }), }); } return ( - - +
+ + - {displayAccountHealth && ( -
- + {displayAccountHealth && ( +
+ -
- +
+ - - {t('myAccount.safeLimit')} - + + {t('myAccount.safeLimit')} + - - {readableSafeBorrowLimit} - + + {readableSafeBorrowLimit} + - - - + + + +
-
- )} - + )} + +
); }; diff --git a/src/pages/Account/Summary/styles.ts b/src/pages/Account/AccountBreakdown/Summary/styles.ts similarity index 100% rename from src/pages/Account/Summary/styles.ts rename to src/pages/Account/AccountBreakdown/Summary/styles.ts diff --git a/src/pages/Account/Summary/testIds.ts b/src/pages/Account/AccountBreakdown/Summary/testIds.ts similarity index 100% rename from src/pages/Account/Summary/testIds.ts rename to src/pages/Account/AccountBreakdown/Summary/testIds.ts diff --git a/src/pages/Account/Summary/useExtractData.ts b/src/pages/Account/AccountBreakdown/Summary/useExtractData.ts similarity index 100% rename from src/pages/Account/Summary/useExtractData.ts rename to src/pages/Account/AccountBreakdown/Summary/useExtractData.ts diff --git a/src/pages/Account/VaultsBreakdown/Table/index.tsx b/src/pages/Account/AccountBreakdown/VaultsBreakdown/Table/index.tsx similarity index 100% rename from src/pages/Account/VaultsBreakdown/Table/index.tsx rename to src/pages/Account/AccountBreakdown/VaultsBreakdown/Table/index.tsx diff --git a/src/pages/Account/VaultsBreakdown/Table/styles.ts b/src/pages/Account/AccountBreakdown/VaultsBreakdown/Table/styles.ts similarity index 100% rename from src/pages/Account/VaultsBreakdown/Table/styles.ts rename to src/pages/Account/AccountBreakdown/VaultsBreakdown/Table/styles.ts diff --git a/src/pages/Account/VaultsBreakdown/__snapshots__/index.spec.tsx.snap b/src/pages/Account/AccountBreakdown/VaultsBreakdown/__snapshots__/index.spec.tsx.snap similarity index 100% rename from src/pages/Account/VaultsBreakdown/__snapshots__/index.spec.tsx.snap rename to src/pages/Account/AccountBreakdown/VaultsBreakdown/__snapshots__/index.spec.tsx.snap diff --git a/src/pages/Account/VaultsBreakdown/index.spec.tsx b/src/pages/Account/AccountBreakdown/VaultsBreakdown/index.spec.tsx similarity index 100% rename from src/pages/Account/VaultsBreakdown/index.spec.tsx rename to src/pages/Account/AccountBreakdown/VaultsBreakdown/index.spec.tsx diff --git a/src/pages/Account/VaultsBreakdown/index.stories.tsx b/src/pages/Account/AccountBreakdown/VaultsBreakdown/index.stories.tsx similarity index 100% rename from src/pages/Account/VaultsBreakdown/index.stories.tsx rename to src/pages/Account/AccountBreakdown/VaultsBreakdown/index.stories.tsx diff --git a/src/pages/Account/VaultsBreakdown/index.tsx b/src/pages/Account/AccountBreakdown/VaultsBreakdown/index.tsx similarity index 100% rename from src/pages/Account/VaultsBreakdown/index.tsx rename to src/pages/Account/AccountBreakdown/VaultsBreakdown/index.tsx diff --git a/src/pages/Account/index.spec.tsx b/src/pages/Account/AccountBreakdown/index.spec.tsx similarity index 100% rename from src/pages/Account/index.spec.tsx rename to src/pages/Account/AccountBreakdown/index.spec.tsx diff --git a/src/pages/Account/index.stories.tsx b/src/pages/Account/AccountBreakdown/index.stories.tsx similarity index 100% rename from src/pages/Account/index.stories.tsx rename to src/pages/Account/AccountBreakdown/index.stories.tsx diff --git a/src/pages/Account/AccountBreakdown/index.tsx b/src/pages/Account/AccountBreakdown/index.tsx new file mode 100644 index 0000000000..73eecb38e8 --- /dev/null +++ b/src/pages/Account/AccountBreakdown/index.tsx @@ -0,0 +1,122 @@ +/** @jsxImportSource @emotion/react */ +import BigNumber from 'bignumber.js'; +import { Spinner } from 'components'; +import React, { useMemo } from 'react'; +import { Pool, Vault } from 'types'; +import { areTokensEqual } from 'utilities'; + +import { useGetPools, useGetVaults } from 'clients/api'; +import { useAuth } from 'context/AuthContext'; +import useGetToken from 'hooks/useGetToken'; + +import { useStyles } from '../styles'; +import AccountPlaceholder from './AccountPlaceholder'; +import PoolsBreakdown from './PoolsBreakdown'; +import Summary from './Summary'; +import VaultsBreakdown from './VaultsBreakdown'; + +export interface AccountUiProps { + pools: Pool[]; + vaults: Vault[]; + isFetching?: boolean; +} + +// We assume 1 VAI = 1 dollar +const VAI_PRICE_CENTS = new BigNumber(100); + +export const AccountUi: React.FC = ({ isFetching, vaults, pools }) => { + const styles = useStyles(); + const xvs = useGetToken({ + symbol: 'XVS', + }); + + // Filter out vaults user has not staked in + const filteredVaults = useMemo( + () => vaults.filter(vault => vault.userStakedWei?.isGreaterThan(0)), + [vaults], + ); + + // Filter out pools user has not supplied in or borrowed from, unless they have assets enabled as + // collateral in that pool + const filteredPools = useMemo( + () => + pools.filter(pool => + pool.assets.some( + asset => + asset.userSupplyBalanceTokens.isGreaterThan(0) || + asset.userBorrowBalanceTokens.isGreaterThan(0) || + asset.isCollateralOfUser, + ), + ), + [pools], + ); + + const xvsPriceCents = useMemo(() => { + let priceCents = new BigNumber(0); + + pools.forEach(pool => + pool.assets.every(asset => { + if (xvs && areTokensEqual(asset.vToken.underlyingToken, xvs)) { + priceCents = asset.tokenPriceCents; + return false; + } + + return true; + }), + ); + + return priceCents; + }, [pools, xvs]); + + if (isFetching) { + return ; + } + + const hasPositions = filteredPools.length > 0 || filteredVaults.length > 0; + + if (!hasPositions) { + return ; + } + + return ( + <> +
+ + {filteredVaults.length > 0 && ( + + )} + + {filteredPools.length > 0 && } + + ); +}; + +const Account: React.FC = () => { + const { accountAddress } = useAuth(); + const { data: getPoolsData, isLoading: isGetPoolsLoading } = useGetPools({ + accountAddress, + }); + + const { data: getVaultsData, isLoading: isGetVaultsLoading } = useGetVaults({ + accountAddress, + }); + + const isFetching = isGetPoolsLoading || isGetVaultsLoading; + + return ( + + ); +}; + +export default Account; diff --git a/src/pages/Account/index.tsx b/src/pages/Account/index.tsx index 1f20cc2a15..7c16f93a03 100644 --- a/src/pages/Account/index.tsx +++ b/src/pages/Account/index.tsx @@ -1,122 +1,22 @@ /** @jsxImportSource @emotion/react */ -import BigNumber from 'bignumber.js'; -import { Spinner } from 'components'; -import React, { useMemo } from 'react'; -import { Pool, Vault } from 'types'; -import { areTokensEqual } from 'utilities'; +import React from 'react'; +import { isFeatureEnabled } from 'utilities'; -import { useGetPools, useGetVaults } from 'clients/api'; -import { useAuth } from 'context/AuthContext'; -import useGetToken from 'hooks/useGetToken'; +import PrimeStatusBanner from 'containers/PrimeStatusBanner'; -import AccountPlaceholder from './AccountPlaceholder'; -import PoolsBreakdown from './PoolsBreakdown'; -import Summary from './Summary'; -import VaultsBreakdown from './VaultsBreakdown'; +import AccountBreakdown from './AccountBreakdown'; import { useStyles } from './styles'; -export interface AccountUiProps { - pools: Pool[]; - vaults: Vault[]; - isFetching?: boolean; -} - -// We assume 1 VAI = 1 dollar -const VAI_PRICE_CENTS = new BigNumber(100); - -export const AccountUi: React.FC = ({ isFetching, vaults, pools }) => { +const Account: React.FC = () => { const styles = useStyles(); - const xvs = useGetToken({ - symbol: 'XVS', - }); - - // Filter out vaults user has not staked in - const filteredVaults = useMemo( - () => vaults.filter(vault => vault.userStakedWei?.isGreaterThan(0)), - [vaults], - ); - - // Filter out pools user has not supplied in or borrowed from, unless they have assets enabled as - // collateral in that pool - const filteredPools = useMemo( - () => - pools.filter(pool => - pool.assets.some( - asset => - asset.userSupplyBalanceTokens.isGreaterThan(0) || - asset.userBorrowBalanceTokens.isGreaterThan(0) || - asset.isCollateralOfUser, - ), - ), - [pools], - ); - - const xvsPriceCents = useMemo(() => { - let priceCents = new BigNumber(0); - - pools.forEach(pool => - pool.assets.every(asset => { - if (xvs && areTokensEqual(asset.vToken.underlyingToken, xvs)) { - priceCents = asset.tokenPriceCents; - return false; - } - - return true; - }), - ); - - return priceCents; - }, [pools, xvs]); - - if (isFetching) { - return ; - } - - const hasPositions = filteredPools.length > 0 || filteredVaults.length > 0; - - if (!hasPositions) { - return ; - } return ( <> - - - {filteredVaults.length > 0 && ( - - )} + {isFeatureEnabled('prime') && } - {filteredPools.length > 0 && } + ); }; -const Account: React.FC = () => { - const { accountAddress } = useAuth(); - const { data: getPoolsData, isLoading: isGetPoolsLoading } = useGetPools({ - accountAddress, - }); - - const { data: getVaultsData, isLoading: isGetVaultsLoading } = useGetVaults({ - accountAddress, - }); - - const isFetching = isGetPoolsLoading || isGetVaultsLoading; - - return ( - - ); -}; - export default Account; diff --git a/src/pages/Account/styles.ts b/src/pages/Account/styles.ts index 533b087437..841fbba29b 100644 --- a/src/pages/Account/styles.ts +++ b/src/pages/Account/styles.ts @@ -16,33 +16,6 @@ export const useStyles = () => { } } `, - sectionTitle: css` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: ${theme.spacing(6)}; - - ${theme.breakpoints.down('lg')} { - margin-bottom: ${theme.spacing(4)}; - } - - ${theme.breakpoints.down('md')} { - flex-direction: column-reverse; - align-items: flex-start; - margin-bottom: ${theme.spacing(4)}; - } - `, - sectionTitleText: css` - ${theme.breakpoints.down('md')} { - font-size: ${theme.typography.h4.fontSize}; - font-weight: ${theme.typography.h4.fontWeight}; - } - `, - sectionTitleToggle: css` - ${theme.breakpoints.down('md')} { - margin-bottom: ${theme.spacing(6)}; - } - `, getNetApyColor: ({ netApyPercentage }: { netApyPercentage: number }) => netApyPercentage >= 0 ? theme.palette.interactive.success : theme.palette.interactive.error, }; diff --git a/src/pages/Governance/ProposalList/CreateProposalModal/UploadOrManualProposal/index.tsx b/src/pages/Governance/ProposalList/CreateProposalModal/UploadOrManualProposal/index.tsx index 50f063b036..dfc19bbcb8 100644 --- a/src/pages/Governance/ProposalList/CreateProposalModal/UploadOrManualProposal/index.tsx +++ b/src/pages/Governance/ProposalList/CreateProposalModal/UploadOrManualProposal/index.tsx @@ -60,6 +60,7 @@ const UploadOrManualProposal: React.FC = ({ ), }} diff --git a/src/translation/translations/en.json b/src/translation/translations/en.json index 3241985958..4eac52811a 100644 --- a/src/translation/translations/en.json +++ b/src/translation/translations/en.json @@ -1,15 +1,6 @@ { "account": { "marketBreakdown": { - "cellGroup": { - "dailyEarnings": "Daily Earnings", - "netApy": "Net APY", - "netApyTooltip": "Percentage of your total supply balance received as yearly interests", - "netApyWithVaultStakeTooltip": "Percentage of your total supply and vault stake received as yearly interests", - "totalBorrow": "Total Borrow", - "totalSupply": "Total Supply", - "totalVaultStake": "Total Vault Stake" - }, "tables": { "borrowTableTitle": "Borrowed assets", "supplyTableTitle": "Supplied assets", @@ -22,6 +13,18 @@ "poolTagTooltip": "Borrow limit used: {{borrowLimitUsedPercentage}}", "title": "Pools" }, + "summary": { + "cellGroup": { + "dailyEarnings": "Daily Earnings", + "netApy": "Net APY", + "netApyTooltip": "Percentage of your total supply balance received as yearly interests", + "netApyWithVaultStakeTooltip": "Percentage of your total supply and vault stake received as yearly interests", + "totalBorrow": "Total Borrow", + "totalSupply": "Total Supply", + "totalVaultStake": "Total Vault Stake" + }, + "title": "Summary" + }, "vaultsBreakdown": { "table": { "column": { @@ -546,6 +549,26 @@ } } }, + "primeStatusBanner": { + "becomePrimeTitle": "You can now become a Primer user", + "claimButtonLabel": "Claim Prime token", + "description": "Stake {{stakeDelta}} more then wait {{claimWaitingPeriod}} to claim your Prime token and boost your earnings. Learn more", + "noPrimeTokenWarning": { + "text": "All Prime tokens have been claimed", + "tooltip": "All {{primeTokenLimit}} Prime tokens are currently claimed. Some may become available if owners no longer meet the eligibility criteria or if we add more." + }, + "progressBar": { + "ariaLabel": "Your current Prime progression", + "label": "Staked: {{userStakedXvsTokens}} / {{minXvsToStakeForPrimeTokens}}" + }, + "promotionalTitle": "Boost your earnings by up to {{percentage}}", + "stakeButtonLabel": "Stake XVS", + "successfulTransactionModal": { + "message": "You are now a Prime user. Visit the dashboard or the account page to see your earning boosts.", + "title": "Prime token claimed" + }, + "waitForPrimeTitle": "{{claimWaitingPeriod}} until you can become a Prime user" + }, "select": { "defaultLabel": "Select" }, diff --git a/src/utilities/isFeatureEnabled.ts b/src/utilities/isFeatureEnabled.ts index 34fa6fc4d6..fe6599562b 100644 --- a/src/utilities/isFeatureEnabled.ts +++ b/src/utilities/isFeatureEnabled.ts @@ -1,6 +1,6 @@ import { ENV_VARIABLES } from 'config'; -export type FeatureFlag = 'isolatedPools' | 'integratedSwap'; +export type FeatureFlag = keyof typeof featureFlags; const featureFlags = { isolatedPools: ENV_VARIABLES.VITE_FF_ISOLATED_POOLS === 'true',