forked from binary-com/deriv-app
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[cashier-v2] Aum / FEQ-1950 / cashier-v2-transfer-amount-input-conver…
…ter (binary-com#14282) * chore: basic setup for TransferModule * feat: added CryptoFiatConverter and TransferCryptoFiatAmountConverter * fix: fixed input validation issue with CryptoFiatConverter * perf: add unit tests for CryptoFiatConverter * perf: added unit tests for TransferCryptoFiatAmountConverter * chore: applied suggestions
- Loading branch information
Showing
26 changed files
with
641 additions
and
5 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
packages/cashier-v2/src/components/CryptoFiatConverter/CryptoFiatConverter.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
.container { | ||
display: flex; | ||
gap: 0.8rem; | ||
|
||
@include mobile-cashier-v2 { | ||
flex-direction: column; | ||
align-items: center; | ||
} | ||
} | ||
|
||
.arrow-container { | ||
height: 3.45rem; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
} | ||
|
||
.arrow-icon { | ||
transition: all 0.2s ease; | ||
rotate: 90deg; | ||
|
||
@include mobile-cashier-v2 { | ||
rotate: 180deg; | ||
} | ||
|
||
&--rtl { | ||
rotate: -90deg; | ||
|
||
@include mobile-cashier-v2 { | ||
rotate: 0deg; | ||
} | ||
} | ||
} |
122 changes: 122 additions & 0 deletions
122
packages/cashier-v2/src/components/CryptoFiatConverter/CryptoFiatConverter.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import clsx from 'clsx'; | ||
import { Field, FieldProps, useFormikContext } from 'formik'; | ||
import { InferType } from 'yup'; | ||
import { StandaloneArrowDownBoldIcon } from '@deriv/quill-icons'; | ||
import { Input, useDevice } from '@deriv-com/ui'; | ||
import { getCryptoFiatConverterValidationSchema, TGetCryptoFiatConverterValidationSchema } from './utils'; | ||
import styles from './CryptoFiatConverter.module.scss'; | ||
|
||
type TGetConvertedAmountParams = | ||
| TGetCryptoFiatConverterValidationSchema['fromAccount'] | ||
| TGetCryptoFiatConverterValidationSchema['toAccount']; | ||
|
||
const getConvertedAmount = (amount: string, source: TGetConvertedAmountParams, target: TGetConvertedAmountParams) => { | ||
const value = Number(amount); | ||
|
||
if (!value) return ''; | ||
|
||
// TODO: replace these temporary values with exchange rate | ||
const fromRate = 1; | ||
const toRate = 0.5; | ||
|
||
const convertedValue = | ||
// eslint-disable-next-line sonarjs/prefer-immediate-return | ||
((value * toRate) / fromRate).toFixed(target.fractionalDigits); | ||
|
||
return convertedValue; | ||
}; | ||
|
||
type TContext = InferType<ReturnType<typeof getCryptoFiatConverterValidationSchema>>; | ||
|
||
const CryptoFiatConverter: React.FC<TGetCryptoFiatConverterValidationSchema> = ({ fromAccount, toAccount }) => { | ||
const { isMobile } = useDevice(); | ||
const [isFromInputActive, setIsFromInputActive] = useState(true); | ||
|
||
const { errors, setFieldValue, setValues } = useFormikContext<TContext>(); | ||
|
||
useEffect(() => { | ||
if (errors.toAmount && !isFromInputActive) { | ||
setFieldValue('fromAmount', ''); | ||
} | ||
}, [errors.toAmount, isFromInputActive, setFieldValue]); | ||
|
||
useEffect(() => { | ||
if (errors.fromAmount && isFromInputActive) { | ||
setFieldValue('toAmount', ''); | ||
} | ||
}, [errors.fromAmount, isFromInputActive, setFieldValue]); | ||
|
||
const handleFromAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const convertedValue = getConvertedAmount(e.target.value, fromAccount, toAccount); | ||
|
||
setValues(currentValues => ({ | ||
...currentValues, | ||
fromAmount: e.target.value, | ||
toAmount: convertedValue, | ||
})); | ||
}; | ||
|
||
const handleToAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const convertedValue = getConvertedAmount(e.target.value, toAccount, fromAccount); | ||
|
||
setValues(currentValues => ({ | ||
...currentValues, | ||
fromAmount: convertedValue, | ||
toAmount: e.target.value, | ||
})); | ||
}; | ||
|
||
return ( | ||
<div className={styles.container}> | ||
<Field name='fromAmount'> | ||
{({ field }: FieldProps) => ( | ||
<Input | ||
{...field} | ||
autoComplete='off' | ||
data-testid='dt_crypto_fiat_converter_from_amount_field' | ||
error={Boolean(errors.fromAmount)} | ||
isFullWidth={fromAccount.currency !== toAccount.currency} | ||
label={`Amount (${fromAccount.currency})`} | ||
message={errors.fromAmount} | ||
onChange={handleFromAmountChange} | ||
onFocus={() => { | ||
setIsFromInputActive(true); | ||
}} | ||
type='text' | ||
/> | ||
)} | ||
</Field> | ||
{fromAccount.currency !== toAccount.currency && ( | ||
<> | ||
<div className={styles['arrow-container']}> | ||
<StandaloneArrowDownBoldIcon | ||
className={clsx(styles['arrow-icon'], { [styles['arrow-icon--rtl']]: isFromInputActive })} | ||
iconSize={isMobile ? 'sm' : 'md'} | ||
/> | ||
</div> | ||
<Field name='toAmount'> | ||
{({ field }: FieldProps) => ( | ||
<Input | ||
{...field} | ||
autoComplete='off' | ||
data-testid='dt_crypto_fiat_converter_to_amount_field' | ||
error={Boolean(errors.toAmount)} | ||
isFullWidth | ||
label={`Amount (${toAccount.currency})`} | ||
message={errors.toAmount} | ||
onChange={handleToAmountChange} | ||
onFocus={() => { | ||
setIsFromInputActive(false); | ||
}} | ||
type='text' | ||
/> | ||
)} | ||
</Field> | ||
</> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default CryptoFiatConverter; |
110 changes: 110 additions & 0 deletions
110
...ages/cashier-v2/src/components/CryptoFiatConverter/__tests__/CryptoFiatConverter.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import React from 'react'; | ||
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'; | ||
import { useDevice } from '@deriv-com/ui'; | ||
import CryptoFiatConverter from '../CryptoFiatConverter'; | ||
import { TCurrency } from '../../../types'; | ||
import { Formik } from 'formik'; | ||
|
||
jest.mock('@deriv-com/ui', () => ({ | ||
...jest.requireActual('@deriv-com/ui'), | ||
useDevice: jest.fn(), | ||
})); | ||
|
||
const mockFromAccount = { | ||
balance: 1000, | ||
currency: 'USD' as TCurrency, | ||
fractionalDigits: 2, | ||
limits: { | ||
max: 100, | ||
min: 1, | ||
}, | ||
}; | ||
|
||
const mockToAccount = { | ||
currency: 'BTC' as TCurrency, | ||
fractionalDigits: 8, | ||
}; | ||
|
||
const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => { | ||
return ( | ||
<Formik | ||
initialValues={{ | ||
fromAmount: '', | ||
toAmount: '', | ||
}} | ||
onSubmit={jest.fn()} | ||
> | ||
{children} | ||
</Formik> | ||
); | ||
}; | ||
|
||
describe('CryptoFiatConverter', () => { | ||
beforeEach(() => { | ||
(useDevice as jest.Mock).mockReturnValue({ | ||
isMobile: true, | ||
}); | ||
}); | ||
|
||
afterEach(cleanup); | ||
|
||
it('should check if the toAmount field is empty when there is an input error in the fromAmount field', async () => { | ||
render(<CryptoFiatConverter fromAccount={mockFromAccount} toAccount={mockToAccount} />, { wrapper }); | ||
|
||
const fromAmountField = screen.getByTestId('dt_crypto_fiat_converter_from_amount_field'); | ||
const toAmountField = screen.getByTestId('dt_crypto_fiat_converter_to_amount_field'); | ||
|
||
await act(async () => { | ||
await fireEvent.change(fromAmountField, { target: { value: '1.0.' } }); | ||
}); | ||
|
||
expect(toAmountField).toHaveValue(''); | ||
}); | ||
|
||
it('should check if the fromAmount field is empty when there is an input error in the toAmount field', async () => { | ||
render(<CryptoFiatConverter fromAccount={mockFromAccount} toAccount={mockToAccount} />, { wrapper }); | ||
|
||
const fromAmountField = screen.getByTestId('dt_crypto_fiat_converter_from_amount_field'); | ||
const toAmountField = screen.getByTestId('dt_crypto_fiat_converter_to_amount_field'); | ||
|
||
await act(async () => { | ||
await fireEvent.change(toAmountField, { target: { value: '1.0.' } }); | ||
}); | ||
|
||
expect(fromAmountField).toHaveValue(''); | ||
}); | ||
|
||
it('should test for properly converted toAmount when valid amount is given in fromAmount', async () => { | ||
(useDevice as jest.Mock).mockReturnValue({ | ||
isMobile: true, | ||
}); | ||
|
||
render(<CryptoFiatConverter fromAccount={mockFromAccount} toAccount={mockToAccount} />, { wrapper }); | ||
|
||
const fromAmountField = screen.getByTestId('dt_crypto_fiat_converter_from_amount_field'); | ||
const toAmountField = screen.getByTestId('dt_crypto_fiat_converter_to_amount_field'); | ||
|
||
await act(async () => { | ||
await fireEvent.change(fromAmountField, { target: { value: '1.0' } }); | ||
}); | ||
|
||
expect(toAmountField).toHaveValue('0.50000000'); | ||
}); | ||
|
||
it('should test for properly converted fromAmount when valid amount is given in toAmount', async () => { | ||
(useDevice as jest.Mock).mockReturnValue({ | ||
isMobile: true, | ||
}); | ||
|
||
render(<CryptoFiatConverter fromAccount={mockFromAccount} toAccount={mockToAccount} />, { wrapper }); | ||
|
||
const fromAmountField = screen.getByTestId('dt_crypto_fiat_converter_from_amount_field'); | ||
const toAmountField = screen.getByTestId('dt_crypto_fiat_converter_to_amount_field'); | ||
|
||
await act(async () => { | ||
await fireEvent.change(toAmountField, { target: { value: '1' } }); | ||
}); | ||
|
||
expect(fromAmountField).toHaveValue('0.50'); | ||
}); | ||
}); |
2 changes: 2 additions & 0 deletions
2
packages/cashier-v2/src/components/CryptoFiatConverter/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default as CryptoFiatConverter } from './CryptoFiatConverter'; | ||
export { getCryptoFiatConverterValidationSchema } from './utils'; |
63 changes: 63 additions & 0 deletions
63
...cashier-v2/src/components/CryptoFiatConverter/utils/cryptoFiatAmountConverterValidator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import * as Yup from 'yup'; | ||
import { TCurrency } from '../../../types'; | ||
import { | ||
betweenMinAndMaxValidator, | ||
decimalsValidator, | ||
insufficientBalanceValidator, | ||
numberValidator, | ||
} from '../../../utils'; | ||
|
||
export type TGetCryptoFiatConverterValidationSchema = { | ||
fromAccount: { | ||
balance: number; | ||
currency: TCurrency; | ||
fractionalDigits?: number; | ||
limits: { | ||
max: number; | ||
min: number; | ||
}; | ||
}; | ||
toAccount: { | ||
currency: TCurrency; | ||
fractionalDigits?: number; | ||
}; | ||
}; | ||
|
||
export const getCryptoFiatConverterValidationSchema = ({ | ||
fromAccount, | ||
toAccount, | ||
}: TGetCryptoFiatConverterValidationSchema) => { | ||
return Yup.object({ | ||
fromAmount: Yup.string() | ||
.required('This field is required.') | ||
.test({ | ||
name: 'test-valid-number', | ||
test: (value = '', context) => numberValidator(value, context), | ||
}) | ||
.test({ | ||
name: 'test-decimals', | ||
test: (value = '', context) => decimalsValidator(value, context, fromAccount.fractionalDigits ?? 2), | ||
}) | ||
.test({ | ||
name: 'test-insufficient-funds', | ||
test: (value = '', context) => insufficientBalanceValidator(value, context, fromAccount.balance), | ||
}) | ||
.test({ | ||
name: 'test-between-min-max', | ||
test: (value = '', context) => | ||
betweenMinAndMaxValidator(value, context, { | ||
currency: fromAccount.currency, | ||
limits: fromAccount.limits, | ||
}), | ||
}), | ||
toAmount: Yup.string() | ||
.test({ | ||
name: 'test-valid-number', | ||
test: (value = '', context) => numberValidator(value, context), | ||
}) | ||
.test({ | ||
name: 'test-decimals', | ||
test: (value = '', context) => decimalsValidator(value, context, toAccount.fractionalDigits ?? 2), | ||
}), | ||
}); | ||
}; |
1 change: 1 addition & 0 deletions
1
packages/cashier-v2/src/components/CryptoFiatConverter/utils/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './cryptoFiatAmountConverterValidator'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
packages/cashier-v2/src/flows/AccountTransfer/AccountTransfer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
import { PageContainer } from '../../components'; | ||
import { TransferModule } from '../../lib'; | ||
|
||
const AccountTransfer = () => { | ||
return ( | ||
<PageContainer> | ||
<TransferModule /> | ||
</PageContainer> | ||
); | ||
}; | ||
|
||
export default AccountTransfer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as AccountTransfer } from './AccountTransfer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { AccountTransfer } from './AccountTransfer'; | ||
export { Deposit } from './Deposit'; | ||
export { PaymentAgentTransfer } from './PaymentAgentTransfer'; | ||
export { Withdrawal } from './Withdrawal'; |
Oops, something went wrong.