Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WALL] [Fix] Rostislav / WALL-3096 / Fix amounts not fitting in ATM inputs on mobile #12377

Merged
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useFormikContext } from 'formik';
import { useDebounce } from 'usehooks-ts';
import { ATMAmountInput, Timer } from '../../../../../../components';
import useInputDecimalFormatter from '../../../../../../hooks/useInputDecimalFormatter';
import { useTransfer } from '../../provider';
import type { TInitialTransferFormValues } from '../../types';
import './TransferFormAmountInput.scss';
Expand All @@ -10,13 +11,20 @@ type TProps = {
fieldName: 'fromAmount' | 'toAmount';
};

const MAX_DIGITS = 14;
const MAX_DIGITS = 12;
const USD_MAX_POSSIBLE_TRANSFER_AMOUNT = 100_000;
nijil-deriv marked this conversation as resolved.
Show resolved Hide resolved

const TransferFormAmountInput: React.FC<TProps> = ({ fieldName }) => {
const { setFieldValue, setValues, values } = useFormikContext<TInitialTransferFormValues>();
const { fromAccount, fromAmount, toAccount, toAmount } = values;

const { activeWalletExchangeRates, preferredLanguage, refetchAccountLimits, refetchExchangeRates } = useTransfer();
const {
USDExchangeRates,
activeWalletExchangeRates,
preferredLanguage,
refetchAccountLimits,
refetchExchangeRates,
} = useTransfer();

const refetchExchangeRatesAndLimits = useCallback(() => {
refetchAccountLimits();
Expand All @@ -43,6 +51,15 @@ const TransferFormAmountInput: React.FC<TProps> = ({ fieldName }) => {
? fromAccount?.currencyConfig?.fractional_digits
: toAccount?.currencyConfig?.fractional_digits;

const convertedMaxPossibleAmount = useMemo(
() => USD_MAX_POSSIBLE_TRANSFER_AMOUNT * (USDExchangeRates?.rates?.[currency ?? 'USD'] ?? 1),
[USDExchangeRates?.rates, currency]
);
const { value: formattedConvertedMaxPossibleAmount } = useInputDecimalFormatter(convertedMaxPossibleAmount, {
fractionDigits,
});
const maxDigits = formattedConvertedMaxPossibleAmount.match(/\d/g)?.length ?? MAX_DIGITS;
nijil-deriv marked this conversation as resolved.
Show resolved Hide resolved

const amountConverterHandler = useCallback(
(value: number) => {
if (
Expand Down Expand Up @@ -129,7 +146,7 @@ const TransferFormAmountInput: React.FC<TProps> = ({ fieldName }) => {
fractionDigits={fractionDigits}
label={amountLabel}
locale={preferredLanguage}
maxDigits={MAX_DIGITS}
maxDigits={maxDigits}
onBlur={() => setFieldValue('activeAmountFieldName', undefined)}
onChange={onChangeHandler}
onFocus={() => setFieldValue('activeAmountFieldName', fieldName)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/* eslint-disable camelcase */
import React from 'react';
import { Formik } from 'formik';
import { APIProvider } from '@deriv/api';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TransferProvider } from '../../../provider';
import { TAccount, TInitialTransferFormValues } from '../../../types';
import TransferFormAmountInput from '../TransferFormAmountInput';

const RATES = {
BTC: {
USD: 44000,
},
USD: {
BTC: 0.000023,
},
};

const ACCOUNTS: NonNullable<TAccount>[] = [
{
account_category: 'wallet',
account_type: 'doughflow',
balance: '1000',
currency: 'USD',
currencyConfig: {
fractional_digits: 2,
},
},
{
account_category: 'wallet',
account_type: 'crypto',
balance: '0.1',
currency: 'BTC',
currencyConfig: {
fractional_digits: 8,
},
},
] as NonNullable<TAccount>[];

const FORM_VALUES: TInitialTransferFormValues = {
activeAmountFieldName: 'fromAmount',
fromAccount: ACCOUNTS[0],
fromAmount: 0,
toAccount: ACCOUNTS[1],
toAmount: 0,
};

jest.mock('@deriv/api', () => ({
...jest.requireActual('@deriv/api'),
useGetExchangeRate: jest.fn(({ base_currency }: { base_currency: string }) => ({
data: {
base_currency,
rates: RATES[base_currency as keyof typeof RATES],
},
refetch: () => ({
data: {
base_currency,
rates: RATES[base_currency as keyof typeof RATES],
},
}),
})),
useTransferBetweenAccounts: jest.fn(() => ({
data: { accounts: ACCOUNTS },
})),
}));

describe('TransferFormAmountInput', () => {
it('renders two fields', () => {
render(
<APIProvider>
<TransferProvider accounts={ACCOUNTS}>
{/* eslint-disable-next-line @typescript-eslint/no-empty-function */}
<Formik initialValues={FORM_VALUES} onSubmit={() => {}}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<TransferFormAmountInput fieldName='fromAmount' />
</form>
)}
</Formik>
</TransferProvider>
</APIProvider>
);

const fields = screen.getAllByRole('textbox');
expect(fields).toHaveLength(2);
});

it('has 2 decimal places in case of USD', () => {
render(
<APIProvider>
<TransferProvider accounts={ACCOUNTS}>
{/* eslint-disable-next-line @typescript-eslint/no-empty-function */}
<Formik initialValues={FORM_VALUES} onSubmit={() => {}}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<TransferFormAmountInput fieldName='fromAmount' />
</form>
)}
</Formik>
</TransferProvider>
</APIProvider>
);

const field = screen.getByDisplayValue(/^\d+\.\d+$/u);
expect(field).toHaveValue('0.00');
});

it('has 8 decimal places in case of BTC', () => {
render(
<APIProvider>
<TransferProvider accounts={ACCOUNTS}>
{/* eslint-disable-next-line @typescript-eslint/no-empty-function */}
<Formik initialValues={FORM_VALUES} onSubmit={() => {}}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<TransferFormAmountInput fieldName='toAmount' />
</form>
)}
</Formik>
</TransferProvider>
</APIProvider>
);

const field = screen.getByDisplayValue(/^\d+\.\d+$/u);
expect(field).toHaveValue('0.00000000');
});

it('has 8 max digits restriction in case of USD', () => {
render(
<APIProvider>
<TransferProvider accounts={ACCOUNTS}>
{/* eslint-disable-next-line @typescript-eslint/no-empty-function */}
<Formik initialValues={FORM_VALUES} onSubmit={() => {}}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<TransferFormAmountInput fieldName='fromAmount' />
</form>
)}
</Formik>
</TransferProvider>
</APIProvider>
);

const field = screen.getByDisplayValue(/^\d+\.\d+$/u);
userEvent.type(field, '9999999999999999999999999999');
expect(field).toHaveValue('999,999.99');
});

it('has 9 max digits restriction in case of BTC', () => {
render(
<APIProvider>
<TransferProvider accounts={ACCOUNTS}>
{/* eslint-disable-next-line @typescript-eslint/no-empty-function */}
<Formik initialValues={FORM_VALUES} onSubmit={() => {}}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<TransferFormAmountInput fieldName='toAmount' />
</form>
)}
</Formik>
</TransferProvider>
</APIProvider>
);

const field = screen.getByDisplayValue(/^\d+\.\d+$/u);
userEvent.type(field, '9999999999999999999999999999');
expect(field).toHaveValue('9.99999999');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import { act, renderHook } from '@testing-library/react-hooks';
import useInputDecimalFormatter from '../useInputDecimalFormatter';

describe('useInputDecimalFormatter', () => {
it('should add zeros when fractionDigits is more then the actual fractional digits', () => {
const { result } = renderHook(() => useInputDecimalFormatter(1.23, { fractionDigits: 8 }));

expect(result.current.value).toBe('1.23000000');
});

it('should update the input value correctly when onChange is called', () => {
const { result } = renderHook(() => useInputDecimalFormatter());

act(() => {
result.current.onChange({ target: { value: '123' } });
});

expect(result.current.value).toBe('123');
expect(result.current.value).toBe('123.00');
});

it('should handle fractional digits and sign options correctly', () => {
Expand All @@ -34,10 +40,10 @@ describe('useInputDecimalFormatter', () => {
expect(result.current.value).toBe('');
});

it('should return empty string when an user clear the unput', () => {
it('should return empty string when the user clears the input', () => {
const { result } = renderHook(() => useInputDecimalFormatter(10));

expect(result.current.value).toBe('10');
expect(result.current.value).toBe('10.00');

act(() => {
result.current.onChange({ target: { value: '' } });
Expand All @@ -61,16 +67,16 @@ describe('useInputDecimalFormatter', () => {
it('should return value with sign after adding sign for integer number', () => {
const { result } = renderHook(() => useInputDecimalFormatter(1, { withSign: true }));

expect(result.current.value).toBe('1');
expect(result.current.value).toBe('1.00');

act(() => {
result.current.onChange({ target: { value: '-1' } });
result.current.onChange({ target: { value: '-1.00' } });
});

expect(result.current.value).toBe('-1');
expect(result.current.value).toBe('-1.00');
});

it('should return 0 if an user type 0', () => {
it('should return 0 if the user types 0', () => {
const { result } = renderHook(() => useInputDecimalFormatter());

expect(result.current.value).toBe('');
Expand All @@ -82,27 +88,27 @@ describe('useInputDecimalFormatter', () => {
expect(result.current.value).toBe('0');
});

it('should return previous value if an user type char', () => {
it('should return previous value if the user types non-digit characters', () => {
const { result } = renderHook(() => useInputDecimalFormatter(10));

expect(result.current.value).toBe('10');
expect(result.current.value).toBe('10.00');

act(() => {
result.current.onChange({ target: { value: 'test' } });
});

expect(result.current.value).toBe('10');
expect(result.current.value).toBe('10.00');
});

it('should return previous value if an user type integer part like this pattern 0*', () => {
it('should return previous value if the user types integer part matching this pattern: 0*', () => {
const { result } = renderHook(() => useInputDecimalFormatter(10));

expect(result.current.value).toBe('10');
expect(result.current.value).toBe('10.00');

act(() => {
result.current.onChange({ target: { value: '03' } });
});

expect(result.current.value).toBe('10');
expect(result.current.value).toBe('10.00');
});
});
2 changes: 1 addition & 1 deletion packages/wallets/src/hooks/useInputATMFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const useInputATMFormatter = (inputRef: React.RefObject<HTMLInputElement>, initi
setCaret(newCaretPosition);
setCaretNeedsRepositioning(true);

if (maxDigits && input.value.replace(separatorRegex, '').length > maxDigits) return;
if (maxDigits && input.value.replace(separatorRegex, '').replace(/^0+/, '').length > maxDigits) return;

const hasNoChangeInDigits =
input.value.length + 1 === prevFormattedValue.length &&
Expand Down
4 changes: 2 additions & 2 deletions packages/wallets/src/hooks/useInputDecimalFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const useInputDecimalFormatter = (initial?: number, options?: TOptions) => {

// The field have a decimal point and decimal places are already as allowed fraction
// digits, So we remove the extra decimal digits from the right and return the new value.
if (hasRight && right.length > fractionDigits) {
const newRight = right.substring(0, fractionDigits);
if (fractionDigits) {
const newRight = `${right ?? ''}${'0'.repeat(fractionDigits)}`.slice(0, fractionDigits);

return `${left}.${newRight}`;
}
Expand Down