update vesting schedule ui

This commit is contained in:
fmtabbara
2023-03-08 17:51:30 +00:00
parent 24b9b17e64
commit 60296f2a41
10 changed files with 273 additions and 229 deletions
@@ -3,7 +3,7 @@ import React, { useContext } from 'react';
import { useTheme } from '@mui/material/styles';
import { Box, Tooltip, Typography } from '@mui/material';
import { format } from 'date-fns';
import { AppContext } from '../../../context';
import { AppContext } from 'src/context';
const calculateMarkerPosition = (arrLength: number, index: number) => (1 / arrLength) * 100 * index;
@@ -0,0 +1,29 @@
import React from 'react';
import { Card, Stack, Button } from '@mui/material';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
export const TokenTransfer = ({
onTransfer,
unlockedTokens,
unlockedRewards,
unlockedTransferable,
}: {
userBalance?: string;
unlockedTokens?: string;
unlockedRewards?: string;
unlockedTransferable?: string;
onTransfer: () => void;
}) => {
return (
<Card variant="outlined" sx={{ p: 3, height: '100%' }}>
<Stack gap={1} sx={{ mb: 2 }}>
<ModalListItem label="Unlocked tokens" value={unlockedTokens} />
<ModalListItem label="Unlocked rewards" value={unlockedRewards} divider />
<ModalListItem label="Unlocked transferabled tokens" value={unlockedTransferable} fontWeight={600} />
</Stack>
<Button size="large" fullWidth variant="contained" onClick={onTransfer} disableElevation>
Transfer
</Button>
</Card>
);
};
@@ -0,0 +1,110 @@
import { useContext, useState, useEffect } from 'react';
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Box,
Typography,
TableCellProps,
Card,
} from '@mui/material';
import { Period } from '@nymproject/types';
import { AppContext } from 'src/context';
import { VestingTimeline } from '../VestingTimeline';
import { Stack } from '@mui/system';
const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = [
{ title: 'Locked', align: 'left' },
{ title: 'Period', align: 'left' },
{ title: 'Unlocked', align: 'right' },
];
const vestingPeriod = (current?: Period, original?: number) => {
if (current === 'After') return 'Complete';
if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`;
return 'N/A';
};
export const VestingSchedule = () => {
const { userBalance, clientDetails } = useContext(AppContext);
const [vestedPercentage, setVestedPercentage] = useState(0);
const calculatePercentage = () => {
const { tokenAllocation, originalVesting } = userBalance;
if (tokenAllocation?.vesting && tokenAllocation.vested && tokenAllocation.vested !== '0' && originalVesting) {
const percentage = (+tokenAllocation.vested / +originalVesting.amount.amount) * 100;
const rounded = percentage.toFixed(2);
setVestedPercentage(+rounded);
} else {
setVestedPercentage(0);
}
};
useEffect(() => {
calculatePercentage();
}, [userBalance.tokenAllocation, calculatePercentage]);
return (
<Card variant="outlined" sx={{ p: 3, height: '100%' }}>
<TableContainer sx={{ mb: 2 }}>
<Table>
<TableHead>
<TableRow>
{columnsHeaders.map((header) => (
<TableCell key={header.title} sx={{ color: 'nym.text.muted', pt: 0 }} align={header.align}>
{header.title}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell
sx={{
color: 'text.primary',
borderBottom: 'none',
textTransform: 'uppercase',
}}
>
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
<TableCell
align="left"
sx={{
color: 'text.primary',
borderBottom: 'none',
}}
>
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
</TableCell>
<TableCell
sx={{
color: 'text.primary',
borderBottom: 'none',
textTransform: 'uppercase',
}}
align="right"
>
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Typography variant="body2" sx={{ color: 'nym.text.muted', mb: 3 }}>
Percentage
</Typography>
<Stack direction="row" alignItems="center" gap={2}>
<Typography variant="body2">{vestedPercentage}%</Typography>
<VestingTimeline percentageComplete={vestedPercentage} />
</Stack>
</Card>
);
};
@@ -8,36 +8,22 @@ import { FeeWarning } from 'src/components/FeeWarning';
import { withdrawVestedCoins } from 'src/requests';
import { Console } from 'src/utils/console';
import { simulateWithdrawVestedCoins } from 'src/requests/simulate';
import { SuccessModal } from './TransferModalSuccess';
import { TResponseState, TTransactionDetails } from '../types';
import { useGetFee } from 'src/hooks/useGetFee';
import { SuccessModal, TTransactionDetails } from './TransferModalSuccess';
import { TResponseState } from '../../../pages/balance/types';
export const TransferModal = ({ onClose }: { onClose: () => void }) => {
const [state, setState] = useState<TResponseState>();
const [fee, setFee] = useState<FeeDetails>();
const [tx, setTx] = useState<TTransactionDetails>();
const { userBalance, clientDetails, network } = useContext(AppContext);
const getFee = async () => {
if (userBalance.tokenAllocation?.spendable && clientDetails?.display_mix_denom) {
try {
const simulatedFee = await simulateWithdrawVestedCoins({
amount: { amount: userBalance.tokenAllocation?.spendable, denom: clientDetails?.display_mix_denom },
});
setFee(simulatedFee);
await userBalance.refreshBalances();
} catch (e) {
setFee({
amount: { amount: 'n/a', denom: clientDetails?.display_mix_denom.toUpperCase() as CurrencyDenom },
fee: { Auto: null },
});
Console.error(e);
}
}
};
const { fee, getFee } = useGetFee();
useEffect(() => {
getFee();
getFee(simulateWithdrawVestedCoins, {
amount: { amount: userBalance.tokenAllocation?.spendable, denom: clientDetails?.display_mix_denom },
});
}, []);
const handleTransfer = async () => {
@@ -2,7 +2,8 @@ import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { ConfirmationModal } from 'src/components';
import { TTransactionDetails } from '../types';
export type TTransactionDetails = { amount: string; url: string };
export const SuccessModal = ({ tx, onClose }: { tx?: TTransactionDetails; onClose: () => void }) => (
<ConfirmationModal
@@ -3,14 +3,20 @@ import { Alert, Grid, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { NymCard, ClientAddress } from '../../components';
import { AppContext, urls } from '../../context/main';
import { Network } from 'src/types';
import { Balance } from '@nymproject/types';
export const BalanceCard = () => {
const { userBalance, clientDetails, network } = useContext(AppContext);
useEffect(() => {
userBalance.fetchBalance();
}, []);
export const BalanceCard = ({
userBalance,
userBalanceError,
network,
clientAddress,
}: {
userBalance?: Balance;
userBalanceError?: string;
network?: Network;
clientAddress?: string;
}) => {
return (
<NymCard
title="Balance"
@@ -20,12 +26,12 @@ export const BalanceCard = () => {
>
<Grid container direction="column" spacing={2}>
<Grid item>
{userBalance.error && (
{userBalanceError && (
<Alert severity="error" data-testid="error-refresh" sx={{ p: 2 }}>
{userBalance.error}
{userBalanceError}
</Alert>
)}
{!userBalance.error && (
{!userBalanceError && (
<Typography
data-testid="refresh-success"
sx={{
@@ -36,14 +42,14 @@ export const BalanceCard = () => {
}}
variant="h5"
>
{userBalance.balance?.printable_balance}
{userBalance?.printable_balance}
</Typography>
)}
</Grid>
{network && (
<Grid item>
<Link
href={`${urls(network).mixnetExplorer}/account/${clientDetails?.client_address}`}
href={`${urls(network).mixnetExplorer}/account/${clientAddress}`}
target="_blank"
text="Last transactions"
fontSize={14}
+79
View File
@@ -0,0 +1,79 @@
import React from 'react';
import { Refresh } from '@mui/icons-material';
import { Grid, IconButton, Typography } from '@mui/material';
import { useSnackbar } from 'notistack';
import { useEffect } from 'react';
import { NymCard } from 'src/components';
import { TokenTransfer } from 'src/components/Balance/cards/TokenTransfer';
import { OriginalVestingResponse } from '@nymproject/types';
import { VestingSchedule } from 'src/components/Balance/cards/VestingSchedule';
export const VestingCard = ({
userBalance,
unlockedTokens,
unlockedRewards,
originalVesting,
onTransfer,
fetchBalance,
fetchTokenAllocation,
}: {
unlockedTokens?: string;
unlockedRewards?: string;
userBalance?: string;
originalVesting?: OriginalVestingResponse;
fetchTokenAllocation: () => Promise<void>;
fetchBalance: () => Promise<void>;
onTransfer: () => Promise<void>;
}) => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const refreshBalances = async () => {
await fetchBalance();
await fetchTokenAllocation();
};
useEffect(() => {
closeSnackbar();
fetchTokenAllocation();
}, []);
if (!originalVesting) return null;
return (
<NymCard
title="Vesting Schedule"
subheader={
<Typography variant="caption" sx={{ color: 'nym.text.muted' }}>
You can use up to 10% of your locked tokens for bonding and delegating
</Typography>
}
borderless
data-testid="check-unvested-tokens"
Action={
<IconButton
onClick={async () => {
await refreshBalances();
enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true });
}}
>
<Refresh />
</IconButton>
}
>
<Grid container spacing={3}>
<Grid item xs={12} xl={8}>
<VestingSchedule />
</Grid>
<Grid item xs={12} xl={4}>
<TokenTransfer
onTransfer={onTransfer}
unlockedTokens={unlockedTokens}
unlockedRewards={unlockedRewards}
unlockedTransferable={unlockedTokens}
userBalance={userBalance}
/>
</Grid>
</Grid>
</NymCard>
);
};
+26 -7
View File
@@ -1,27 +1,46 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Box } from '@mui/material';
import { AppContext } from '../../context/main';
import { BalanceCard } from './balance';
import { VestingCard } from './vesting';
import { BalanceCard } from './Balance';
import { VestingCard } from './Vesting';
import { PageLayout } from '../../layouts';
import { TransferModal } from './components/TransferModal';
import { TransferModal } from '../../components/Balance/modals/TransferModal';
export const Balance = () => {
const [showTransferModal, setShowTransferModal] = useState(false);
const { userBalance } = useContext(AppContext);
const { userBalance, clientDetails, network } = useContext(AppContext);
useEffect(() => {
userBalance.fetchBalance();
}, []);
const handleShowTransferModal = async () => {
await userBalance.refreshBalances();
setShowTransferModal(true);
};
const appendDenom = (value: string = '') => `${value} ${clientDetails?.display_mix_denom.toUpperCase()}`;
return (
<PageLayout>
<Box display="flex" flexDirection="column" gap={4}>
<BalanceCard />
<VestingCard onTransfer={handleShowTransferModal} />
<BalanceCard
userBalance={userBalance.balance}
userBalanceError={userBalance.error}
clientAddress={clientDetails?.client_address}
network={network}
/>
<VestingCard
userBalance={appendDenom(userBalance.balance?.printable_balance)}
unlockedTokens={appendDenom(userBalance.tokenAllocation?.spendable)}
unlockedRewards={appendDenom(userBalance.tokenAllocation?.spendable)}
originalVesting={userBalance.originalVesting}
onTransfer={handleShowTransferModal}
fetchBalance={userBalance.fetchBalance}
fetchTokenAllocation={userBalance.fetchTokenAllocation}
/>
{showTransferModal && <TransferModal onClose={() => setShowTransferModal(false)} />}
</Box>
</PageLayout>
-1
View File
@@ -1,2 +1 @@
export type TResponseState = 'loading' | 'success' | 'fail';
export type TTransactionDetails = { amount: string; url: string };
-185
View File
@@ -1,185 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import { Refresh } from '@mui/icons-material';
import {
Box,
Button,
IconButton,
Table,
TableBody,
TableCell,
TableCellProps,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@mui/material';
import { useSnackbar } from 'notistack';
import { NymCard } from 'src/components';
import { AppContext } from 'src/context/main';
import { Period } from 'src/types';
import { VestingTimeline } from './components/vesting-timeline';
const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = [
{ title: 'Locked', align: 'left' },
{ title: 'Period', align: 'left' },
{ title: 'Percentage Vested', align: 'left' },
{ title: 'Unlocked', align: 'right' },
];
const vestingPeriod = (current?: Period, original?: number) => {
if (current === 'After') return 'Complete';
if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`;
return 'N/A';
};
const VestingSchedule = () => {
const { userBalance, clientDetails } = useContext(AppContext);
const [vestedPercentage, setVestedPercentage] = useState(0);
const calculatePercentage = () => {
const { tokenAllocation, originalVesting } = userBalance;
if (tokenAllocation?.vesting && tokenAllocation.vested && tokenAllocation.vested !== '0' && originalVesting) {
const percentage = (+tokenAllocation.vested / +originalVesting.amount.amount) * 100;
const rounded = percentage.toFixed(2);
setVestedPercentage(+rounded);
} else {
setVestedPercentage(0);
}
};
useEffect(() => {
calculatePercentage();
}, [userBalance.tokenAllocation, calculatePercentage]);
return (
<TableContainer sx={{ py: 1 }}>
<Table>
<TableHead>
<TableRow>
{columnsHeaders.map((header) => (
<TableCell key={header.title} sx={{ color: (t) => t.palette.nym.text.muted }} align={header.align}>
{header.title}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell
sx={{
color: 'text.primary',
borderBottom: 'none',
textTransform: 'uppercase',
}}
>
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
<TableCell
align="left"
sx={{
color: 'text.primary',
borderBottom: 'none',
}}
>
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
</TableCell>
<TableCell
sx={{
color: 'text.primary',
borderBottom: 'none',
}}
>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="body2">{`${vestedPercentage}%`}</Typography>
<VestingTimeline percentageComplete={vestedPercentage} />
</Box>
</TableCell>
<TableCell
sx={{
color: 'text.primary',
borderBottom: 'none',
textTransform: 'uppercase',
}}
align="right"
>
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
};
const TokenTransfer = () => {
const { userBalance, clientDetails } = useContext(AppContext);
return (
<Box sx={{ my: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 3, fontWeight: '600' }}>
Unlocked transferable tokens
</Typography>
<Typography
data-testid="refresh-success"
sx={{ color: 'text.primary', fontWeight: '600', fontSize: 28 }}
variant="h5"
textTransform="uppercase"
>
{userBalance.tokenAllocation?.spendable || 'n/a'} {clientDetails?.display_mix_denom.toUpperCase()}
</Typography>
</Box>
);
};
export const VestingCard = ({ onTransfer }: { onTransfer: () => Promise<void> }) => {
const { userBalance } = useContext(AppContext);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const refreshBalances = async () => {
await userBalance.fetchBalance();
await userBalance.fetchTokenAllocation();
};
useEffect(() => {
closeSnackbar();
userBalance.fetchTokenAllocation();
}, []);
if (!userBalance.originalVesting) return null;
return (
<NymCard
title="Vesting Schedule"
subheader={
<Typography variant="caption" sx={{ color: 'nym.text.muted' }}>
You can use up to 10% of your locked tokens for bonding and delegating
</Typography>
}
borderless
data-testid="check-unvested-tokens"
Action={
<IconButton
onClick={async () => {
await refreshBalances();
enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true });
}}
>
<Refresh />
</IconButton>
}
>
<VestingSchedule />
<TokenTransfer />
<Box display="flex" justifyContent="end" alignItems="center">
<Button size="large" variant="contained" onClick={onTransfer} disableElevation>
Transfer
</Button>
</Box>
</NymCard>
);
};