chore(deposits): impove UX on deposit dialog (#3401)

This commit is contained in:
Maciek 2023-04-10 13:59:02 +02:00 committed by GitHub
parent 0d9bd67465
commit ecd362615e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 88 deletions

View File

@ -87,7 +87,10 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
.clear() .clear()
.type('850') .type('850')
.next(`[data-testid="${formFieldError}"]`) .next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Insufficient amount in Ethereum wallet'); .should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
}); });
}); });

View File

@ -51,11 +51,12 @@ export const ApproveNotification = ({
intent={intent} intent={intent}
testId="approve-default" testId="approve-default"
message={t( message={t(
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet` 'Before you can make a deposit of your chosen asset, %s, you need to approve its use in your Ethereum wallet',
selectedAsset?.symbol
)} )}
buttonProps={{ buttonProps={{
size: 'sm', size: 'sm',
text: `Approve ${selectedAsset?.symbol}`, text: t('Approve %s', selectedAsset?.symbol),
action: onApprove, action: onApprove,
dataTestId: 'approve-submit', dataTestId: 'approve-submit',
}} }}
@ -68,13 +69,12 @@ export const ApproveNotification = ({
intent={intent} intent={intent}
testId="reapprove-default" testId="reapprove-default"
message={t( message={t(
`Approve again to deposit more than ${formatNumber( 'Approve again to deposit more than %s',
balances.allowance.toString() formatNumber(balances.allowance.toString())
)}`
)} )}
buttonProps={{ buttonProps={{
size: 'sm', size: 'sm',
text: `Approve ${selectedAsset?.symbol}`, text: t('Approve %s', selectedAsset?.symbol),
action: onApprove, action: onApprove,
dataTestId: 'reapprove-submit', dataTestId: 'reapprove-submit',
}} }}
@ -157,7 +157,8 @@ const ApprovalTxFeedback = ({
intent={Intent.Warning} intent={Intent.Warning}
testId="approve-requested" testId="approve-requested"
message={t( message={t(
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}` 'Go to your Ethereum wallet and approve the transaction to enable the use of %s',
selectedAsset?.symbol
)} )}
/> />
</div> </div>
@ -174,7 +175,8 @@ const ApprovalTxFeedback = ({
<> <>
<p> <p>
{t( {t(
`Your ${selectedAsset?.symbol} approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit` 'Your %s approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
selectedAsset?.symbol
)}{' '} )}{' '}
</p> </p>
{txLink && <p>{txLink}</p>} {txLink && <p>{txLink}</p>}
@ -194,13 +196,10 @@ const ApprovalTxFeedback = ({
message={ message={
<> <>
<p> <p>
{t( {t('You approved deposits of up to %s %s.', [
`You can now make deposits in ${ selectedAsset?.symbol,
selectedAsset?.symbol formatNumber(allowance?.toString() || 0),
}, up to a maximum of ${formatNumber( ])}
allowance?.toString() || 0
)}`
)}
</p> </p>
{txLink && <p>{txLink}</p>} {txLink && <p>{txLink}</p>}
</> </>

View File

@ -1,4 +1,11 @@
import { waitFor, fireEvent, render, screen } from '@testing-library/react'; import {
waitFor,
fireEvent,
render,
screen,
act,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { DepositFormProps } from './deposit-form'; import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form'; import { DepositForm } from './deposit-form';
@ -141,12 +148,14 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
expect( expect(
await screen.findByText('Insufficient amount in Ethereum wallet') await screen.findByText(
"You can't deposit more than you have in your Ethereum wallet, 5"
)
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('fails when submitted amount is more than the maximum limit', async () => { it('fails when submitted amount is more than the maximum limit', async () => {
render(<DepositForm {...props} />); render(<DepositForm {...props} selectedAsset={asset} />);
const amountMoreThanLimit = '21'; const amountMoreThanLimit = '21';
fireEvent.change(screen.getByLabelText('Amount'), { fireEvent.change(screen.getByLabelText('Amount'), {
@ -155,7 +164,9 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
expect( expect(
await screen.findByText('Amount is above lifetime deposit limit') await screen.findByText(
"You can't deposit more than your remaining deposit allowance, 10 asset-symbol"
)
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -179,7 +190,9 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
expect( expect(
await screen.findByText('Amount is above approved amount') await screen.findByText(
"You can't deposit more than your approved deposit amount, 30"
)
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -283,8 +296,6 @@ describe('Deposit form', () => {
expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent( expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent(
'50' '50'
); );
expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20');
expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10');
expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10'); expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10');
fireEvent.change(screen.getByLabelText('Amount'), { fireEvent.change(screen.getByLabelText('Amount'), {
@ -365,4 +376,32 @@ describe('Deposit form', () => {
/this app only works on/i /this app only works on/i
); );
}); });
it('Remaining deposit allowance tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
})
).toBeInTheDocument();
});
});
it('Ethereum deposit cap tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /The deposit cap is set when you approve an asset for use with this app/,
})
).toBeInTheDocument();
});
});
}); });

View File

@ -8,6 +8,7 @@ import {
maxSafe, maxSafe,
addDecimal, addDecimal,
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';
@ -133,11 +134,8 @@ export const DepositForm = ({
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : []; return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
}, [_pubKeys]); }, [_pubKeys]);
const approved = balances const approved =
? balances.allowance.isGreaterThan(0) balances && balances.allowance.isGreaterThan(0) ? true : false;
? true
: false
: false;
return ( return (
<form <form
@ -194,6 +192,44 @@ export const DepositForm = ({
<InputError intent="danger">{errors.from.message}</InputError> <InputError intent="danger">{errors.from.message}</InputError>
)} )}
</FormGroup> </FormGroup>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset"> <FormGroup label={t('Asset')} labelFor="asset">
<Controller <Controller
control={control} control={control}
@ -250,45 +286,7 @@ export const DepositForm = ({
selectedAsset={selectedAsset} selectedAsset={selectedAsset}
faucetTxId={faucetTxId} faucetTxId={faucetTxId}
/> />
<FormGroup label={t('To (Vega key)')} labelFor="to"> {approved && selectedAsset && balances && (
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
{selectedAsset && balances && (
<div className="mb-6"> <div className="mb-6">
<DepositLimits {...balances} asset={selectedAsset} /> <DepositLimits {...balances} asset={selectedAsset} />
</div> </div>
@ -305,8 +303,15 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value), minSafe: (value) => minSafe(new BigNumber(min))(value),
approved: (v) => { approved: (v) => {
const value = new BigNumber(v); const value = new BigNumber(v);
if (value.isGreaterThan(balances?.allowance || 0)) { const allowance = new BigNumber(balances?.allowance || 0);
return t('Amount is above approved amount'); if (value.isGreaterThan(allowance)) {
return t(
"You can't deposit more than your approved deposit amount, %s %s",
[
formatNumber(allowance.toString()),
selectedAsset?.symbol || ' ',
]
);
} }
return true; return true;
}, },
@ -322,14 +327,24 @@ export const DepositForm = ({
} }
if (value.isGreaterThan(lifetimeLimit)) { if (value.isGreaterThan(lifetimeLimit)) {
return t('Amount is above lifetime deposit limit'); return t(
"You can't deposit more than your remaining deposit allowance, %s %s",
[
formatNumber(lifetimeLimit.toString()),
selectedAsset?.symbol || ' ',
]
);
} }
return true; return true;
}, },
balance: (v) => { balance: (v) => {
const value = new BigNumber(v); const value = new BigNumber(v);
if (value.isGreaterThan(balances?.balance || 0)) { const balance = new BigNumber(balances?.balance || 0);
return t('Insufficient amount in Ethereum wallet'); if (value.isGreaterThan(balance)) {
return t(
"You can't deposit more than you have in your Ethereum wallet, %s %s",
[formatNumber(balance), selectedAsset?.symbol || ' ']
);
} }
return true; return true;
}, },
@ -405,7 +420,7 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
type="submit" type="submit"
data-testid="deposit-submit" data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'} variant={isActive ? 'primary' : 'default'}
fill={true} fill
disabled={invalidChain} disabled={invalidChain}
> >
{t('Deposit')} {t('Deposit')}

View File

@ -1,8 +1,13 @@
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 {
KeyValueTable,
KeyValueTableRow,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js'; import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals // Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts` // See `libs/deposits/src/lib/use-deposit-balances.ts`
@ -33,21 +38,35 @@ export const DepositLimits = ({
'-' '-'
), ),
}, },
{
key: 'MAX_LIMIT',
label: t('Lifetime deposit allowance'),
rawValue: max,
value: <CompactNumber number={max} decimals={asset.decimals} />,
},
{
key: 'DEPOSITED',
label: t('Deposited'),
rawValue: deposited,
value: <CompactNumber number={deposited} decimals={asset.decimals} />,
},
{ {
key: 'REMAINING', key: 'REMAINING',
label: t('Remaining'), label: (
<Tooltip
description={
<>
<p>
{t(
'VEGA has a lifetime deposit limit of %s %s per address. This can be changed through governance',
[formatNumber(max.toString()), asset.symbol]
)}
</p>
<p>
{t(
'To date, %s %s has been deposited from this Ethereum address, so you can deposit up to %s %s more.',
[
formatNumber(deposited.toString()),
asset.symbol,
formatNumber(max.minus(deposited).toString()),
asset.symbol,
]
)}
</p>
</>
}
>
<button type="button">{t('Remaining deposit allowance')}</button>
</Tooltip>
),
rawValue: max.minus(deposited), rawValue: max.minus(deposited),
value: ( value: (
<CompactNumber <CompactNumber
@ -58,7 +77,20 @@ export const DepositLimits = ({
}, },
{ {
key: 'ALLOWANCE', key: 'ALLOWANCE',
label: t('Approved'), label: (
<Tooltip
description={
<p>
{t(
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve %s again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
asset.symbol
)}
</p>
}
>
<button type="button">{t('Ethereum deposit cap')}</button>
</Tooltip>
),
rawValue: allowance, rawValue: allowance,
value: allowance ? ( value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} /> <CompactNumber number={allowance} decimals={asset.decimals} />

View File

@ -1 +1,4 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;