Skip to content

Commit

Permalink
feat: add Prime status banner (#1464)
Browse files Browse the repository at this point in the history
  • Loading branch information
therealemjy authored Sep 26, 2023
1 parent 3e8cb17 commit fe44c45
Show file tree
Hide file tree
Showing 40 changed files with 721 additions and 190 deletions.
11 changes: 11 additions & 0 deletions src/assets/img/primeLogo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/components/ProgressBar/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
313 changes: 313 additions & 0 deletions src/containers/PrimeStatusBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ContractReceipt>;
onRedirectToXvsVaultPage: () => void;
userStakedXvsTokens: BigNumber;
minXvsToStakeForPrimeTokens: BigNumber;
highestHypotheticalPrimeApyBoostPercentage: BigNumber;
primeClaimWaitingPeriodSeconds: number;
hidePromotionalTitle?: boolean;
className?: string;
}

export const PrimeStatusBannerUi: React.FC<PrimeStatusBannerUiProps> = ({
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 (
<Trans
i18nKey="primeStatusBanner.promotionalTitle"
components={{
GreenText: <span css={styles.greenText} />,
}}
values={{
percentage: readableApyBoostPercentage,
}}
/>
);
}
}, [hidePromotionalTitle, readableApyBoostPercentage, isUserXvsStakeHighEnoughForPrime]);

const ctaButton = useMemo(() => {
if (haveAllPrimeTokensBeenClaimed) {
return undefined;
}

if (isUserXvsStakeHighEnoughForPrime) {
return (
<PrimaryButton
onClick={handleClaimPrimeToken}
css={styles.button}
loading={isClaimPrimeTokenLoading}
>
{t('primeStatusBanner.claimButtonLabel')}
</PrimaryButton>
);
}

return (
<PrimaryButton onClick={onRedirectToXvsVaultPage} css={styles.button}>
{t('primeStatusBanner.stakeButtonLabel')}
</PrimaryButton>
);
}, [isUserXvsStakeHighEnoughForPrime, haveAllPrimeTokensBeenClaimed]);

return (
<Paper
css={styles.getContainer({ isProgressDisplayed: !isUserXvsStakeHighEnoughForPrime })}
className={className}
>
<div css={styles.getContentColumn({ isWarningDisplayed: haveAllPrimeTokensBeenClaimed })}>
<div css={styles.getHeader({ isProgressDisplayed: !isUserXvsStakeHighEnoughForPrime })}>
<div
css={styles.getPrimeLogo({ isProgressDisplayed: !isUserXvsStakeHighEnoughForPrime })}
>
<PrimeLogo />
</div>

<div>
{!!title && (
<Typography
variant="h3"
css={styles.getTitle({ isDescriptionDisplayed: !isUserXvsStakeHighEnoughForPrime })}
>
{title}
</Typography>
)}

{!isUserXvsStakeHighEnoughForPrime && (
<Typography>
<Trans
i18nKey="primeStatusBanner.description"
components={{
WhiteText: <span css={styles.whiteText} />,
Link: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
// TODO: add correct link
href="https://google.com"
rel="noreferrer"
target="_blank"
/>
),
}}
values={{
stakeDelta: readableStakeDeltaTokens,
claimWaitingPeriod: readableClaimWaitingPeriod,
}}
/>
</Typography>
)}
</div>
</div>

{!isUserXvsStakeHighEnoughForPrime && (
<div css={styles.getProgress({ addLeftPadding: !!title })}>
<ProgressBar
css={styles.progressBar}
value={+userStakedXvsTokens.toFixed(0)}
step={1}
ariaLabel={t('primeStatusBanner.progressBar.ariaLabel')}
min={0}
max={+minXvsToStakeForPrimeTokens.toFixed(0)}
/>

<Typography variant="small2">
<Trans
i18nKey="primeStatusBanner.progressBar.label"
components={{
WhiteText: <span css={styles.whiteText} />,
}}
values={{
minXvsToStakeForPrimeTokens: readableMinXvsToStakeForPrimeTokens,
userStakedXvsTokens: readableUserStakedXvsTokens,
}}
/>
</Typography>
</div>
)}
</div>

<div
css={styles.getCtaColumn({
isWarningDisplayed: haveAllPrimeTokensBeenClaimed,
isTitleDisplayed: !!title,
})}
>
{haveAllPrimeTokensBeenClaimed ? (
<div
css={styles.getNoPrimeTokenWarning({
isProgressDisplayed: !isUserXvsStakeHighEnoughForPrime,
})}
>
<Typography variant="small2" component="label" css={styles.warningText}>
{t('primeStatusBanner.noPrimeTokenWarning.text')}
</Typography>

<Tooltip
title={t('primeStatusBanner.noPrimeTokenWarning.tooltip', { primeTokenLimit })}
css={styles.tooltip}
>
<Icon name="info" css={styles.tooltipIcon} />
</Tooltip>
</div>
) : (
ctaButton
)}
</div>
</Paper>
);
};

export type PrimeStatusBannerProps = Pick<
PrimeStatusBannerUiProps,
'className' | 'hidePromotionalTitle'
>;

const PrimeStatusBanner: React.FC<PrimeStatusBannerProps> = 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 (
<PrimeStatusBannerUi
xvs={xvs!}
claimedPrimeTokenCount={claimedPrimeTokenCount}
primeTokenLimit={primeTokenLimit}
primeClaimWaitingPeriodSeconds={primeClaimWaitingPeriodSeconds}
userStakedXvsTokens={userStakedXvsTokens}
onRedirectToXvsVaultPage={redirectToXvsPage}
onClaimPrimeToken={claimPrimeToken}
minXvsToStakeForPrimeTokens={minXvsToStakeForPrimeTokens}
highestHypotheticalPrimeApyBoostPercentage={highestHypotheticalPrimeApyBoostPercentage}
isClaimPrimeTokenLoading={isClaimPrimeTokenLoading}
{...props}
/>
);
};

export default PrimeStatusBanner;
Loading

0 comments on commit fe44c45

Please sign in to comment.