diff --git a/nym-wallet/src/pages/balance/components/vesting-timeline.tsx b/nym-wallet/src/components/Balance/VestingTimeline.tsx similarity index 98% rename from nym-wallet/src/pages/balance/components/vesting-timeline.tsx rename to nym-wallet/src/components/Balance/VestingTimeline.tsx index b1a0f663ce..6ff740e33a 100644 --- a/nym-wallet/src/pages/balance/components/vesting-timeline.tsx +++ b/nym-wallet/src/components/Balance/VestingTimeline.tsx @@ -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; diff --git a/nym-wallet/src/components/Balance/cards/TokenTransfer.tsx b/nym-wallet/src/components/Balance/cards/TokenTransfer.tsx new file mode 100644 index 0000000000..cd1f3ba03c --- /dev/null +++ b/nym-wallet/src/components/Balance/cards/TokenTransfer.tsx @@ -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 ( + + + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx b/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx new file mode 100644 index 0000000000..8eec8c7eb7 --- /dev/null +++ b/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx @@ -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 ( + + + + + + {columnsHeaders.map((header) => ( + + {header.title} + + ))} + + + + + + {userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '} + {clientDetails?.display_mix_denom.toUpperCase()} + + + {vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)} + + + {userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '} + {clientDetails?.display_mix_denom.toUpperCase()} + + + +
+
+ + Percentage + + + {vestedPercentage}% + + +
+ ); +}; diff --git a/nym-wallet/src/pages/balance/components/TransferModal.tsx b/nym-wallet/src/components/Balance/modals/TransferModal.tsx similarity index 79% rename from nym-wallet/src/pages/balance/components/TransferModal.tsx rename to nym-wallet/src/components/Balance/modals/TransferModal.tsx index 02a8318064..8e2d03251d 100644 --- a/nym-wallet/src/pages/balance/components/TransferModal.tsx +++ b/nym-wallet/src/components/Balance/modals/TransferModal.tsx @@ -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(); - const [fee, setFee] = useState(); + const [tx, setTx] = useState(); 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 () => { diff --git a/nym-wallet/src/pages/balance/components/TransferModalSuccess.tsx b/nym-wallet/src/components/Balance/modals/TransferModalSuccess.tsx similarity index 92% rename from nym-wallet/src/pages/balance/components/TransferModalSuccess.tsx rename to nym-wallet/src/components/Balance/modals/TransferModalSuccess.tsx index b27c679d5a..13b762bcb5 100644 --- a/nym-wallet/src/pages/balance/components/TransferModalSuccess.tsx +++ b/nym-wallet/src/components/Balance/modals/TransferModalSuccess.tsx @@ -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 }) => ( { - 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 ( { > - {userBalance.error && ( + {userBalanceError && ( - {userBalance.error} + {userBalanceError} )} - {!userBalance.error && ( + {!userBalanceError && ( { }} variant="h5" > - {userBalance.balance?.printable_balance} + {userBalance?.printable_balance} )} {network && ( Promise; + fetchBalance: () => Promise; + onTransfer: () => Promise; +}) => { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + + const refreshBalances = async () => { + await fetchBalance(); + await fetchTokenAllocation(); + }; + + useEffect(() => { + closeSnackbar(); + fetchTokenAllocation(); + }, []); + + if (!originalVesting) return null; + + return ( + + You can use up to 10% of your locked tokens for bonding and delegating + + } + borderless + data-testid="check-unvested-tokens" + Action={ + { + await refreshBalances(); + enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true }); + }} + > + + + } + > + + + + + + + + + + ); +}; diff --git a/nym-wallet/src/pages/balance/index.tsx b/nym-wallet/src/pages/balance/index.tsx index e214d1bddf..778ca020fc 100644 --- a/nym-wallet/src/pages/balance/index.tsx +++ b/nym-wallet/src/pages/balance/index.tsx @@ -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 ( - - + + {showTransferModal && setShowTransferModal(false)} />} diff --git a/nym-wallet/src/pages/balance/types.ts b/nym-wallet/src/pages/balance/types.ts index 5f88fe60b1..4c4ee67403 100644 --- a/nym-wallet/src/pages/balance/types.ts +++ b/nym-wallet/src/pages/balance/types.ts @@ -1,2 +1 @@ export type TResponseState = 'loading' | 'success' | 'fail'; -export type TTransactionDetails = { amount: string; url: string }; diff --git a/nym-wallet/src/pages/balance/vesting.tsx b/nym-wallet/src/pages/balance/vesting.tsx deleted file mode 100644 index d84accf898..0000000000 --- a/nym-wallet/src/pages/balance/vesting.tsx +++ /dev/null @@ -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 ( - - - - - {columnsHeaders.map((header) => ( - t.palette.nym.text.muted }} align={header.align}> - {header.title} - - ))} - - - - - - {userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '} - {clientDetails?.display_mix_denom.toUpperCase()} - - - {vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)} - - - - {`${vestedPercentage}%`} - - - - - {userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '} - {clientDetails?.display_mix_denom.toUpperCase()} - - - -
-
- ); -}; - -const TokenTransfer = () => { - const { userBalance, clientDetails } = useContext(AppContext); - - return ( - - - Unlocked transferable tokens - - - - {userBalance.tokenAllocation?.spendable || 'n/a'} {clientDetails?.display_mix_denom.toUpperCase()} - - - ); -}; - -export const VestingCard = ({ onTransfer }: { onTransfer: () => Promise }) => { - 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 ( - - You can use up to 10% of your locked tokens for bonding and delegating - - } - borderless - data-testid="check-unvested-tokens" - Action={ - { - await refreshBalances(); - enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true }); - }} - > - - - } - > - - - - - - - ); -};