feat(withdraws): improve ux surrounding withdraw balances thresholds and delays (#3402)

Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
This commit is contained in:
Bartłomiej Głownia 2023-04-12 11:21:24 +02:00 committed by GitHub
parent 45f2e926c3
commit bbfda65bcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 148 additions and 30 deletions

View File

@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { Asset } from './asset-data-provider'; import type { Asset } from './asset-data-provider';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
type Rows = { type Rows = {
key: AssetDetail; key: AssetDetail;
@ -121,9 +122,7 @@ export const rows: Rows = [
{ {
key: AssetDetail.WITHDRAWAL_THRESHOLD, key: AssetDetail.WITHDRAWAL_THRESHOLD,
label: t('Withdrawal threshold'), label: t('Withdrawal threshold'),
tooltip: t( tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
'The maximum you can withdraw instantly. Theres no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them'
),
value: (asset) => value: (asset) =>
num(asset, (asset.source as Schema.ERC20).withdrawThreshold), num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
}, },

View File

@ -0,0 +1,4 @@
import { t } from '@vegaprotocol/i18n';
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
);

View File

@ -5,3 +5,4 @@ export * from './assets-data-provider';
export * from './asset-details-dialog'; export * from './asset-details-dialog';
export * from './asset-details-table'; export * from './asset-details-table';
export * from './asset-option'; export * from './asset-option';
export * from './constants';

View File

@ -70,6 +70,8 @@ export const Notification = ({
'text-vega-green dark:text-vega-green': intent === Intent.Success, 'text-vega-green dark:text-vega-green': intent === Intent.Success,
'text-yellow-600 dark:text-yellow': intent === Intent.Warning, 'text-yellow-600 dark:text-yellow': intent === Intent.Warning,
'text-vega-pink': intent === Intent.Danger, 'text-vega-pink': intent === Intent.Danger,
'mt-1': !!title,
'mt-[0.125rem]': !title,
}, },
'flex items-start mt-1' 'flex items-start mt-1'
)} )}
@ -78,11 +80,16 @@ export const Notification = ({
</div> </div>
<div className="flex flex-col flex-grow items-start gap-1.5"> <div className="flex flex-col flex-grow items-start gap-1.5">
{title && ( {title && (
<div className="whitespace-nowrap overflow-hidden text-ellipsis uppercase leading-6"> <div
key="title"
className="whitespace-nowrap overflow-hidden text-ellipsis uppercase leading-6"
>
{title} {title}
</div> </div>
)} )}
<div className="text-sm [word-break:break-word]">{message}</div> <div key="message" className="text-sm [word-break:break-word]">
{message}
</div>
{buttonProps && ( {buttonProps && (
<Button <Button
size={buttonProps.size || 'sm'} size={buttonProps.size || 'sm'}

View File

@ -6,6 +6,7 @@ import {
removeDecimal, removeDecimal,
required, required,
isAssetTypeERC20, isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { useLocalStorage } from '@vegaprotocol/react-helpers';
@ -14,12 +15,16 @@ import {
FormGroup, FormGroup,
Input, Input,
InputError, InputError,
Notification,
RichSelect, RichSelect,
ExternalLink,
Intent,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes } from 'react'; import type { ButtonHTMLAttributes } from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { formatDistanceToNow } from 'date-fns';
import { useForm, Controller, useWatch } from 'react-hook-form'; import { useForm, Controller, useWatch } from 'react-hook-form';
import type { WithdrawalArgs } from './use-create-withdraw'; import type { WithdrawalArgs } from './use-create-withdraw';
import { WithdrawLimits } from './withdraw-limits'; import { WithdrawLimits } from './withdraw-limits';
@ -45,6 +50,47 @@ export interface WithdrawFormProps {
submitWithdraw: (withdrawal: WithdrawalArgs) => void; submitWithdraw: (withdrawal: WithdrawalArgs) => void;
} }
const WithdrawDelayNotification = ({
threshold,
delay,
symbol,
decimals,
}: {
threshold: BigNumber;
delay: number | undefined;
symbol: string;
decimals: number;
}) => {
const replacements = [
symbol,
delay ? formatDistanceToNow(Date.now() + delay * 1000) : ' ',
];
return (
<Notification
intent={Intent.Warning}
testId={
threshold.isFinite()
? 'amount-withdrawal-delay-notification'
: 'withdrawals-delay-notification'
}
message={[
!threshold.isFinite()
? t('All %s withdrawals are subject to a %s delay.', replacements)
: t('Withdrawals of %s %s or more will be delayed for %s.', [
formatNumber(threshold, decimals),
...replacements,
]),
<ExternalLink
className="ml-1"
href="https://docs.vega.xyz/testnet/concepts/deposits-withdrawals#withdrawal-limits"
>
{t('Read more')}
</ExternalLink>,
]}
/>
);
};
export const WithdrawForm = ({ export const WithdrawForm = ({
assets, assets,
balance, balance,
@ -113,6 +159,12 @@ export const WithdrawForm = ({
); );
}; };
const showWithdrawDelayNotification =
delay &&
selectedAsset &&
(!threshold.isFinite() ||
new BigNumber(amount).isGreaterThanOrEqualTo(threshold));
return ( return (
<> <>
<div className="mb-4 text-sm"> <div className="mb-4 text-sm">
@ -206,6 +258,16 @@ export const WithdrawForm = ({
{t('Use maximum')} {t('Use maximum')}
</UseButton> </UseButton>
)} )}
{showWithdrawDelayNotification && (
<div className="mt-2">
<WithdrawDelayNotification
threshold={threshold}
symbol={selectedAsset.symbol}
decimals={selectedAsset.decimals}
delay={delay}
/>
</div>
)}
</FormGroup> </FormGroup>
<Button <Button
data-testid="submit-withdrawal" data-testid="submit-withdrawal"

View File

@ -1,7 +1,12 @@
import type { Asset } from '@vegaprotocol/assets'; import type { Asset } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { CompactNumber } from '@vegaprotocol/react-helpers'; import { CompactNumber } from '@vegaprotocol/react-helpers';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from '@vegaprotocol/assets';
import {
KeyValueTable,
KeyValueTableRow,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
@ -25,7 +30,13 @@ export const WithdrawLimits = ({
? formatDistanceToNow(Date.now() + delay * 1000) ? formatDistanceToNow(Date.now() + delay * 1000)
: t('None'); : t('None');
const limits = [ const limits: {
key: string;
label: string;
value: string | JSX.Element;
rawValue?: BigNumber;
tooltip?: string;
}[] = [
{ {
key: 'BALANCE_AVAILABLE', key: 'BALANCE_AVAILABLE',
label: t('Balance available'), label: t('Balance available'),
@ -36,24 +47,35 @@ export const WithdrawLimits = ({
'-' '-'
), ),
}, },
{ ];
if (threshold.isFinite()) {
limits.push({
key: 'WITHDRAWAL_THRESHOLD', key: 'WITHDRAWAL_THRESHOLD',
label: t('Delayed withdrawal threshold'), label: t('Delayed withdrawal threshold'),
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
rawValue: threshold, rawValue: threshold,
value: <CompactNumber number={threshold} decimals={asset.decimals} />, value: <CompactNumber number={threshold} decimals={asset.decimals} />,
}, });
{ }
key: 'DELAY_TIME', limits.push({
label: t('Delay time'), key: 'DELAY_TIME',
value: delayTime, label: t('Delay time'),
}, value: delayTime,
]; });
return ( return (
<KeyValueTable> <KeyValueTable>
{limits.map(({ key, label, rawValue, value }) => ( {limits.map(({ key, label, rawValue, value, tooltip }) => (
<KeyValueTableRow key={key}> <KeyValueTableRow key={key}>
<div data-testid={`${key}_label`}>{label}</div> <div data-testid={`${key}_label`}>
{tooltip ? (
<Tooltip description={tooltip}>
<span>{label}</span>
</Tooltip>
) : (
label
)}
</div>
<div <div
data-testid={`${key}_value`} data-testid={`${key}_value`}
className="truncate" className="truncate"

View File

@ -12,15 +12,17 @@ jest.mock('@web3-react/core', () => ({
useWeb3React: () => ({ account: ethereumAddress }), useWeb3React: () => ({ account: ethereumAddress }),
})); }));
const withdrawAsset = {
asset,
balance: new BigNumber(1),
min: new BigNumber(0.0000001),
threshold: new BigNumber(1000),
delay: 10,
handleSelectAsset: jest.fn(),
};
jest.mock('./use-withdraw-asset', () => ({ jest.mock('./use-withdraw-asset', () => ({
useWithdrawAsset: () => ({ useWithdrawAsset: () => withdrawAsset,
asset,
balance: new BigNumber(1),
min: new BigNumber(0.0000001),
threshold: new BigNumber(1000),
delay: 10,
handleSelectAsset: jest.fn(),
}),
})); }));
jest.mock('@vegaprotocol/web3', () => ({ jest.mock('@vegaprotocol/web3', () => ({
@ -109,4 +111,25 @@ describe('WithdrawManager', () => {
}); });
fireEvent.submit(screen.getByTestId('withdraw-form')); fireEvent.submit(screen.getByTestId('withdraw-form'));
}; };
it('shows withdraw delay notification if amount greater than threshold', async () => {
render(generateJsx(props));
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '1000' },
});
expect(
await screen.findByTestId('amount-withdrawal-delay-notification')
).toBeInTheDocument();
});
it('shows withdraw delay notification if threshold is 0', async () => {
withdrawAsset.threshold = new BigNumber(Infinity);
render(generateJsx(props));
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.01' },
});
expect(
await screen.findByTestId('withdrawals-delay-notification')
).toBeInTheDocument();
});
}); });

View File

@ -70,7 +70,7 @@
"react-hook-form": "^7.27.0", "react-hook-form": "^7.27.0",
"react-i18next": "^11.11.4", "react-i18next": "^11.11.4",
"react-intersection-observer": "^9.2.2", "react-intersection-observer": "^9.2.2",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.6",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"react-syntax-highlighter": "^15.4.5", "react-syntax-highlighter": "^15.4.5",
"react-use-websocket": "^3.0.0", "react-use-websocket": "^3.0.0",

View File

@ -19541,10 +19541,10 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown@^8.0.5: react-markdown@^8.0.6:
version "8.0.5" version "8.0.6"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.6.tgz#3e939018f8bfce800ffdf22cf50aba3cdded7ad1"
integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A== integrity sha512-KgPWsYgHuftdx510wwIzpwf+5js/iHqBR+fzxefv8Khk3mFbnioF1bmL2idHN3ler0LMQmICKeDrWnZrX9mtbQ==
dependencies: dependencies:
"@types/hast" "^2.0.0" "@types/hast" "^2.0.0"
"@types/prop-types" "^15.0.0" "@types/prop-types" "^15.0.0"