Skip to content

Commit

Permalink
[WALL] [Fix] Rostislav / WALL-3096 / Fix amounts not fitting in ATM i…
Browse files Browse the repository at this point in the history
…nputs on mobile (#12377)

* refactor: 14 -> 10 max digits

* refactor: 10 -> 12

* fix: a poc

* fix: edge case

* fix: tests

* refactor: a minor change + tests
  • Loading branch information
rostislav-deriv committed Jan 4, 2024
1 parent 59a4968 commit 0373314
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 20 deletions.
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;

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;

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

1 comment on commit 0373314

@vercel
Copy link

@vercel vercel bot commented on 0373314 Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

deriv-app – ./

binary.sx
deriv-app.vercel.app
deriv-app-git-master.binary.sx
deriv-app.binary.sx

Please sign in to comment.