diff --git a/packages/account/package.json b/packages/account/package.json index 62711f003835..afb67c220197 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@binary-com/binary-document-uploader": "^2.4.8", + "@deriv/api": "^1.0.0", "@deriv/api-types": "^1.0.118", "@deriv/components": "^1.0.0", "@deriv/hooks": "^1.0.0", diff --git a/packages/account/src/App.tsx b/packages/account/src/App.tsx index 9c71d674d019..58d34d3b6f71 100644 --- a/packages/account/src/App.tsx +++ b/packages/account/src/App.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Routes from './Containers/routes'; import ResetTradingPassword from './Containers/reset-trading-password'; -import { setWebsocket } from '@deriv/shared'; +import { APIProvider } from '@deriv/api'; import { StoreProvider } from '@deriv/stores'; import type { TCoreStores } from '@deriv/stores/types'; @@ -14,17 +14,18 @@ type TAppProps = { }; const App = ({ passthrough }: TAppProps) => { - const { root_store, WS } = passthrough; - setWebsocket(WS); + const { root_store } = passthrough; const { notification_messages_ui: Notifications } = root_store.ui; return ( - - {Notifications && } - - - + + + {Notifications && } + + + + ); }; diff --git a/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx b/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx index 96cd85212e52..abdf3767e41a 100644 --- a/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx +++ b/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx @@ -1,8 +1,11 @@ import React from 'react'; -import { FormikProps, FormikValues } from 'formik'; +import { FormikProps } from 'formik'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { isDesktop, isMobile, PlatformContext } from '@deriv/shared'; -import AddressDetails from '../address-details'; +import { useStatesList } from '@deriv/hooks'; +import { isDesktop, isMobile } from '@deriv/shared'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import AddressDetails, { TAddressDetailFormProps } from '../address-details'; +import { TStores } from '@deriv/stores/types'; jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), @@ -19,6 +22,11 @@ jest.mock('../../real-account-signup/helpers/utils.ts', () => ({ })), })); +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useStatesList: jest.fn(() => ({ data: [], isFetched: true })), +})); + jest.mock('@deriv/components', () => { const original_module = jest.requireActual('@deriv/components'); @@ -45,19 +53,16 @@ describe('', () => { let modal_root_el: HTMLDivElement; const mock_props: React.ComponentProps = { - fetchStatesList: jest.fn(() => Promise.resolve([])), getCurrentStep: jest.fn(), goToNextStep: jest.fn(), goToPreviousStep: jest.fn(), - has_real_account: false, is_gb_residence: '', - is_svg: true, onCancel: jest.fn(), onSave: jest.fn(), onSubmit: jest.fn(), - onSubmitEnabledChange: jest.fn(), - selected_step_ref: { current: { isSubmitting: false } } as React.RefObject>, - states_list: [], + selected_step_ref: { current: { isSubmitting: false } } as React.RefObject< + FormikProps + >, value: { address_city: '', address_line_1: '', @@ -67,10 +72,12 @@ describe('', () => { }, validate: jest.fn(), disabled_items: [], + has_real_account: false, }; + const store = mockStore({}); + const svgCommonRenderCheck = () => { - expect(mock_props.onSubmitEnabledChange).toHaveBeenCalledTimes(1); expect(screen.getByLabelText(address_line_1_marked)).toBeInTheDocument(); expect(screen.getByLabelText(address_line_2)).toBeInTheDocument(); expect(screen.getByLabelText(address_postcode)).toBeInTheDocument(); @@ -84,6 +91,14 @@ describe('', () => { expect(screen.queryByLabelText(address_town)).not.toBeInTheDocument(); }; + const renderComponent = ({ props = mock_props, store_config = store }) => { + return render( + + + + ); + }; + beforeEach(() => { (isDesktop as jest.Mock).mockReturnValue(true); (isMobile as jest.Mock).mockReturnValue(false); @@ -101,10 +116,15 @@ describe('', () => { }); it('should render AddressDetails component for mobile', async () => { - (isDesktop as jest.Mock).mockReturnValue(false); - (isMobile as jest.Mock).mockReturnValue(true); + const new_store_config: TStores = { + ...store, + ui: { + ...store.ui, + is_mobile: true, + }, + }; - render(); + renderComponent({ store_config: new_store_config }); await waitFor(() => { svgCommonRenderCheck(); @@ -117,33 +137,18 @@ describe('', () => { expect(required_fields).toHaveLength(2); }); - it('should call fetchResidenceList if states list is empty', async () => { - render(); - expect(mock_props.fetchStatesList).toHaveBeenCalled(); - }); - - it('should not call fetchResidenceList if states list is empty', async () => { - render( - - ); - - expect(mock_props.fetchStatesList).not.toHaveBeenCalled(); - expect(screen.queryByText('mockedLoading')).not.toBeInTheDocument(); - }); - it('should show a loader when states list is not fully fetched', async () => { - render(); + (useStatesList as jest.Mock).mockReturnValue({ + data: [], + isFetched: false, + }); + + renderComponent({}); expect(screen.getByText('mockedLoading')).toBeInTheDocument(); }); it('should render AddressDetails component and trigger buttons', async () => { - render(); + renderComponent({}); await waitFor(() => { svgCommonRenderCheck(); @@ -202,80 +207,26 @@ describe('', () => { }); }); - it('should render AddressDetails component not svg', async () => { - mock_props.is_svg = false; - - render(); - - expect(mock_props.onSubmitEnabledChange).toHaveBeenCalledTimes(1); - - const inputs: HTMLTextAreaElement[] = screen.getAllByRole('textbox'); - expect(inputs).toHaveLength(5); - const required_fields = inputs.filter(input => input.required === true); - expect(required_fields).toHaveLength(0); - - await waitFor(() => { - expect(screen.getByLabelText(address_line_1)).toBeInTheDocument(); - expect(screen.getByLabelText(address_line_2)).toBeInTheDocument(); - expect(screen.getByLabelText(address_postcode)).toBeInTheDocument(); - expect(screen.getByLabelText(address_state)).toBeInTheDocument(); - expect(screen.getByLabelText(address_town)).toBeInTheDocument(); - expect(screen.getByText(use_address_info)).toBeInTheDocument(); - }); - - expect(screen.queryByText(address_line_1_marked)).not.toBeInTheDocument(); - expect(screen.queryByText(address_line_2_marked)).not.toBeInTheDocument(); - expect(screen.queryByText(address_postcode_marked)).not.toBeInTheDocument(); - expect(screen.queryByText(address_town_marked)).not.toBeInTheDocument(); - expect(screen.queryByText(verification_info)).not.toBeInTheDocument(); - }); - - it('should render AddressDetails component for appstore', async () => { - render( - - - - ); - - expect(mock_props.onSubmitEnabledChange).toHaveBeenCalledTimes(1); - await waitFor(() => { - expect(screen.getByText(verification_info)).toBeInTheDocument(); - }); - expect(screen.queryByText(use_address_info)).not.toBeInTheDocument(); - - const inputs: HTMLTextAreaElement[] = screen.getAllByRole('textbox'); - expect(inputs).toHaveLength(5); - - const required_fields = inputs.filter(input => input.required === true); - expect(required_fields).toHaveLength(4); - - expect(screen.getByLabelText(address_line_1_marked)).toBeInTheDocument(); - expect(screen.getByLabelText(address_line_2_marked)).toBeInTheDocument(); - expect(screen.getByLabelText(address_postcode_marked)).toBeInTheDocument(); - expect(screen.getByLabelText(address_state)).toBeInTheDocument(); - expect(screen.getByLabelText(address_town_marked)).toBeInTheDocument(); - expect(screen.getByText(verification_info)).toBeInTheDocument(); - - expect(screen.queryByText(address_line_1)).not.toBeInTheDocument(); - expect(screen.queryByText(address_line_2)).not.toBeInTheDocument(); - expect(screen.queryByText(address_postcode)).not.toBeInTheDocument(); - expect(screen.queryByText(address_town)).not.toBeInTheDocument(); - expect(screen.queryByText(use_address_info)).not.toBeInTheDocument(); - }); - it('should render AddressDetails component with states_list for mobile', async () => { (isDesktop as jest.Mock).mockReturnValue(false); (isMobile as jest.Mock).mockReturnValue(true); - - mock_props.states_list = [ - { text: 'State 1', value: 'State 1' }, - { text: 'State 2', value: 'State 2' }, - ]; - - render(); + (useStatesList as jest.Mock).mockReturnValue({ + data: [ + { text: 'State 1', value: 'State 1' }, + { text: 'State 2', value: 'State 2' }, + ], + isFetched: true, + }); + const new_store_config: TStores = { + ...store, + ui: { + ...store.ui, + is_mobile: true, + }, + }; + renderComponent({ store_config: new_store_config }); expect(screen.getByText('Default test state')).toBeInTheDocument(); - const address_state_input: HTMLInputElement = screen.getByRole('combobox'); expect(address_state_input.value).toBe(''); fireEvent.change(address_state_input, { target: { value: 'State 2' } }); @@ -285,12 +236,14 @@ describe('', () => { }); it('should render AddressDetails component with states_list for desktop', async () => { - mock_props.states_list = [ - { text: 'State 1', value: 'State 1' }, - { text: 'State 2', value: 'State 2' }, - ]; - - render(); + (useStatesList as jest.Mock).mockReturnValue({ + data: [ + { text: 'State 1', value: 'State 1' }, + { text: 'State 2', value: 'State 2' }, + ], + isFetched: true, + }); + renderComponent({}); const address_state_input: HTMLTextAreaElement = screen.getByRole('textbox', { name: 'State/Province' }); expect(address_state_input).toHaveValue('Default test state'); @@ -300,18 +253,21 @@ describe('', () => { }); }); - it('should disable the field if it is immuatble from BE', async () => { - mock_props.disabled_items = ['address_line_1', 'address_line_2']; - mock_props.value.address_state = ''; + it('should disable the field if it is immutable from BE', async () => { + const new_props: React.ComponentProps = { + ...mock_props, + disabled_items: ['address_line_1', 'address_line_2'], + value: { ...mock_props.value, address_state: '' }, + }; - render(); + renderComponent({ props: new_props }); - expect(screen.getByLabelText(address_line_1)).toBeDisabled(); + expect(screen.getByLabelText(address_line_1_marked)).toBeDisabled(); expect(screen.getByLabelText(address_line_2)).toBeDisabled(); await waitFor(() => { expect(screen.getByRole('textbox', { name: 'State/Province' })).toBeEnabled(); }); - expect(screen.getByLabelText(address_town)).toBeEnabled(); + expect(screen.getByLabelText(address_town_marked)).toBeEnabled(); expect(screen.getByLabelText(address_postcode)).toBeEnabled(); }); }); diff --git a/packages/account/src/Components/address-details/address-details.tsx b/packages/account/src/Components/address-details/address-details.tsx index 0125c81be508..ccc23484d607 100644 --- a/packages/account/src/Components/address-details/address-details.tsx +++ b/packages/account/src/Components/address-details/address-details.tsx @@ -1,5 +1,15 @@ import React from 'react'; -import { Formik, Field, FormikProps, FormikValues } from 'formik'; +import classNames from 'classnames'; +import { + Formik, + Field, + FormikProps, + FormikValues, + FormikErrors, + FormikHelpers, + FormikHandlers, + FormikState, +} from 'formik'; import { StatesList } from '@deriv/api-types'; import { Modal, @@ -8,51 +18,46 @@ import { DesktopWrapper, Div100vhContainer, FormSubmitButton, - Input, Loading, MobileWrapper, ThemedScrollbars, SelectNative, Text, } from '@deriv/components'; +import { useStatesList } from '@deriv/hooks'; +import { getLocation } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; import { localize, Localize } from '@deriv/translations'; -import { isDesktop, isMobile, getLocation, makeCancellablePromise, PlatformContext } from '@deriv/shared'; +import { FormInputField } from '../forms/form-fields'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; -import classNames from 'classnames'; + +export type TAddressDetailFormProps = { + address_line_1: string; + address_line_2?: string; + address_city: string; + address_state?: string; + address_postcode?: string; +}; type TAddressDetails = { disabled_items: string[]; states_list: StatesList; getCurrentStep?: () => number; - onSave: (current_step: number, values: FormikValues) => void; + onSave: (current_step: number, values: TAddressDetailFormProps) => void; onCancel: (current_step: number, goToPreviousStep: () => void) => void; goToNextStep: () => void; goToPreviousStep: () => void; - validate: (values: FormikValues) => FormikValues; + validate: (values: TAddressDetailFormProps) => TAddressDetailFormProps; onSubmit: ( current_step: number | null, - values: FormikValues, + values: TAddressDetailFormProps, action: (isSubmitting: boolean) => void, next_step: () => void ) => void; - is_svg: boolean; - is_mf?: boolean; is_gb_residence: boolean | string; + selected_step_ref?: React.RefObject>; + value: TAddressDetailFormProps; has_real_account: boolean; - onSubmitEnabledChange: (is_submit_disabled: boolean) => void; - selected_step_ref?: React.RefObject>; - fetchStatesList: () => Promise; - value: FormikValues; -}; - -type TInputField = { - name: string; - required?: boolean | string; - label: string; - maxLength?: number | string; - placeholder: string; - onChange?: (e: any) => void; - disabled?: string; }; type TAutoComplete = { @@ -60,122 +65,104 @@ type TAutoComplete = { text: string; }; -const InputField = (props: TInputField) => { - return ( - - {({ field, form: { errors, touched } }: FormikValues) => ( - - - - )} - - ); -}; +/** + * Component to display address details form + * @name AddressDetails + * @param getCurrentStep - function to get current step + * @param states_list - array of states for the selected residence country + * @param onSave - function to save form values + * @param onCancel - function to cancel form values + * @param goToNextStep - function to go to next step + * @param goToPreviousStep - function to go to previous step + * @param validate - function to validate form values + * @param onSubmit - function to submit form values + * @param is_gb_residence - is residence Great Britan + * @param selected_step_ref - reference to selected step + * @param value - form values + * @param disabled_items - array of disabled fields + * @param has_real_account - has real account + * @returns react node + */ +const AddressDetails = observer( + ({ + getCurrentStep, + onSave, + onCancel, + goToNextStep, + goToPreviousStep, + validate, + onSubmit, + is_gb_residence, + selected_step_ref, + disabled_items, + has_real_account, + ...props + }: TAddressDetails) => { + const [address_state_to_display, setAddressStateToDisplay] = React.useState(''); -const AddressDetails = ({ - states_list, - getCurrentStep, - onSave, - onCancel, - goToNextStep, - goToPreviousStep, - validate, - onSubmit, - is_svg, - is_mf, - is_gb_residence, - onSubmitEnabledChange, - selected_step_ref, - disabled_items, - has_real_account, - ...props -}: TAddressDetails) => { - const { is_appstore } = React.useContext(PlatformContext); - const [has_fetched_states_list, setHasFetchedStatesList] = React.useState(false); - const [address_state_to_display, setAddressStateToDisplay] = React.useState(''); + const { + ui, + client: { residence }, + } = useStore(); - React.useEffect(() => { - let cancelFn: (() => void) | undefined; - if (states_list.length) { - setHasFetchedStatesList(true); - } else { - const { cancel, promise } = makeCancellablePromise(props.fetchStatesList()); - cancelFn = cancel; - promise.then(() => { - setHasFetchedStatesList(true); - if (props.value?.address_state) { - setAddressStateToDisplay(getLocation(states_list, props.value?.address_state, 'text')); - } - }); - } - return () => { - setHasFetchedStatesList(false); - if (cancelFn) { - cancelFn(); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const is_submit_disabled_ref = React.useRef(true); - - const isSubmitDisabled = (errors?: { [key: string]: string } | FormikValues) => { - return selected_step_ref?.current?.isSubmitting || (errors && Object.keys(errors).length > 0); - }; + const { is_desktop, is_mobile } = ui; + const { data: states_list, isFetched } = useStatesList(residence); - const checkSubmitStatus = (errors?: { [key: string]: string } | FormikValues) => { - const is_submit_disabled = isSubmitDisabled(errors); + const isSubmitDisabled = (errors: FormikErrors = {}): boolean => { + const is_submitting = selected_step_ref?.current?.isSubmitting ?? false; + return is_submitting || Object.keys(errors).length > 0; + }; - if (is_submit_disabled_ref.current !== is_submit_disabled) { - is_submit_disabled_ref.current = is_submit_disabled; - onSubmitEnabledChange?.(!is_submit_disabled); - } - }; + const handleCancel = (values: TAddressDetailFormProps) => { + const current_step = (getCurrentStep?.() || 1) - 1; + onSave(current_step, values); + onCancel(current_step, goToPreviousStep); + }; - const handleCancel = (values: FormikValues) => { - const current_step = (getCurrentStep?.() || 1) - 1; - onSave(current_step, values); - onCancel(current_step, goToPreviousStep); - }; + const handleValidate = (values: TAddressDetailFormProps) => { + const { errors } = splitValidationResultTypes(validate(values)); + return errors; + }; - const handleValidate = (values: FormikValues) => { - const { errors } = splitValidationResultTypes(validate(values)); - checkSubmitStatus(errors); - return errors; - }; + const handleSubmitData = (values: TAddressDetailFormProps, actions: FormikHelpers) => { + if (values.address_state && states_list.length) { + values.address_state = address_state_to_display + ? getLocation(states_list, address_state_to_display, 'value') + : getLocation(states_list, values.address_state, 'value'); + } + onSubmit((getCurrentStep?.() || 1) - 1, values, actions.setSubmitting, goToNextStep); + }; - return ( - { - if (values.address_state && states_list.length) { - values.address_state = address_state_to_display - ? getLocation(states_list, address_state_to_display, 'value') - : getLocation(states_list, values.address_state, 'value'); - } - onSubmit((getCurrentStep?.() || 1) - 1, values, actions.setSubmitting, goToNextStep); - }} - > - {({ handleSubmit, errors, values, setFieldValue, handleChange, setFieldTouched }: FormikValues) => ( - - {({ setRef, height }: { setRef: (instance: HTMLFormElement) => void; height: number | string }) => ( -
- - {!is_appstore && ( + return ( + + {({ + handleSubmit, + errors, + values, + setFieldValue, + handleChange, + setFieldTouched, + }: FormikHandlers & FormikHelpers & FormikState) => ( + + {({ + setRef, + height, + }: { + setRef: (instance: HTMLFormElement) => void; + height: number | string; + }) => ( + + - )} - - {is_appstore && ( -
- - {localize( - 'We need this for verification. If the information you provide is fake or inaccurate, you won’t be able to deposit and withdraw.' - )} - -
- )} -
- - - - {!has_fetched_states_list && ( -
- -
- )} - {states_list?.length > 0 ? ( - - {({ field }: FormikValues) => ( - <> - - { - setFieldValue( - 'address_state', - value ? text : '', - true - ); - setAddressStateToDisplay(''); - }} - list_portal_id={is_appstore ? '' : 'modal_root'} - disabled={ - disabled_items.includes('address_state') || - (props.value?.address_state && has_real_account) - } - /> - - - { - setFieldValue( - 'address_state', - e.target.value, - true - ); - setAddressStateToDisplay(''); - }} - disabled={ - disabled_items.includes('address_state') || - (props.value?.address_state && has_real_account) - } - /> - - - )} - - ) : ( - // Fallback to input field when states list is empty / unavailable for country - +
+ - )} - { - setFieldTouched('address_postcode', true); - handleChange(e); - }} - disabled={ - disabled_items.includes('address_postcode') || - (props.value?.address_postcode && has_real_account) - } - /> -
- - - - handleCancel(values)} - /> - - - )} - - )} - - ); -}; + + + {!isFetched && ( +
+ +
+ )} + {states_list?.length > 0 ? ( + + {({ field }: FormikValues) => ( + + + { + setFieldValue( + 'address_state', + value ? text : '', + true + ); + setAddressStateToDisplay(''); + }} + list_portal_id='modal_root' + disabled={ + disabled_items.includes('address_state') || + (props.value?.address_state && has_real_account) + } + /> + + + { + setFieldValue( + 'address_state', + e.target.value, + true + ); + setAddressStateToDisplay(''); + }} + disabled={ + disabled_items.includes('address_state') || + (props.value?.address_state && has_real_account) + } + /> + + + )} + + ) : ( + // Fallback to input field when states list is empty / unavailable for country + + )} + { + setFieldTouched('address_postcode', true); + handleChange(e); + }} + disabled={ + disabled_items.includes('address_postcode') || + (props.value?.address_postcode && has_real_account) + } + /> +
+
+
+ + handleCancel(values)} + /> + + + )} +
+ )} +
+ ); + } +); export default AddressDetails; diff --git a/packages/account/src/Components/currency-selector/__tests__/currency-selector.spec.tsx b/packages/account/src/Components/currency-selector/__tests__/currency-selector.spec.tsx index f5342532bb5f..06153456282c 100644 --- a/packages/account/src/Components/currency-selector/__tests__/currency-selector.spec.tsx +++ b/packages/account/src/Components/currency-selector/__tests__/currency-selector.spec.tsx @@ -1,13 +1,8 @@ import React from 'react'; import { fireEvent, screen, render, waitFor } from '@testing-library/react'; -import { isDesktop, isMobile, PlatformContext } from '@deriv/shared'; -import CurrencySelector, { TCurrencySelector } from '../currency-selector'; - -jest.mock('@deriv/shared', () => ({ - ...jest.requireActual('@deriv/shared'), - isDesktop: jest.fn().mockReturnValue(false), - isMobile: jest.fn().mockReturnValue(false), -})); +import { StoreProvider, mockStore } from '@deriv/stores'; +import CurrencySelector from '../currency-selector'; +import { TStores } from '@deriv/stores/types'; jest.mock('../../real-account-signup/helpers/utils.ts', () => ({ splitValidationResultTypes: jest.fn(() => ({ @@ -17,197 +12,22 @@ jest.mock('../../real-account-signup/helpers/utils.ts', () => ({ })); describe('', () => { - const props: TCurrencySelector = { - accounts: { - VRTC90000010: { - account_type: 'trading', - currency: 'USD', - is_disabled: 0, - is_virtual: 1, - landing_company_shortcode: 'virtual', - trading: {}, - token: '', - email: '', - session_start: 1651059038, - excluded_until: '', - landing_company_name: 'virtual', - residence: 'es', - balance: 10000, - accepted_bch: 0, - }, - }, - legal_allowed_currencies: [ - { - value: 'EUR', - fractional_digits: 2, - is_deposit_suspended: 0, - is_suspended: 0, - is_withdrawal_suspended: 0, - name: 'Euro', - stake_default: 10, - transfer_between_accounts: { - fees: { - AUD: 0, - }, - limits: { - max: 4717.96, - min: 0.94, - }, - limits_dxtrade: { - max: 2358.45, - min: 0.01, - }, - limits_mt5: { - max: 14150.68, - min: 0.01, - }, - }, - type: 'fiat', - }, - { - value: 'USD', - fractional_digits: 2, - is_deposit_suspended: 0, - is_suspended: 0, - is_withdrawal_suspended: 0, - name: 'US Dollar', - stake_default: 10, - transfer_between_accounts: { - fees: { - AUD: 0, - }, - limits: { - max: 5000, - min: 1, - }, - limits_dxtrade: { - max: 2500, - min: 0.01, - }, - limits_mt5: { - max: 15000, - min: 0.01, - }, - }, - type: 'fiat', - }, - { - value: 'USDC', - fractional_digits: 2, - is_deposit_suspended: 0, - is_suspended: 0, - is_withdrawal_suspended: 0, - name: 'USD Coin', - stake_default: 10, - transfer_between_accounts: { - fees: { - AUD: 2, - }, - limits: { - max: 5001.52, - min: 1, - }, - limits_dxtrade: { - max: 2500.76, - min: 0.01, - }, - limits_mt5: { - max: 15004.55, - min: 0.01, - }, - }, - type: 'crypto', - }, - { - value: 'eUSDT', - fractional_digits: 2, - is_deposit_suspended: 0, - is_suspended: 0, - is_withdrawal_suspended: 0, - name: 'Tether ERC20', - stake_default: 10, - transfer_between_accounts: { - fees: { - AUD: 2, - }, - limits: { - max: 5001.78, - min: 1, - }, - limits_dxtrade: { - max: 2500.89, - min: 0.01, - }, - limits_mt5: { - max: 15005.33, - min: 0.01, - }, - }, - type: 'crypto', - }, - ], - has_fiat: true, + const mock_props: React.ComponentProps = { value: { currency: '', }, validate: jest.fn(), is_virtual: true, - available_crypto_currencies: [ - { - value: 'eUSDT', - fractional_digits: 2, - is_deposit_suspended: 0, - is_suspended: 0, - is_withdrawal_suspended: 0, - name: 'Tether ERC20', - stake_default: 10, - transfer_between_accounts: { - fees: { - AUD: 2, - }, - limits: { - max: 5001.78, - min: 1, - }, - limits_dxtrade: { - max: 2500.89, - min: 0.01, - }, - limits_mt5: { - max: 15005.33, - min: 0.01, - }, - }, - type: 'crypto', - }, - ], getCurrentStep: jest.fn(() => 1), onSave: jest.fn(), onCancel: jest.fn(), - real_account_signup: { - active_modal_index: -1, - previous_currency: '', - current_currency: '', - success_message: '', - error_message: '', - error_code: 2, - }, goToNextStep: jest.fn(), goToStep: jest.fn(), - resetRealAccountSignupParams: jest.fn(), onSubmit: jest.fn(), goToPreviousStep: jest.fn(), has_cancel: false, - has_currency: false, - has_real_account: false, has_wallet_account: false, - is_appstore: false, - is_dxtrade_allowed: false, - is_eu: false, - is_mt5_allowed: false, set_currency: false, - onSubmitEnabledChange: jest.fn(), - real_account_signup_target: '', }; const fiat_msg = @@ -221,7 +41,7 @@ describe('', () => { const mt5_non_eu = 'You are limited to one fiat account. You won’t be able to change your account currency if you have already made your first deposit or created a real Deriv MT5 account.'; - const runCommonTests = msg => { + const runCommonTests = (msg: string) => { expect(screen.getByRole('heading', { name: /fiat currencies/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /us dollar \(usd\)/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /euro \(eur\)/i })).toBeInTheDocument(); @@ -241,78 +61,302 @@ describe('', () => { expect(screen.getByText(msg)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /next/i })).toBeEnabled(); }; + const store = mockStore({ + client: { + accounts: { + VRTC90000010: { + account_type: 'trading', + currency: 'USD', + is_disabled: 0, + is_virtual: 1, + landing_company_shortcode: 'virtual', + trading: {}, + token: '', + email: '', + session_start: 1651059038, + landing_company_name: 'virtual', + residence: 'es', + balance: 10000, + accepted_bch: 0, + }, + }, + upgradeable_currencies: [ + { + value: 'EUR', + fractional_digits: 2, + is_deposit_suspended: 0, + is_suspended: 0, + is_withdrawal_suspended: 0, + name: 'Euro', + stake_default: 10, + transfer_between_accounts: { + fees: { + AUD: 0, + }, + limits: { + max: 4717.96, + min: 0.94, + }, + limits_dxtrade: { + max: 2358.45, + min: 0.01, + }, + limits_mt5: { + max: 14150.68, + min: 0.01, + }, + }, + type: 'fiat', + }, + { + value: 'USD', + fractional_digits: 2, + is_deposit_suspended: 0, + is_suspended: 0, + is_withdrawal_suspended: 0, + name: 'US Dollar', + stake_default: 10, + transfer_between_accounts: { + fees: { + AUD: 0, + }, + limits: { + max: 5000, + min: 1, + }, + limits_dxtrade: { + max: 2500, + min: 0.01, + }, + limits_mt5: { + max: 15000, + min: 0.01, + }, + }, + type: 'fiat', + }, + { + value: 'USDC', + fractional_digits: 2, + is_deposit_suspended: 0, + is_suspended: 0, + is_withdrawal_suspended: 0, + name: 'USD Coin', + stake_default: 10, + transfer_between_accounts: { + fees: { + AUD: 2, + }, + limits: { + max: 5001.52, + min: 1, + }, + limits_dxtrade: { + max: 2500.76, + min: 0.01, + }, + limits_mt5: { + max: 15004.55, + min: 0.01, + }, + }, + type: 'crypto', + }, + { + value: 'eUSDT', + fractional_digits: 2, + is_deposit_suspended: 0, + is_suspended: 0, + is_withdrawal_suspended: 0, + name: 'Tether ERC20', + stake_default: 10, + transfer_between_accounts: { + fees: { + AUD: 2, + }, + limits: { + max: 5001.78, + min: 1, + }, + limits_dxtrade: { + max: 2500.89, + min: 0.01, + }, + limits_mt5: { + max: 15005.33, + min: 0.01, + }, + }, + type: 'crypto', + }, + ], + available_crypto_currencies: [ + { + value: 'eUSDT', + fractional_digits: 2, + is_deposit_suspended: 0, + is_suspended: 0, + is_withdrawal_suspended: 0, + name: 'Tether ERC20', + stake_default: 10, + transfer_between_accounts: { + fees: { + AUD: 2, + }, + limits: { + max: 5001.78, + min: 1, + }, + limits_dxtrade: { + max: 2500.89, + min: 0.01, + }, + limits_mt5: { + max: 15005.33, + min: 0.01, + }, + }, + type: 'crypto', + }, + ], + }, + ui: { + real_account_signup: { + active_modal_index: -1, + previous_currency: '', + current_currency: '', + success_message: '', + error_message: '', + error_code: '2', + }, + }, + }); + + const renderComponent = ({ props = mock_props, store_config = store }) => { + return render( + + + + ); + }; + + it('should render Currency selector', () => { + renderComponent({}); - it('should render currencyselector', () => { - render(); expect(screen.getByTestId('currency_selector_form')).toBeInTheDocument(); }); it('should render Fiat currencies and submit the form', async () => { - render(); + renderComponent({}); + runCommonTests(fiat_msg); fireEvent.click(screen.getByRole('button', { name: /next/i })); await waitFor(() => { - expect(props.onSubmit).toHaveBeenCalled(); - expect(props.onSubmit).toHaveBeenCalledWith( + expect(mock_props.onSubmit).toHaveBeenCalled(); + expect(mock_props.onSubmit).toHaveBeenCalledWith( 0, { currency: 'USD' }, expect.any(Function), - props.goToNextStep + mock_props.goToNextStep ); }); }); it('should disable fiat if user already have a fiat ', () => { - const new_props = { - ...props, - accounts: { - VRTC90000010: { - account_type: 'trading', - currency: 'USD', - is_disabled: 0, - is_virtual: 1, - landing_company_shortcode: 'svg', - trading: {}, - token: '', - email: '', - session_start: 1651059038, - excluded_until: '', - landing_company_name: 'svg', - residence: 'es', - balance: 10000, - accepted_bch: 0, + const new_store: TStores = { + ...store, + client: { + ...store.client, + accounts: { + VRTC90000010: { + account_type: 'trading', + currency: 'USD', + is_disabled: 0, + is_virtual: 1, + landing_company_shortcode: 'svg', + trading: {}, + token: '', + email: '', + session_start: 1651059038, + excluded_until: '', + landing_company_name: 'svg', + residence: 'es', + balance: 10000, + accepted_bch: 0, + }, }, + has_active_real_account: true, + has_fiat: true, + }, + ui: { + ...store.ui, + real_account_signup_target: 'svg', }, - has_real_account: true, - real_account_signup_target: 'svg', }; - render(); + renderComponent({ store_config: new_store }); expect(screen.getByRole('radio', { name: /us dollar \(usd\)/i })).toBeDisabled(); expect(screen.getByRole('radio', { name: /euro \(eur\)/i })).toBeDisabled(); }); + it('should render Fiat currencies when is_dxtrade_allowed and is_mt5_allowed are true', () => { - render(); + const new_store = { + ...store, + client: { + ...store.client, + is_dxtrade_allowed: true, + is_mt5_allowed: true, + }, + }; + renderComponent({ store_config: new_store }); runCommonTests(dxtrade_non_eu_msg); }); it('should render Fiat currencies when is_dxtrade_allowed,is_eu and is_mt5_allowed are true', () => { - render(); + const new_store: TStores = { + ...store, + client: { + ...store.client, + is_dxtrade_allowed: true, + is_mt5_allowed: true, + is_eu: true, + }, + }; + renderComponent({ store_config: new_store }); runCommonTests(dxtrade_eu_msg); }); it('should render Fiat currencies when is_mt5_allowed and is_eu are true', () => { - render(); + const new_store: TStores = { + ...store, + client: { + ...store.client, + is_mt5_allowed: true, + is_eu: true, + }, + }; + renderComponent({ store_config: new_store }); runCommonTests(mt5_eu); }); it('should render Fiat currencies when is_mt5_allowed is true', () => { - render(); + const new_store: TStores = { + ...store, + client: { + ...store.client, + is_mt5_allowed: true, + }, + }; + renderComponent({ store_config: new_store }); runCommonTests(mt5_non_eu); }); it('should render Cryptocurrencies and submit the form ', async () => { - render(); + const new_props: React.ComponentProps = { + ...mock_props, + set_currency: true, + }; + renderComponent({ props: new_props }); expect(screen.getByRole('heading', { name: /cryptocurrencies/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /tether erc20 \(eusdt\)/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /usd coin \(usdc\)/i })).toBeInTheDocument(); @@ -332,57 +376,54 @@ describe('', () => { fireEvent.click(set_currency_btn); await waitFor(() => { - expect(props.onSubmit).toHaveBeenCalledWith( + expect(mock_props.onSubmit).toHaveBeenCalledWith( 0, { currency: 'eUSDT' }, expect.any(Function), - props.goToNextStep + mock_props.goToNextStep ); }); }); it('should submit the form when getCurrentStep is not passed ', async () => { - const new_props: TCurrencySelector = { ...props }; - render(); + renderComponent({}); runCommonTests(fiat_msg); fireEvent.click(screen.getByRole('button', { name: /next/i })); await waitFor(() => { - expect(props.onSubmit).toHaveBeenCalled(); - }); - }); - - it('should render the selector__container with proper div height when appstore is true', () => { - (isMobile as jest.Mock).mockReturnValue(true); - (isDesktop as jest.Mock).mockReturnValue(false); - Object.defineProperty(window, 'innerHeight', { - writable: true, - configurable: true, - value: 150, + expect(mock_props.onSubmit).toHaveBeenCalled(); }); - render( - - - - ); - - expect(screen.getByTestId('currency_selector_form').childNodes[0]).toHaveStyle('height: calc(150px - 222px);'); }); it('should render the selector__container with proper div height', () => { - (isMobile as jest.Mock).mockReturnValue(true); - (isDesktop as jest.Mock).mockReturnValue(false); Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 150, }); - render(); + const new_store = { + ...store, + client: { + ...store.client, + has_active_real_account: true, + }, + ui: { + ...store.ui, + is_desktop: false, + is_mobile: true, + }, + }; + renderComponent({ store_config: new_store }); expect(screen.getByTestId('currency_selector_form').childNodes[0]).toHaveStyle('height: calc(150px - 89px);'); }); it('should call handleCancel when previous button is called', () => { - render(); + const new_props: React.ComponentProps = { + ...mock_props, + has_wallet_account: true, + has_cancel: true, + }; + renderComponent({ props: new_props }); const usdc: HTMLInputElement = screen.getByRole('radio', { name: /usd coin \(usdc\)/i }); expect(usdc.checked).toEqual(false); @@ -393,28 +434,39 @@ describe('', () => { const prev_btn = screen.getByRole('button', { name: /previous/i }); expect(prev_btn).toBeInTheDocument(); fireEvent.click(prev_btn); - expect(props.onSave).toHaveBeenCalledWith(0, { currency: 'USDC' }); + expect(mock_props.onSave).toHaveBeenCalledWith(0, { currency: 'USDC' }); }); it('should bypass to next step in case of personal details form error', () => { - const real_account_signup = { - ...props.real_account_signup, - error_details: { first_name: 'numbers not allowed' }, + const new_store: TStores = { + ...store, + ui: { + ...store.ui, + real_account_signup: { + ...store.ui.real_account_signup, + error_details: { first_name: 'numbers not allowed' }, + }, + }, }; + renderComponent({ store_config: new_store }); - render(); - expect(props.goToNextStep).toHaveBeenCalled(); - expect(props.resetRealAccountSignupParams).toHaveBeenCalled(); + expect(mock_props.goToNextStep).toHaveBeenCalled(); + expect(store.ui.resetRealAccountSignupParams).toHaveBeenCalled(); }); it('should bypass to address step in case of address details form error', () => { - const real_account_signup = { - ...props.real_account_signup, - error_details: { address_line_1: 'po box is not allowed' }, + const new_store: TStores = { + ...store, + ui: { + ...store.ui, + real_account_signup: { + ...store.ui.real_account_signup, + error_details: { address_line_1: 'po box is not allowed' }, + }, + }, }; - - render(); - expect(props.goToStep).toHaveBeenCalledWith(3); - expect(props.resetRealAccountSignupParams).toHaveBeenCalled(); + renderComponent({ store_config: new_store }); + expect(mock_props.goToStep).toHaveBeenCalledWith(3); + expect(store.ui.resetRealAccountSignupParams).toHaveBeenCalled(); }); }); diff --git a/packages/account/src/Components/currency-selector/__tests__/ust-popover.spec.tsx b/packages/account/src/Components/currency-selector/__tests__/ust-popover.spec.tsx new file mode 100644 index 000000000000..c19deee7262d --- /dev/null +++ b/packages/account/src/Components/currency-selector/__tests__/ust-popover.spec.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import USTPopover from '../ust-popover'; + +jest.mock('@deriv/components', () => ({ + ...jest.requireActual('@deriv/components'), + Popover: jest.fn(props => {props.message}), +})); + +describe('', () => { + it('should render USTPopover with UST info', () => { + render(); + + expect(screen.getByText(/Tether as an Omni token \(USDT\)/i)).toBeInTheDocument(); + }); + + it('should render USTPopover with tUSDT info', () => { + render(); + + expect(screen.getByText(/Tether as a TRC20 token \(tUSDT\)/i)).toBeInTheDocument(); + }); + + it('should render USTPopover with default info', () => { + render(); + + expect(screen.getByText(/Tether as an ERC20 token \(eUSDT\)/i)).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/currency-selector/currency-selector.tsx b/packages/account/src/Components/currency-selector/currency-selector.tsx index 37d9b0b89264..c24a372da174 100644 --- a/packages/account/src/Components/currency-selector/currency-selector.tsx +++ b/packages/account/src/Components/currency-selector/currency-selector.tsx @@ -1,294 +1,313 @@ +import React from 'react'; import classNames from 'classnames'; -import React, { HTMLAttributes, RefObject } from 'react'; -import { Field, Formik, FormikHandlers, FormikProps, FormikState, FormikValues } from 'formik'; +import { Field, Formik, FormikHandlers, FormikProps, FormikState } from 'formik'; +import { WebsiteStatus } from '@deriv/api-types'; import { AutoHeightWrapper, FormSubmitButton, Div100vhContainer, Modal, ThemedScrollbars } from '@deriv/components'; -import { - getPlatformSettings, - isMobile, - isDesktop, - reorderCurrencies, - PlatformContext, - getAddressDetailsFields, -} from '@deriv/shared'; +import { getPlatformSettings, reorderCurrencies, getAddressDetailsFields } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; import { localize, Localize } from '@deriv/translations'; -import RadioButtonGroup from './radio-button-group'; import RadioButton from './radio-button'; +import RadioButtonGroup from './radio-button-group'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; -import { TAuthAccountInfo, TCurrencyConfig, TRealAccount, TFormValidation } from 'Types'; export const Hr = () =>
; +const CURRENCY_TYPE: Record = { + CRYPTO: 'crypto', + FIAT: 'fiat', +}; + +export type TCurrencySelectorFormProps = { + currency: string; +}; + type TCurrencySelectorExtend = { - accounts: { [key: string]: TAuthAccountInfo }; - available_crypto_currencies: TCurrencyConfig[]; getCurrentStep: () => number; goToNextStep: () => void; goToStep: (step: number) => void; goToPreviousStep: () => void; has_cancel: boolean; - has_currency: boolean; - has_fiat: boolean; - has_real_account: boolean; has_wallet_account: boolean; - is_appstore: boolean; - is_dxtrade_allowed: boolean; - is_eu: boolean; is_virtual: boolean; - is_mt5_allowed: boolean; - legal_allowed_currencies: TCurrencyConfig[]; onCancel: (current_step: number, goToPreviousStep: () => void) => void; - onSave: (current_step: number, values: FormikValues) => void; + onSave: (current_step: number, values: TCurrencySelectorFormProps) => void; onSubmit: ( current_step: number | null, - values: FormikValues, + values: TCurrencySelectorFormProps, action: (isSubmitting: boolean) => void, next_step: () => void ) => void; - onSubmitEnabledChange: (is_submit_disabled: boolean) => void; - real_account_signup: TRealAccount; - real_account_signup_target: string; - resetRealAccountSignupParams: () => void; - selected_step_ref?: RefObject>; + selected_step_ref?: React.RefObject>; set_currency: boolean; - validate: (values: FormikValues) => FormikValues; - value: FormikValues; + validate: (values: TCurrencySelectorFormProps) => TCurrencySelectorFormProps; + value: TCurrencySelectorFormProps; }; -type TCurrencySelector = HTMLAttributes & TCurrencySelectorExtend; +type TCurrencySelector = React.HTMLAttributes & TCurrencySelectorExtend; -const CurrencySelector = ({ - getCurrentStep, - goToNextStep, - goToStep, - has_currency, - has_real_account, - legal_allowed_currencies, - onSubmit, - onSave, - onCancel, - goToPreviousStep, - real_account_signup, - real_account_signup_target, - resetRealAccountSignupParams, - set_currency, - validate, - has_cancel = false, - selected_step_ref, - onSubmitEnabledChange, - has_wallet_account, - is_dxtrade_allowed, - is_mt5_allowed, - available_crypto_currencies, - has_fiat, - accounts, - is_eu, - ...props -}: TCurrencySelector) => { - const { is_appstore } = React.useContext(PlatformContext); - const crypto = legal_allowed_currencies.filter((currency: TCurrencyConfig) => currency.type === 'crypto'); - const fiat = legal_allowed_currencies.filter((currency: TCurrencyConfig) => currency.type === 'fiat'); - const [is_bypass_step, setIsBypassStep] = React.useState(false); - const is_submit_disabled_ref = React.useRef(true); +/** + * Currency selector component to select the Account currency + * @name CurrencySelector + * @param getCurrentStep - Get the current step + * @param goToNextStep - Go to the next step + * @param goToStep - Go to a specific step + * @param goToPreviousStep - Go to the previous step + * @param has_cancel - Has cancel button + * @param has_wallet_account - Has wallet account + * @param is_virtual - Is virtual account + * @param onCancel - To handle click on cancel button + * @param onSave - To handle click on save button + * @param onSubmit - To handle click on submit button + * @param selected_step_ref - Ref of the selected step + * @param set_currency - Is current set + * @param alidate - To validate the form + * @param alue - Value of the form + * @returns React node + */ +const CurrencySelector = observer( + ({ + getCurrentStep, + goToNextStep, + goToStep, + onSubmit, + onSave, + onCancel, + goToPreviousStep, + set_currency, + validate, + has_cancel = false, + selected_step_ref, + has_wallet_account, + value, + }: TCurrencySelector) => { + const { client, ui } = useStore(); + + const { + currency, + has_active_real_account: has_real_account, + upgradeable_currencies: legal_allowed_currencies, + available_crypto_currencies, + is_dxtrade_allowed, + is_mt5_allowed, + has_fiat, + accounts, + is_eu, + } = client; + + const has_currency = Boolean(currency); + + const { real_account_signup, real_account_signup_target, resetRealAccountSignupParams, is_desktop, is_mobile } = + ui; + + // Wrapped with String() to avoid type mismatch + const crypto = legal_allowed_currencies.filter( + selected_currency => String(selected_currency.type) === CURRENCY_TYPE.CRYPTO + ); - const should_disable_fiat = !!Object.values(accounts).filter( - item => item.landing_company_shortcode === real_account_signup_target - ).length; + // Wrapped with String() to avoid type mismatch + const fiat = legal_allowed_currencies.filter( + selected_currency => String(selected_currency.type) === CURRENCY_TYPE.FIAT + ); + const [is_bypass_step, setIsBypassStep] = React.useState(false); - const isSubmitDisabled = (values: FormikValues) => { - return selected_step_ref?.current?.isSubmitting || !values.currency; - }; + const should_disable_fiat = !!Object.values(accounts).filter( + item => item.landing_company_shortcode === real_account_signup_target + ).length; - const checkSubmitStatus = (values: FormikValues) => { - const is_submit_disabled = isSubmitDisabled(values); + const isSubmitDisabled = (values: TCurrencySelectorFormProps) => { + return selected_step_ref?.current?.isSubmitting || !values.currency; + }; - if (is_submit_disabled_ref.current !== is_submit_disabled) { - is_submit_disabled_ref.current = is_submit_disabled; - onSubmitEnabledChange?.(!is_submit_disabled); - } - }; + const handleCancel = (values: TCurrencySelectorFormProps) => { + const current_step = getCurrentStep() - 1; + onSave(current_step, values); + onCancel(current_step, goToPreviousStep); + }; - const handleCancel = (values: FormikValues) => { - const current_step = getCurrentStep() - 1; - onSave(current_step, values); - onCancel(current_step, goToPreviousStep); - }; + const handleValidate = (values: TCurrencySelectorFormProps) => { + const { errors } = splitValidationResultTypes(validate(values)); + return errors; + }; - const handleValidate = (values: FormikValues) => { - checkSubmitStatus(values); - const { errors }: Partial = splitValidationResultTypes(validate(values)); - return errors; - }; + // In case of form error bypass to update personal data + React.useEffect(() => { + if (real_account_signup?.error_code) { + setIsBypassStep(true); + } + }, [real_account_signup?.error_code]); - // In case of form error bypass to update personal data - React.useEffect(() => { - if (real_account_signup?.error_code) { - setIsBypassStep(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + React.useEffect(() => { + if (is_bypass_step && real_account_signup?.error_details) { + const keys = Object.keys(real_account_signup?.error_details); + const route_to_address_details = Object.keys(getAddressDetailsFields()).filter(item => + keys.includes(item) + ); + if (route_to_address_details?.length > 0) { + goToStep(3); + } else { + goToNextStep(); + } + resetRealAccountSignupParams(); + setIsBypassStep(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [is_bypass_step]); - React.useEffect(() => { - if (is_bypass_step && real_account_signup?.error_details) { - const keys = Object.keys(real_account_signup?.error_details); - const route_to_address_details = Object.keys(getAddressDetailsFields()).filter(item => keys.includes(item)); - if (route_to_address_details?.length > 0) goToStep(3); - else { - goToNextStep(); + const getHeightOffset = () => { + if (!has_currency && has_real_account) { + return '89px'; } - resetRealAccountSignupParams(); - setIsBypassStep(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [is_bypass_step]); + return '159px'; + }; - const getHeightOffset = () => { - if (is_appstore) { - return '222px'; - } else if (!has_currency && has_real_account) { - return '89px'; - } - return '159px'; - }; + const getSubmitLabel = () => { + if (set_currency) { + return localize('Set currency'); + } else if (has_wallet_account) { + return localize('Finish'); + } + return localize('Next'); + }; - const getSubmitLabel = () => { - if (set_currency) { - return localize('Set currency'); - } else if (has_wallet_account) { - return localize('Finish'); - } - return localize('Next'); - }; + const description = React.useMemo(() => { + const dmt5_label = is_eu ? localize('CFDs') : localize('Deriv MT5'); + const platform_name_dxtrade = getPlatformSettings('dxtrade').name; - const description = React.useMemo(() => { - const dmt5_label = is_eu ? localize('CFDs') : localize('Deriv MT5'); - const platform_name_dxtrade = getPlatformSettings('dxtrade').name; + if (is_dxtrade_allowed && is_mt5_allowed) { + return ( + + ); + } else if (!is_dxtrade_allowed && is_mt5_allowed) { + return ( + + ); + } - if (is_dxtrade_allowed && is_mt5_allowed) { return ( - + ); - } else if (!is_dxtrade_allowed && is_mt5_allowed) { - return ( - - ); - } + }, [is_eu, is_dxtrade_allowed, is_mt5_allowed]); return ( - - ); - }, [is_eu, is_dxtrade_allowed, is_mt5_allowed]); - - return ( - { - onSubmit(getCurrentStep ? getCurrentStep() - 1 : null, values, actions.setSubmitting, goToNextStep); - }} - validate={handleValidate} - > - {({ handleSubmit, values }: FormikState & FormikHandlers) => ( - - {({ setRef, height }: { setRef: (instance: HTMLFormElement | null) => void; height: number }) => ( -
- { + onSubmit(getCurrentStep ? getCurrentStep() - 1 : null, values, actions.setSubmitting, goToNextStep); + }} + validate={handleValidate} + > + {({ handleSubmit, values }: FormikState & FormikHandlers) => ( + + {({ + setRef, + height, + }: { + setRef: (instance: HTMLFormElement | null) => void; + height: number; + }) => ( + - - {!!reorderCurrencies(fiat)?.length && ( - - - {reorderCurrencies(fiat).map((currency: FormikValues) => ( - - ))} - - {!!reorderCurrencies(crypto, 'crypto')?.length &&
} -
- )} - {!!reorderCurrencies(crypto, 'crypto')?.length && ( - - - {reorderCurrencies(crypto, 'crypto').map((currency: FormikValues) => ( - value === currency.value - )?.length === 0 - } - name='currency' - id={currency.value} - label={currency.name} - /> - ))} - - - )} -
-
- - handleCancel(values), - } - : {})} - /> - -
- )} -
- )} -
- ); -}; -export type { TCurrencySelector }; + + + {!!fiat?.length && ( + + + {reorderCurrencies(fiat).map( + (avbl_currency: WebsiteStatus['currencies_config']) => ( + + ) + )} + + {!!reorderCurrencies(crypto, 'crypto')?.length &&
} +
+ )} + {!!reorderCurrencies(crypto, 'crypto')?.length && ( + + + {reorderCurrencies(crypto, 'crypto').map(avbl_currency => ( + + crypto_data.value === avbl_currency.value + )?.length === 0 + } + name='currency' + id={avbl_currency.value} + label={avbl_currency.name} + /> + ))} + + + )} +
+
+ + handleCancel(values), + } + : {})} + /> + + + )} + + )} + + ); + } +); export default CurrencySelector; diff --git a/packages/account/src/Components/currency-selector/index.js b/packages/account/src/Components/currency-selector/index.ts similarity index 100% rename from packages/account/src/Components/currency-selector/index.js rename to packages/account/src/Components/currency-selector/index.ts diff --git a/packages/account/src/Components/currency-selector/radio-button-group.tsx b/packages/account/src/Components/currency-selector/radio-button-group.tsx index 31ae2ab10ead..239c7b4000ad 100644 --- a/packages/account/src/Components/currency-selector/radio-button-group.tsx +++ b/packages/account/src/Components/currency-selector/radio-button-group.tsx @@ -13,10 +13,22 @@ export type TRadioButtonGroup = { has_fiat?: boolean; }; +/** + * Wrapper component for RadioButton + * @name RadioButtonGroup + * @param {string} className - class name for styling + * @param {boolean} is_fiat - is fiat currency + * @param {boolean} is_title_enabled - is title enabled + * @param {number} item_count - number of items + * @param {string} label - label for the radio button + * @param {React.ReactNode} description - description for the radio button + * @param {boolean} has_fiat - has fiat currency + * @returns {React.ReactNode} - returns a React node + */ const RadioButtonGroup = ({ + children, label, className, - children, is_title_enabled = true, is_fiat, item_count, diff --git a/packages/account/src/Components/currency-selector/radio-button.tsx b/packages/account/src/Components/currency-selector/radio-button.tsx index 903646d0dfe4..ccc2fd2110a0 100644 --- a/packages/account/src/Components/currency-selector/radio-button.tsx +++ b/packages/account/src/Components/currency-selector/radio-button.tsx @@ -1,60 +1,32 @@ -import React, { InputHTMLAttributes, AllHTMLAttributes, ReactElement } from 'react'; +import React, { AllHTMLAttributes } from 'react'; import classNames from 'classnames'; -import { Localize } from '@deriv/translations'; -import { Popover, Icon } from '@deriv/components'; +import { Icon } from '@deriv/components'; import { getCurrencyDisplayCode } from '@deriv/shared'; - -export type TUSTPopover = { - id: string; -}; +import USTPopover from './ust-popover'; type TRadioButtonExtend = { - field: InputHTMLAttributes; + field: React.InputHTMLAttributes; icon?: string; second_line_label?: string; + id: string; + label: string; + onClick?: (e: React.MouseEvent) => void; }; export type TRadioButton = AllHTMLAttributes & TRadioButtonExtend; -const USTPopover = ({ id }: TUSTPopover) => { - let popover_message: ReactElement | undefined; - if (/^UST$/i.test(id)) { - popover_message = ( - ]} - /> - ); - } else if (/^tUSDT$/i.test(id)) { - popover_message = ( - - ); - } else { - popover_message = ( - - ); - } - - return ( - - ); -}; +/** + * RadioButton component to select currency + * @name RadioButton + * @param {React.InputHTMLAttributes} field - field props given by Formik + * @param {string} icon - icon name + * @param {string} id - currency id + * @param {string} label - currency name + * @param {string} second_line_label - currency code + * @param {Function} onClick - function to be called on click + * @param {AllHTMLAttributes} props - other props to be passed + * @returns {React.ReactNode} - returns a React node + */ const RadioButton = ({ field: { name, value, onChange, onBlur }, diff --git a/packages/account/src/Components/currency-selector/ust-popover.tsx b/packages/account/src/Components/currency-selector/ust-popover.tsx new file mode 100644 index 000000000000..0a7f2443b676 --- /dev/null +++ b/packages/account/src/Components/currency-selector/ust-popover.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Popover } from '@deriv/components'; +import { Localize } from '@deriv/translations'; + +export type TUSTPopover = { + id: string; +}; + +const USTPopover = ({ id }: TUSTPopover) => { + let popover_message: React.ReactElement; + if (/^UST$/i.test(id)) { + popover_message = ( + ]} + /> + ); + } else if (/^tUSDT$/i.test(id)) { + popover_message = ( + + ); + } else { + popover_message = ( + + ); + } + + return ( + + ); +}; + +export default USTPopover; diff --git a/packages/account/src/Components/financial-details/__tests__/financial-details.spec.tsx b/packages/account/src/Components/financial-details/__tests__/financial-details.spec.tsx index 0096dd892061..b7683eef5b7f 100644 --- a/packages/account/src/Components/financial-details/__tests__/financial-details.spec.tsx +++ b/packages/account/src/Components/financial-details/__tests__/financial-details.spec.tsx @@ -1,8 +1,9 @@ +import { FormikValues } from 'formik'; import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { isDesktop, isMobile } from '@deriv/shared'; -import FinancialDetails, { TFinancialInformationAndTradingExperience, TFinancialDetails } from '../financial-details'; -import { FormikValues } from 'formik'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import FinancialDetails from '../financial-details'; jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), @@ -14,48 +15,8 @@ const modal_root_el = document.createElement('div'); modal_root_el.setAttribute('id', 'modal_root'); document.body.appendChild(modal_root_el); -const fields_enums: TFinancialInformationAndTradingExperience = { - account_turnover_enum: [ - { value: 'account turnover 1', text: 'account turnover 1' }, - { value: 'account turnover 2', text: 'account turnover 2' }, - ], - education_level_enum: [ - { value: 'education level 1', text: 'education level 1' }, - { value: 'education level 2', text: 'education level 2' }, - ], - employment_industry_enum: [ - { value: 'employment industry 1', text: 'employment industry 1' }, - { value: 'employment industry 2', text: 'employment industry 2' }, - ], - estimated_worth_enum: [ - { value: 'estimated worth 1', text: 'estimated worth 1' }, - { value: 'estimated worth 2', text: 'estimated worth 2' }, - ], - income_source_enum: [ - { value: 'income source 1', text: 'income source 1' }, - { value: 'income source 2', text: 'income source 2' }, - ], - net_income_enum: [ - { value: 'net income 1', text: 'net income 1' }, - { value: 'net income 2', text: 'net income 2' }, - ], - occupation_enum: [ - { value: 'occupation 1', text: 'occupation 1' }, - { value: 'occupation 2', text: 'occupation 2' }, - ], - - source_of_wealth_enum: [ - { value: 'source of wealth 1', text: 'source of wealth 1' }, - { value: 'source of wealth 2', text: 'source of wealth 2' }, - ], - employment_status_enum: [ - { value: 'employment status 1', text: 'employment status 1' }, - { value: 'employment status 2', text: 'employment status 2' }, - ], -}; - describe('', () => { - let mock_props: TFinancialDetails & TFinancialInformationAndTradingExperience = { + const mock_props: React.ComponentProps = { getCurrentStep: jest.fn(), goToNextStep: jest.fn(), onCancel: jest.fn(), @@ -64,32 +25,8 @@ describe('', () => { validate: jest.fn(() => ({ errors: {} })), goToPreviousStep: jest.fn(() => ({ errors: {} })), value: {}, - income_source_enum: [{}], - employment_status_enum: [{}], - employment_industry_enum: [{}], - occupation_enum: [{}], - source_of_wealth_enum: [{}], - education_level_enum: [{}], - net_income_enum: [{}], - estimated_worth_enum: [{}], - account_turnover_enum: [{}], - forex_trading_experience_enum: [{}], - forex_trading_frequency_enum: [{}], - binary_options_trading_experience_enum: [{}], - binary_options_trading_frequency_enum: [{}], - cfd_trading_experience_enum: [{}], - cfd_trading_frequency_enum: [{}], - other_instruments_trading_experience_enum: [{}], - other_instruments_trading_frequency_enum: [{}], }; - beforeEach(() => { - mock_props = { - ...mock_props, - ...fields_enums, - }; - }); - const fieldsRenderCheck = () => { expect(screen.getByText('Anticipated annual turnover')).toBeInTheDocument(); expect(screen.getByText('Estimated net worth')).toBeInTheDocument(); @@ -107,7 +44,7 @@ describe('', () => { fieldsRenderCheck(); const inputs = screen.getAllByTestId('dti_dropdown_display'); - expect(inputs.length).toBe(8); + expect(inputs).toHaveLength(8); expect(screen.getByText('Next')).toBeInTheDocument(); expect(screen.getByText('Previous')).toBeInTheDocument(); @@ -122,7 +59,7 @@ describe('', () => { fieldsRenderCheck(); const inputs = screen.getAllByRole('combobox'); - expect(inputs.length).toBe(8); + expect(inputs).toHaveLength(8); expect(screen.getByText('Next')).toBeInTheDocument(); expect(screen.getByText('Previous')).toBeInTheDocument(); @@ -185,4 +122,19 @@ describe('', () => { expect(mock_props.onSubmit).toHaveBeenCalledTimes(1); }); }); + + it('should change the selected value when user changes the value in the dropdown', () => { + (isDesktop as jest.Mock).mockReturnValue(false); + (isMobile as jest.Mock).mockReturnValue(true); + + render(); + + const select_inputs = screen.getAllByRole('combobox'); + + const income_source_select = select_inputs.find((option: FormikValues) => option.name === 'income_source'); + + userEvent.selectOptions(income_source_select as HTMLElement, 'Salaried Employee'); + + expect(screen.getByRole('option', { name: 'Salaried Employee' }).selected).toBe(true); + }); }); diff --git a/packages/account/src/Components/financial-details/financial-details-partials.tsx b/packages/account/src/Components/financial-details/financial-details-partials.tsx index 37bfc4a8e149..1887f19849d0 100644 --- a/packages/account/src/Components/financial-details/financial-details-partials.tsx +++ b/packages/account/src/Components/financial-details/financial-details-partials.tsx @@ -1,966 +1,135 @@ -import { Field, FormikProps, FormikValues } from 'formik'; +import { Field, FormikValues, useFormikContext } from 'formik'; import React from 'react'; import { DesktopWrapper, MobileWrapper, Dropdown, SelectNative } from '@deriv/components'; import { localize } from '@deriv/translations'; - -type TEmploymentStatus = { - employment_status_enum: object[]; -}; - -export const EmploymentStatus = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - employment_status_enum, -}: Partial> & TEmploymentStatus) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('employment_status', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TIncomeSource = { - income_source_enum: object[]; -}; - -export const IncomeSource = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - income_source_enum, -}: Partial> & TIncomeSource) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('income_source', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TEmploymentIndustry = { - employment_industry_enum: object[]; -}; - -export const EmploymentIndustry = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - employment_industry_enum, -}: Partial> & TEmploymentIndustry) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('employment_industry', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TOccupation = { - occupation_enum: object[]; -}; - -export const Occupation = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - occupation_enum, -}: Partial> & TOccupation) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('occupation', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TSourceOfWealth = { - source_of_wealth_enum: object[]; -}; - -export const SourceOfWealth = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - source_of_wealth_enum, -}: Partial> & TSourceOfWealth) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('source_of_wealth', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TEducationLevel = { - education_level_enum: object[]; -}; - -export const EducationLevel = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - education_level_enum, -}: Partial> & TEducationLevel) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('education_level', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TNetIncome = { - net_income_enum: object[]; -}; - -export const NetIncome = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - net_income_enum, -}: Partial> & TNetIncome) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('net_income', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TEstimatedWorth = { - estimated_worth_enum: object[]; -}; - -export const EstimatedWorth = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - estimated_worth_enum, -}: Partial> & TEstimatedWorth) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('estimated_worth', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TAccountTurnover = { - account_turnover_enum: object[]; -}; - -export const AccountTurnover = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - account_turnover_enum, -}: Partial> & TAccountTurnover) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('account_turnover', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TForexTradingExperience = { - forex_trading_experience_enum?: object[]; -}; - -export const ForexTradingExperience = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - forex_trading_experience_enum, -}: Partial> & TForexTradingExperience) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('forex_trading_experience', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TForexTradingFrequency = { - forex_trading_frequency_enum?: object[]; -}; - -export const ForexTradingFrequency = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - forex_trading_frequency_enum, -}: Partial> & TForexTradingFrequency) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('forex_trading_frequency', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TBinaryOptionsTradingExperience = { - binary_options_trading_experience_enum?: object[]; -}; - -export const BinaryOptionsTradingExperience = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - binary_options_trading_experience_enum, -}: Partial> & TBinaryOptionsTradingExperience) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('binary_options_trading_experience', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TBinaryOptionsTradingFrequency = { - binary_options_trading_frequency_enum?: object[]; -}; - -export const BinaryOptionsTradingFrequency = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - binary_options_trading_frequency_enum, -}: Partial> & TBinaryOptionsTradingFrequency) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('binary_options_trading_frequency', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TCFDTradingExperience = { - cfd_trading_experience_enum?: object[]; -}; - -export const CFDTradingExperience = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - cfd_trading_experience_enum, -}: Partial> & TCFDTradingExperience) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('cfd_trading_experience', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TCFDTradingFrequency = { - cfd_trading_frequency_enum?: object[]; -}; - -export const CFDTradingFrequency = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - cfd_trading_frequency_enum, -}: Partial> & TCFDTradingFrequency) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('cfd_trading_frequency', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TOtherInstrumentsTradingExperience = { - other_instruments_trading_experience_enum?: object[]; -}; - -export const OtherInstrumentsTradingExperience = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - other_instruments_trading_experience_enum, -}: Partial> & TOtherInstrumentsTradingExperience) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('other_instruments_trading_experience', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); - -type TOtherInstrumentsTradingFrequency = { - other_instruments_trading_frequency_enum?: object[]; -}; - -export const OtherInstrumentsTradingFrequency = ({ - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - other_instruments_trading_frequency_enum, -}: Partial> & TOtherInstrumentsTradingFrequency) => ( - - {({ field }: FormikValues) => ( - - - - - - ) => { - if (typeof handleChange === 'function') { - handleChange(e); - } - if (typeof setFieldValue === 'function') { - setFieldValue('other_instruments_trading_frequency', e.target.value, true); - } - }} - {...field} - required - /> - - - )} - -); +import { + getAccountTurnoverList, + getEducationLevelList, + getEmploymentIndustryList, + getEstimatedWorthList, + getIncomeSourceList, + getNetIncomeList, + getOccupationList, + getSourceOfWealthList, +} from 'Configs/financial-details-config'; + +type TFinancialDetailsDropdownFieldProps = { + dropdown_list: Array; + field_key: string; + placeholder?: string; + label: string; +}; + +/** + * Dropdown field for financial details form. + * @name FinancialDetailsDropdownField + * @param {Array} dropdown_list - list of dropdown items + * @param {string} field_key - field reference of the field + * @param {string} placeholder - placeholder of the field + * @param {string} label - label of the field + * @returns {JSX.Element} + */ +const FinancialDetailsDropdownField = ({ + dropdown_list, + field_key, + placeholder = localize('Please select'), + label, +}: TFinancialDetailsDropdownFieldProps) => { + const { values, handleChange, handleBlur, touched, errors, setFieldValue } = useFormikContext<{ + [key: string]: string; + }>(); + + return ( + + {({ field }: FormikValues) => ( + + + + + + ) => { + handleChange(e); + setFieldValue('field_key', e.target.value, true); + }} + {...field} + required + /> + + + )} + + ); +}; + +/** + * Wrapper for financial details form fields. + * @name FinancialInformation + * @returns {JSX.Element} + */ +const FinancialInformation = () => { + return ( + + + + + + + + + + + ); +}; + +export default FinancialInformation; diff --git a/packages/account/src/Components/financial-details/financial-details.tsx b/packages/account/src/Components/financial-details/financial-details.tsx index 9eed2b5c395f..54a25059229a 100644 --- a/packages/account/src/Components/financial-details/financial-details.tsx +++ b/packages/account/src/Components/financial-details/financial-details.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { Formik, FormikValues } from 'formik'; +import { Formik } from 'formik'; import React from 'react'; import { AutoHeightWrapper, @@ -11,88 +11,50 @@ import { } from '@deriv/components'; import { isDesktop, isMobile } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; -import { - AccountTurnover, - IncomeSource, - EducationLevel, - EmploymentIndustry, - EstimatedWorth, - NetIncome, - Occupation, - SourceOfWealth, -} from './financial-details-partials'; +import FinancialInformation from './financial-details-partials'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; -export type TFinancialDetails = { +type TFinancialDetailsFormValues = { + income_source: string; + employment_industry: string; + occupation: string; + source_of_wealth: string; + education_level: string; + net_income: string; + estimated_worth: string; + account_turnover: string; +}; + +type TFinancialDetails = { goToPreviousStep: () => void; goToNextStep: () => void; getCurrentStep: () => number; - onSave: (current_step: number, values: FormikValues) => void; + onSave: (current_step: number, values: TFinancialDetailsFormValues) => void; onSubmit: ( current_step: number, - values: FormikValues, + values: TFinancialDetailsFormValues, actions: (isSubmitting: boolean) => void, props: () => void ) => void; onCancel: (current_step: number, props: () => void) => void; - validate: (values: FormikValues) => object; - value: object; -}; - -export type TFinancialInformationAndTradingExperience = { - shared_props?: object; - income_source_enum: object[]; - employment_status_enum: object[]; - employment_industry_enum: object[]; - occupation_enum: object[]; - source_of_wealth_enum: object[]; - education_level_enum: object[]; - net_income_enum: object[]; - estimated_worth_enum: object[]; - account_turnover_enum: object[]; - forex_trading_experience_enum?: object[]; - forex_trading_frequency_enum?: object[]; - binary_options_trading_experience_enum?: object[]; - binary_options_trading_frequency_enum?: object[]; - cfd_trading_experience_enum?: object[]; - cfd_trading_frequency_enum?: object[]; - other_instruments_trading_experience_enum?: object[]; - other_instruments_trading_frequency_enum?: object[]; + validate: (values: TFinancialDetailsFormValues) => object; + value: TFinancialDetailsFormValues; }; -const FinancialInformation = ({ - shared_props, - income_source_enum, - employment_industry_enum, - occupation_enum, - source_of_wealth_enum, - education_level_enum, - net_income_enum, - estimated_worth_enum, - account_turnover_enum, -}: TFinancialInformationAndTradingExperience) => { - return ( - - - - - - - - - - - ); -}; - -const FinancialDetails = (props: TFinancialDetails & TFinancialInformationAndTradingExperience) => { - const handleCancel = (values: FormikValues) => { +/** + * A wrapper for the financial details form. + * @name FinancialDetails + * @param {TFinancialDetails} props - props of the component + * @returns {React.ReactNode} React component that renders FinancialDetails form. + */ +const FinancialDetails = (props: TFinancialDetails) => { + const handleCancel = (values: TFinancialDetailsFormValues) => { const current_step = props.getCurrentStep() - 1; props.onSave(current_step, values); props.onCancel(current_step, props.goToPreviousStep); }; - const handleValidate = (values: FormikValues) => { + const handleValidate = (values: TFinancialDetailsFormValues) => { const { errors } = splitValidationResultTypes(props.validate(values)); return errors; }; @@ -106,16 +68,7 @@ const FinancialDetails = (props: TFinancialDetails & TFinancialInformationAndTra }} validateOnMount > - {({ handleSubmit, isSubmitting, errors, values, setFieldValue, handleChange, handleBlur, touched }) => { - const shared_props = { - values, - handleChange, - handleBlur, - touched, - errors, - setFieldValue, - }; - + {({ handleSubmit, isSubmitting, errors, values }) => { return ( {({ @@ -141,43 +94,13 @@ const FinancialDetails = (props: TFinancialDetails & TFinancialInformationAndTra 'financial-assessment__form' )} > - + 0 - } + is_disabled={isSubmitting || Object.keys(errors).length > 0} is_absolute={isMobile()} label={localize('Next')} has_cancel diff --git a/packages/account/src/Components/financial-details/index.js b/packages/account/src/Components/financial-details/index.ts similarity index 100% rename from packages/account/src/Components/financial-details/index.js rename to packages/account/src/Components/financial-details/index.ts diff --git a/packages/account/src/Components/forms/form-fields.jsx b/packages/account/src/Components/forms/form-fields.jsx deleted file mode 100644 index d7b7fe337747..000000000000 --- a/packages/account/src/Components/forms/form-fields.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Field } from 'formik'; -import { DateOfBirthPicker, Input } from '@deriv/components'; -import { toMoment } from '@deriv/shared'; - -export const DateOfBirthField = ({ name, portal_id, ...rest }) => ( - - {({ field: { value }, form: { setFieldValue, errors, touched, setFieldTouched } }) => ( - setFieldTouched(name)} - onChange={({ target }) => - setFieldValue(name, target?.value ? toMoment(target.value).format('YYYY-MM-DD') : '', true) - } - value={value} - portal_id={portal_id} - {...rest} - /> - )} - -); - -export const FormInputField = ({ name, warn, ...rest }) => ( - - {({ field, form: { errors, touched } }) => ( - - )} - -); diff --git a/packages/account/src/Components/forms/form-fields/__tests__/date-of-birth-field.spec.tsx b/packages/account/src/Components/forms/form-fields/__tests__/date-of-birth-field.spec.tsx new file mode 100644 index 000000000000..849b83620276 --- /dev/null +++ b/packages/account/src/Components/forms/form-fields/__tests__/date-of-birth-field.spec.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { render, screen } from '@testing-library/react'; +import DateOfBirthField from '../date-of-birth-field'; + +describe('Tesing component', () => { + it('should render properties', () => { + const props: Partial> = { + name: 'test-name', + portal_id: 'test-portal-id', + }; + render( + + + + ); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/forms/form-fields/__tests__/form-input-field.spec.tsx b/packages/account/src/Components/forms/form-fields/__tests__/form-input-field.spec.tsx new file mode 100644 index 000000000000..8d961e72fe20 --- /dev/null +++ b/packages/account/src/Components/forms/form-fields/__tests__/form-input-field.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { render, screen } from '@testing-library/react'; +import FormInputField from '../form-input-field'; + +describe('Tesing component', () => { + it('should render properties', () => { + const props: React.ComponentProps = { + name: 'test-name', + required: true, + }; + render( + + + + ); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('should render Input field with optional status', () => { + const props = { + name: 'test-name', + }; + render( + + + + ); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).not.toBeRequired(); + }); +}); diff --git a/packages/account/src/Components/forms/form-fields/date-of-birth-field.tsx b/packages/account/src/Components/forms/form-fields/date-of-birth-field.tsx new file mode 100644 index 000000000000..b05db31da632 --- /dev/null +++ b/packages/account/src/Components/forms/form-fields/date-of-birth-field.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Field, FieldProps } from 'formik'; +import { DateOfBirthPicker } from '@deriv/components'; +import { toMoment } from '@deriv/shared'; + +type TDateOfBirthFieldProps = { + name: string; + portal_id: string; +} & Omit, 'onBlur' | 'onChange' | 'error'>; + +/** + * DateOfBirthField is a wrapper around DateOfBirthPicker that can be used with Formik. + * @name DateOfBirthField + * @param name - Name of the field + * @param portal_id - Portal ID + * @param [props] - Other props to pass to DateOfBirthPicker + * @returns {React.ReactNode} + */ +const DateOfBirthField = ({ name, portal_id, ...rest }: TDateOfBirthFieldProps) => ( + + {({ field, form: { setFieldValue }, meta: { error, touched } }: FieldProps) => ( + + setFieldValue(name, target?.value ? toMoment(target.value).format('YYYY-MM-DD') : '', true) + } + portal_id={portal_id} + /> + )} + +); + +export default DateOfBirthField; diff --git a/packages/account/src/Components/forms/form-fields/form-input-field.tsx b/packages/account/src/Components/forms/form-fields/form-input-field.tsx new file mode 100644 index 000000000000..a6bd0fb498ad --- /dev/null +++ b/packages/account/src/Components/forms/form-fields/form-input-field.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { FieldInputProps, FormikHelpers, FormikState, Field } from 'formik'; +import { Input } from '@deriv/components'; + +type FormInputFieldProps = { + name: string; + optional?: boolean; + warn?: string; +} & React.ComponentProps; + +type TFormInputFieldHelpers = { + field: FieldInputProps; + form: FormikHelpers & FormikState; +}; + +/** + * FormInputField is a wrapper around Input that can be used with Formik. + * @name FormInputField + * @param name - Name of the field + * @param [optional] - Whether the field is optional + * @param [warn] - Display a warning message + * @param [props] - Other props to pass to Input + * @returns ReactNode + */ +const FormInputField = ({ name, warn, ...rest }: FormInputFieldProps) => ( + + {({ field, form: { errors, touched } }: TFormInputFieldHelpers>) => ( + + )} + +); + +export default FormInputField; diff --git a/packages/account/src/Components/forms/form-fields/index.ts b/packages/account/src/Components/forms/form-fields/index.ts new file mode 100644 index 000000000000..8ecb3a2bce22 --- /dev/null +++ b/packages/account/src/Components/forms/form-fields/index.ts @@ -0,0 +1,4 @@ +import FormInputField from './form-input-field'; +import DateOfBirthField from './date-of-birth-field'; + +export { FormInputField, DateOfBirthField }; diff --git a/packages/account/src/Components/forms/personal-details-form.jsx b/packages/account/src/Components/forms/personal-details-form.jsx index dca56ae8014e..f53db65df731 100644 --- a/packages/account/src/Components/forms/personal-details-form.jsx +++ b/packages/account/src/Components/forms/personal-details-form.jsx @@ -23,7 +23,7 @@ import FormSubHeader from '../form-sub-header'; import InlineNoteWithIcon from '../inline-note-with-icon'; import ConfirmationCheckbox from './confirmation-checkbox'; -import { DateOfBirthField, FormInputField } from './form-fields.jsx'; +import { DateOfBirthField, FormInputField } from './form-fields'; const PersonalDetailsForm = props => { const { diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index 106864344042..791b7440ae21 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -43,7 +43,6 @@ const PersonalDetails = ({ is_virtual, is_fully_authenticated, account_opening_reason_list, - onSubmitEnabledChange, selected_step_ref, closeRealAccountSignup, has_real_account, @@ -51,7 +50,6 @@ const PersonalDetails = ({ }) => { const { account_status, account_settings, residence, real_account_signup_target } = props; const [should_close_tooltip, setShouldCloseTooltip] = React.useState(false); - const is_submit_disabled_ref = React.useRef(true); const PoiNameDobExampleIcon = PoiNameDobExample; @@ -59,15 +57,6 @@ const PersonalDetails = ({ return selected_step_ref?.current?.isSubmitting || Object.keys(errors).length > 0; }; - const checkSubmitStatus = errors => { - const is_submit_disabled = isSubmitDisabled(errors); - - if (is_submit_disabled_ref.current !== is_submit_disabled) { - is_submit_disabled_ref.current = is_submit_disabled; - onSubmitEnabledChange?.(!is_submit_disabled); - } - }; - const handleCancel = values => { const current_step = getCurrentStep() - 1; onSave(current_step, values); @@ -108,7 +97,6 @@ const PersonalDetails = ({ } const { errors } = splitValidationResultTypes(validate(values)); const error_data = { ...idv_error, ...errors }; - checkSubmitStatus(error_data); return error_data; }; diff --git a/packages/account/src/Components/sent-email-modal/sent-email-modal.tsx b/packages/account/src/Components/sent-email-modal/sent-email-modal.tsx index 476a00f910d7..f9ba89987275 100644 --- a/packages/account/src/Components/sent-email-modal/sent-email-modal.tsx +++ b/packages/account/src/Components/sent-email-modal/sent-email-modal.tsx @@ -8,7 +8,7 @@ type TSentEmailModal = { is_modal_when_mobile?: boolean; is_open: boolean; has_live_chat?: boolean; - onClickSendEmail: () => void; + onClickSendEmail: (prop?: string) => void; onClose: () => void; }; diff --git a/packages/account/src/Components/terms-of-use/__tests__/terms-of-use.spec.js b/packages/account/src/Components/terms-of-use/__tests__/terms-of-use.spec.js deleted file mode 100644 index 629854d40ff6..000000000000 --- a/packages/account/src/Components/terms-of-use/__tests__/terms-of-use.spec.js +++ /dev/null @@ -1,187 +0,0 @@ -import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { isDesktop, isMobile, PlatformContext } from '@deriv/shared'; -import TermsOfUse from '../terms-of-use'; - -jest.mock('@deriv/shared', () => ({ - ...jest.requireActual('@deriv/shared'), - isDesktop: jest.fn(() => true), - isMobile: jest.fn(() => false), -})); - -describe('', () => { - const agree_check = /i agree to the/i; - const iom_description = - 'Your account will be opened with Deriv (MX) Ltd, regulated by the UK Gaming Commission (UKGC), and will be subject to the laws of the Isle of Man.'; - const law_title = 'Jurisdiction and choice of law'; - const malta_description = - 'Your account will be opened with Deriv (Europe) Limited, regulated by the Malta Gaming Authority, and will be subject to the laws of Malta.'; - const malta_invest_description = - 'Your account will be opened with Deriv Investments (Europe) Limited, regulated by the Malta Financial Services Authority (MFSA), and will be subject to the laws of Malta.'; - const not_pep_check = 'I am not a PEP, and I have not been a PEP in the last 12 months.'; - const peps_message = - 'A politically exposed person (PEP) is someone appointed with a prominent public position. Close associates and family members of a PEP are also considered to be PEPs.'; - const peps_title = 'Real accounts are not available to politically exposed persons (PEPs).'; - const responsibility_warning_msg = - 'The financial trading services offered on this site are only suitable for customers who accept the possibility of losing all the money they invest and who understand and have experience of the risk involved in the purchase of financial contracts. Transactions in financial contracts carry a high degree of risk. If the contracts you purchased expire as worthless, you will lose all your investment, which includes the contract premium.'; - const risk_warning_title = 'Risk warning'; - const samoa_description = - 'Your account will be opened with Deriv Capital International Ltd and will be subject to the laws of Samoa.'; - const svg_description = - 'Your account will be opened with Deriv (SVG) LLC, and will be subject to the laws of Saint Vincent and the Grenadines.'; - - const mock_props = { - getCurrentStep: jest.fn(), - goToNextStep: jest.fn(), - goToPreviousStep: jest.fn(), - onCancel: jest.fn(), - onSubmit: jest.fn(), - real_account_signup_target: '', - value: { agreed_tos: false, agreed_tnc: false }, - }; - - const commonFieldsCheck = () => { - expect(screen.getByText(agree_check)).toBeInTheDocument(); - expect(screen.getByText(not_pep_check)).toBeInTheDocument(); - expect(screen.getByText(peps_message)).toBeInTheDocument(); - expect(screen.getByText(peps_title)).toBeInTheDocument(); - }; - - it('should render TermsOfUse component for svg accounts', () => { - mock_props.real_account_signup_target = 'svg'; - - render(); - - commonFieldsCheck(); - expect(screen.getByText(law_title)).toBeInTheDocument(); - expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); - expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); - expect(screen.getByText(svg_description)).toBeInTheDocument(); - - expect(screen.queryByText(iom_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_invest_description)).not.toBeInTheDocument(); - expect(screen.queryByText(samoa_description)).not.toBeInTheDocument(); - }); - - it('should render TermsOfUse component for iom accounts', () => { - mock_props.real_account_signup_target = 'iom'; - - render(); - - commonFieldsCheck(); - expect(screen.getByText(iom_description)).toBeInTheDocument(); - expect(screen.getByText(law_title)).toBeInTheDocument(); - - expect(screen.queryByText(malta_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_invest_description)).not.toBeInTheDocument(); - expect(screen.queryByText(responsibility_warning_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(risk_warning_title)).not.toBeInTheDocument(); - expect(screen.queryByText(samoa_description)).not.toBeInTheDocument(); - expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); - }); - - it('should render TermsOfUse component for samoa accounts', () => { - mock_props.real_account_signup_target = 'samoa'; - - render(); - - commonFieldsCheck(); - expect(screen.getByText(law_title)).toBeInTheDocument(); - expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); - expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); - expect(screen.getByText(samoa_description)).toBeInTheDocument(); - - expect(screen.queryByText(iom_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_invest_description)).not.toBeInTheDocument(); - expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); - }); - - it('should render TermsOfUse component for maltainvest accounts and show "Add account" button', () => { - mock_props.real_account_signup_target = 'maltainvest'; - - render(); - - commonFieldsCheck(); - expect(screen.getByText(law_title)).toBeInTheDocument(); - expect(screen.getByText(malta_invest_description)).toBeInTheDocument(); - expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); - expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); - - expect(screen.queryByText(iom_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_description)).not.toBeInTheDocument(); - expect(screen.queryByText(samoa_description)).not.toBeInTheDocument(); - expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); - - const add_btn = screen.getByRole('button', { name: /add account/i }); - expect(add_btn).toBeInTheDocument(); - }); - - it('should render TermsOfUse component for maltainvest accounts and show "Add account" button for mobile', () => { - isMobile.mockReturnValue(true); - isDesktop.mockReturnValue(false); - - mock_props.real_account_signup_target = 'maltainvest'; - - render(); - - commonFieldsCheck(); - expect(screen.getByText(law_title)).toBeInTheDocument(); - expect(screen.getByText(malta_invest_description)).toBeInTheDocument(); - expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); - expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); - - expect(screen.queryByText(iom_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_description)).not.toBeInTheDocument(); - expect(screen.queryByText(samoa_description)).not.toBeInTheDocument(); - expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); - - const add_btn = screen.getByRole('button', { name: /add account/i }); - expect(add_btn).toBeInTheDocument(); - }); - - it('should render TermsOfUse component for malta accounts and trigger buttons', async () => { - mock_props.real_account_signup_target = 'malta'; - - render( - - - - ); - - commonFieldsCheck(); - expect(screen.getByText(law_title)).toBeInTheDocument(); - expect(screen.getByText(malta_description)).toBeInTheDocument(); - - expect(screen.queryByText(iom_description)).not.toBeInTheDocument(); - expect(screen.queryByText(malta_invest_description)).not.toBeInTheDocument(); - expect(screen.queryByText(responsibility_warning_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(risk_warning_title)).not.toBeInTheDocument(); - expect(screen.queryByText(samoa_description)).not.toBeInTheDocument(); - expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); - - const previous_btn = screen.getByRole('button', { name: /previous/i }); - fireEvent.click(previous_btn); - expect(mock_props.getCurrentStep).toHaveBeenCalledTimes(1); - expect(mock_props.onCancel).toHaveBeenCalledTimes(1); - - const agree_checkbox = screen.getByLabelText(agree_check); - const not_pep_checkbox = screen.getByLabelText(not_pep_check); - expect(agree_checkbox.checked).toBeFalsy(); - expect(not_pep_checkbox.checked).toBeFalsy(); - - fireEvent.click(agree_checkbox); - fireEvent.click(not_pep_checkbox); - expect(agree_checkbox.checked).toBeTruthy(); - expect(not_pep_checkbox.checked).toBeTruthy(); - - const finish_btn = screen.getByRole('button', { name: /finish/i }); - - fireEvent.click(finish_btn); - await waitFor(() => { - expect(mock_props.getCurrentStep).toHaveBeenCalledTimes(2); - expect(mock_props.onSubmit).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/packages/account/src/Components/terms-of-use/__tests__/terms-of-use.spec.tsx b/packages/account/src/Components/terms-of-use/__tests__/terms-of-use.spec.tsx new file mode 100644 index 000000000000..a8a8ef15dfee --- /dev/null +++ b/packages/account/src/Components/terms-of-use/__tests__/terms-of-use.spec.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { isDesktop, isMobile } from '@deriv/shared'; +import TermsOfUse from '../terms-of-use'; + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + isDesktop: jest.fn(() => true), + isMobile: jest.fn(() => false), +})); + +describe('', () => { + const agree_check = /i agree to the/i; + const law_title = 'Jurisdiction and choice of law'; + const malta_invest_description = + 'Your account will be opened with Deriv Investments (Europe) Limited, regulated by the Malta Financial Services Authority (MFSA), and will be subject to the laws of Malta.'; + const not_pep_check = 'I am not a PEP, and I have not been a PEP in the last 12 months.'; + const peps_message = + 'A politically exposed person (PEP) is someone appointed with a prominent public position. Close associates and family members of a PEP are also considered to be PEPs.'; + const peps_title = 'Real accounts are not available to politically exposed persons (PEPs).'; + const responsibility_warning_msg = + 'The financial trading services offered on this site are only suitable for customers who accept the possibility of losing all the money they invest and who understand and have experience of the risk involved in the purchase of financial contracts. Transactions in financial contracts carry a high degree of risk. If the contracts you purchased expire as worthless, you will lose all your investment, which includes the contract premium.'; + const risk_warning_title = 'Risk warning'; + const svg_description = + 'Your account will be opened with Deriv (SVG) LLC, and will be subject to the laws of Saint Vincent and the Grenadines.'; + + const mock_props: React.ComponentProps = { + getCurrentStep: jest.fn(), + goToNextStep: jest.fn(), + goToPreviousStep: jest.fn(), + onCancel: jest.fn(), + onSubmit: jest.fn(), + real_account_signup_target: 'svg', + value: { agreed_tos: false, agreed_tnc: false }, + }; + + const commonFieldsCheck = () => { + expect(screen.getByText(agree_check)).toBeInTheDocument(); + expect(screen.getByText(not_pep_check)).toBeInTheDocument(); + expect(screen.getByText(peps_message)).toBeInTheDocument(); + expect(screen.getByText(peps_title)).toBeInTheDocument(); + }; + + it('should render TermsOfUse component for svg accounts', () => { + render(); + + commonFieldsCheck(); + expect(screen.getByText(law_title)).toBeInTheDocument(); + expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); + expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); + expect(screen.getByText(svg_description)).toBeInTheDocument(); + expect(screen.queryByText(malta_invest_description)).not.toBeInTheDocument(); + }); + + it('should render TermsOfUse component for maltainvest accounts and show "Add account" button', () => { + mock_props.real_account_signup_target = 'maltainvest'; + + render(); + + commonFieldsCheck(); + expect(screen.getByText(law_title)).toBeInTheDocument(); + expect(screen.getByText(malta_invest_description)).toBeInTheDocument(); + expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); + expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); + expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); + + const add_btn = screen.getByRole('button', { name: /add account/i }); + expect(add_btn).toBeInTheDocument(); + }); + + it('should render TermsOfUse component for maltainvest accounts and show "Add account" button for mobile', () => { + (isMobile as jest.Mock).mockReturnValue(true); + (isDesktop as jest.Mock).mockReturnValue(false); + + mock_props.real_account_signup_target = 'maltainvest'; + + render(); + + commonFieldsCheck(); + expect(screen.getByText(law_title)).toBeInTheDocument(); + expect(screen.getByText(malta_invest_description)).toBeInTheDocument(); + expect(screen.getByText(responsibility_warning_msg)).toBeInTheDocument(); + expect(screen.getByText(risk_warning_title)).toBeInTheDocument(); + expect(screen.queryByText(svg_description)).not.toBeInTheDocument(); + + const add_btn = screen.getByRole('button', { name: /add account/i }); + expect(add_btn).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/terms-of-use/checkbox-field.jsx b/packages/account/src/Components/terms-of-use/checkbox-field.jsx deleted file mode 100644 index 0c5a8685c587..000000000000 --- a/packages/account/src/Components/terms-of-use/checkbox-field.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { Checkbox } from '@deriv/components'; - -/* - * This component is used with Formik's Field component. - */ -const CheckboxField = ({ field: { name, value, onChange }, id, label, className, ...props }) => { - return ( -
- -
- ); -}; - -export default CheckboxField; diff --git a/packages/account/src/Components/terms-of-use/checkbox-field.tsx b/packages/account/src/Components/terms-of-use/checkbox-field.tsx new file mode 100644 index 000000000000..2d97193fa454 --- /dev/null +++ b/packages/account/src/Components/terms-of-use/checkbox-field.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { FieldInputProps } from 'formik'; +import { Checkbox } from '@deriv/components'; + +type TCheckboxFieldProps = { + field: FieldInputProps; + id: string; + className: string; + label: string; +}; + +/** + * This component is used with Formik's Field component. + * @param {FieldInputProps} field - Formik's field props + * @param {string} id - Checkbox id + * @param {string} className - Class name for styling + * @param {string} label - Checkbox label + * @param {object} props - Other props + * @returns {React.ReactNode} - React node + */ +const CheckboxField = ({ field: { name, value, onChange }, id, label, className, ...props }: TCheckboxFieldProps) => { + return ( +
+ +
+ ); +}; + +export default CheckboxField; diff --git a/packages/account/src/Components/terms-of-use/index.js b/packages/account/src/Components/terms-of-use/index.js deleted file mode 100644 index 5e27f5dc4464..000000000000 --- a/packages/account/src/Components/terms-of-use/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import TermsOfUse from './terms-of-use.jsx'; - -export default TermsOfUse; diff --git a/packages/account/src/Components/terms-of-use/index.ts b/packages/account/src/Components/terms-of-use/index.ts new file mode 100644 index 000000000000..8ae373d500ae --- /dev/null +++ b/packages/account/src/Components/terms-of-use/index.ts @@ -0,0 +1,3 @@ +import TermsOfUse from './terms-of-use'; + +export default TermsOfUse; diff --git a/packages/account/src/Components/terms-of-use/terms-of-use-messages.jsx b/packages/account/src/Components/terms-of-use/terms-of-use-messages.jsx deleted file mode 100644 index 08b0d207aaa8..000000000000 --- a/packages/account/src/Components/terms-of-use/terms-of-use-messages.jsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react'; -import { Localize } from '@deriv/translations'; -import { getLegalEntityName } from '@deriv/shared'; -import { Text } from '@deriv/components'; - -export const Hr = () =>
; - -export const BrokerSpecificMessage = ({ target }) => ( - - {target === 'svg' && } - {target === 'iom' && } - {target === 'malta' && } - {target === 'maltainvest' && } - {target === 'samoa' && } - -); - -export const SVGDescription = () => ( - - - - -

- -

-
- - - -

- -

-
-); - -export const IOMDescription = () => ( - - - - -

- -

-
-); - -export const MaltaDescription = () => ( - - - - -

- -

-
-); - -export const MaltaInvestDescription = () => ( - - - - -

- -

-
- - - -

- -

-
-); - -export const SamoaDescription = () => ( - - - - -

- -

-
- - - -

- -

-
-); - -export const SharedMessage = () => ( - - - - -

- -

-
-); diff --git a/packages/account/src/Components/terms-of-use/terms-of-use-messages.tsx b/packages/account/src/Components/terms-of-use/terms-of-use-messages.tsx new file mode 100644 index 000000000000..1cb8a0a1ea77 --- /dev/null +++ b/packages/account/src/Components/terms-of-use/terms-of-use-messages.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Text } from '@deriv/components'; +import { getLegalEntityName, Jurisdiction, TBrokerCodes } from '@deriv/shared'; +import { Localize } from '@deriv/translations'; + +/** + * Renders a horizontal line + * @name Hr + * @returns JSX.Element + */ +export const Hr = () =>
; + +/** + * Renders the broker specific message based on the broker code + * @name BrokerSpecificMessage + * @param target - Broker code + * @returns JSX.Element + */ +export const BrokerSpecificMessage = ({ target }: { target: Extract }) => ( + + + + +

+ {target === Jurisdiction.SVG ? ( + + ) : ( + + )} +

+
+ + + +

+ +

+
+); + +/** + * Returns the generic terms of use message + * @name SVGDescription + * @returns JSX.Element + */ +export const SharedMessage = () => ( + + + + +

+ +

+
+); diff --git a/packages/account/src/Components/terms-of-use/terms-of-use.scss b/packages/account/src/Components/terms-of-use/terms-of-use.scss index b821ef2b1276..c3bd52949e20 100644 --- a/packages/account/src/Components/terms-of-use/terms-of-use.scss +++ b/packages/account/src/Components/terms-of-use/terms-of-use.scss @@ -4,6 +4,7 @@ flex-grow: 1; margin: 0 8rem !important; width: 84% !important; + padding-bottom: unset; @include mobile { margin: unset !important; diff --git a/packages/account/src/Components/terms-of-use/terms-of-use.jsx b/packages/account/src/Components/terms-of-use/terms-of-use.tsx similarity index 75% rename from packages/account/src/Components/terms-of-use/terms-of-use.jsx rename to packages/account/src/Components/terms-of-use/terms-of-use.tsx index 64aeb86b3f19..41d378f2f9f5 100644 --- a/packages/account/src/Components/terms-of-use/terms-of-use.jsx +++ b/packages/account/src/Components/terms-of-use/terms-of-use.tsx @@ -1,6 +1,6 @@ -import { Field, Formik } from 'formik'; import React from 'react'; -import cn from 'classnames'; +import { Field, Formik } from 'formik'; +import className from 'classnames'; import { Div100vhContainer, Modal, @@ -9,12 +9,47 @@ import { AutoHeightWrapper, StaticUrl, } from '@deriv/components'; -import { isDesktop, isMobile, PlatformContext } from '@deriv/shared'; +import { isDesktop, isMobile, PlatformContext, TBrokerCodes } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; -import CheckboxField from './checkbox-field.jsx'; -import { SharedMessage, BrokerSpecificMessage, Hr } from './terms-of-use-messages.jsx'; +import CheckboxField from './checkbox-field'; +import { SharedMessage, BrokerSpecificMessage, Hr } from './terms-of-use-messages'; import './terms-of-use.scss'; +type TTermsOfUseFormProps = { + agreed_tos: boolean; + agreed_tnc: boolean; +}; + +type TTermsOfUseProps = { + getCurrentStep: () => number; + onCancel: (current_step: number, goToPreviousStep: () => void) => void; + goToPreviousStep: () => void; + goToNextStep: () => void; + onSubmit: ( + current_step: number | null, + values: TTermsOfUseFormProps, + action: (isSubmitting: boolean) => void, + next_step: () => void + ) => void; + value: TTermsOfUseFormProps; + real_account_signup_target: TBrokerCodes; + form_error?: string; +}; + +/** + * Terms of use component for account signup + * @name TermsOfUse + * @param getCurrentStep - function to get current step + * @param onCancel - function to cancel account signup + * @param goToPreviousStep - function to go to previous step + * @param goToNextStep - function to go to next step + * @param onSubmit - function to submit form + * @param value - form values + * @param real_account_signup_target - broker code + * @param form_error - form error + * @param props - other props + * @returns React node + */ const TermsOfUse = ({ getCurrentStep, onCancel, @@ -24,7 +59,7 @@ const TermsOfUse = ({ value, real_account_signup_target, ...props -}) => { +}: TTermsOfUseProps) => { const { is_appstore } = React.useContext(PlatformContext); const handleCancel = () => { @@ -43,7 +78,7 @@ const TermsOfUse = ({ { - onSubmit(getCurrentStep() - 1, {}, actions.setSubmitting, goToNextStep); + onSubmit(getCurrentStep() - 1, values, actions.setSubmitting, goToNextStep); }} > {({ handleSubmit, values, isSubmitting }) => ( @@ -56,10 +91,7 @@ const TermsOfUse = ({ is_disabled={isDesktop()} > -
+

diff --git a/packages/account/src/Components/unlink-account-modal/__tests__/unlink-account-modal.spec.tsx b/packages/account/src/Components/unlink-account-modal/__tests__/unlink-account-modal.spec.tsx new file mode 100644 index 000000000000..3cea93998ab4 --- /dev/null +++ b/packages/account/src/Components/unlink-account-modal/__tests__/unlink-account-modal.spec.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import UnlinkAccountModal from '../unlink-account-modal'; + +describe('UnlinkAccountModal', () => { + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const mock_props: React.ComponentProps = { + onClose: jest.fn(), + is_open: true, + identifier_title: 'Google', + onClickSendEmail: jest.fn(), + }; + + it('should render modal body', () => { + render(); + + expect( + screen.getByText( + "To change your email address, you'll first need to unlink your email address from your Google account." + ) + ).toBeInTheDocument(); + }); + + it('should render 2 buttons', () => { + render(); + + expect(screen.getAllByRole('button')).toHaveLength(2); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Unlink from Google')).toBeInTheDocument(); + }); + + it('should invoke onClickSendEmail when clicking on Unlink button', () => { + render(); + + userEvent.click(screen.getByText('Unlink from Google')); + expect(mock_props.onClickSendEmail).toHaveBeenCalledTimes(1); + expect(mock_props.onClose).toHaveBeenCalledTimes(1); + }); + + it('should invoke onClose when clicking on Cancel button', () => { + render(); + + userEvent.click(screen.getByText('Cancel')); + expect(mock_props.onClose).toHaveBeenCalled(); + }); +}); diff --git a/packages/account/src/Components/unlink-account-modal/index.js b/packages/account/src/Components/unlink-account-modal/index.js deleted file mode 100644 index c806dfd906cc..000000000000 --- a/packages/account/src/Components/unlink-account-modal/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import UnlinkAccountModal from './unlink-account-modal.jsx'; - -export default UnlinkAccountModal; diff --git a/packages/account/src/Components/unlink-account-modal/index.ts b/packages/account/src/Components/unlink-account-modal/index.ts new file mode 100644 index 000000000000..39493d809a90 --- /dev/null +++ b/packages/account/src/Components/unlink-account-modal/index.ts @@ -0,0 +1,3 @@ +import UnlinkAccountModal from './unlink-account-modal'; + +export default UnlinkAccountModal; diff --git a/packages/account/src/Components/unlink-account-modal/unlink-account-modal.jsx b/packages/account/src/Components/unlink-account-modal/unlink-account-modal.tsx similarity index 74% rename from packages/account/src/Components/unlink-account-modal/unlink-account-modal.jsx rename to packages/account/src/Components/unlink-account-modal/unlink-account-modal.tsx index f39ecf5bf8b7..04c43a505d0c 100644 --- a/packages/account/src/Components/unlink-account-modal/unlink-account-modal.jsx +++ b/packages/account/src/Components/unlink-account-modal/unlink-account-modal.tsx @@ -1,9 +1,24 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Button, Modal, Text, Icon } from '@deriv/components'; import { localize, Localize } from '@deriv/translations'; -const UnlinkAccountModal = ({ onClose, is_open, identifier_title, onClickSendEmail }) => { +type TUnlinkAccountModalProps = { + onClose: () => void; + is_open: boolean; + identifier_title: string; + onClickSendEmail: () => void; +}; + +/** + * Modal displayed when user clicks on the 'Change email' button in the account settings page. + * @name UnlinkAccountModal + * @param {Function} onClose - function to close the modal + * @param {boolean} is_open - state to toggle the modal + * @param {string} identifier_title - title of the identifier (e.g. Google, Facebook) + * @param {Function} onClickSendEmail - function to send email to user + * @returns {React.ReactNode} - returns jsx component + */ +const UnlinkAccountModal = ({ onClose, is_open, identifier_title, onClickSendEmail }: TUnlinkAccountModalProps) => { const onClickUnlinkButton = () => { onClose(); onClickSendEmail(); @@ -40,11 +55,4 @@ const UnlinkAccountModal = ({ onClose, is_open, identifier_title, onClickSendEma ); }; -UnlinkAccountModal.prototypes = { - onClose: PropTypes.func, - is_open: PropTypes.bool, - identifier_title: PropTypes.string, - onClickSendEmail: PropTypes.func, -}; - export default UnlinkAccountModal; diff --git a/packages/account/src/Configs/__test__/address-details-config.spec.ts b/packages/account/src/Configs/__test__/address-details-config.spec.ts index 16b0cb99945e..a6c0cf07a7b2 100644 --- a/packages/account/src/Configs/__test__/address-details-config.spec.ts +++ b/packages/account/src/Configs/__test__/address-details-config.spec.ts @@ -1,4 +1,4 @@ -import { TSchema, regex_checks } from '@deriv/shared'; +import { regex_checks, TSchema } from '@deriv/shared'; import { transformConfig } from 'Configs/address-details-config'; describe('address-details-config', () => { diff --git a/packages/account/src/Configs/__test__/get-status-badge-config.spec.tsx b/packages/account/src/Configs/__test__/get-status-badge-config.spec.tsx new file mode 100644 index 000000000000..aaa24d33998b --- /dev/null +++ b/packages/account/src/Configs/__test__/get-status-badge-config.spec.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { BrowserRouter } from 'react-router-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; +import getStatusBadgeConfig from 'Configs/get-status-badge-config'; + +describe('getStatusBadgeConfig', () => { + let account_status = ''; + const openFailedVerificationModal = jest.fn(); + const selected_account_type = 'test type'; + + const renderCheck = ( + account_status: Parameters[0], + openFailedVerificationModal: Parameters[1], + selected_account_type: Parameters[2] + ) => { + const badge = getStatusBadgeConfig(account_status, openFailedVerificationModal, selected_account_type); + render( + +
{badge.text}
+
{badge.icon}
+
+ ); + }; + + it('should render pending status', () => { + account_status = 'pending'; + + renderCheck(account_status, openFailedVerificationModal, selected_account_type); + + expect(screen.getByText('Pending verification')).toBeInTheDocument(); + expect(screen.getByText('IcAlertWarning')).toBeInTheDocument(); + }); + + it('should render failed status and trigger "Why?"', () => { + account_status = 'failed'; + + renderCheck(account_status, openFailedVerificationModal, selected_account_type); + + expect(screen.getByText('Verification failed.')).toBeInTheDocument(); + expect(screen.getByText('IcRedWarning')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Why?')); + expect(openFailedVerificationModal).toBeCalledWith(selected_account_type); + }); + + it('should render need_verification status and redirect to identity', () => { + account_status = 'need_verification'; + + renderCheck(account_status, openFailedVerificationModal, selected_account_type); + + expect(screen.getByText('Need verification.')); + expect(screen.getByText('IcAlertInfo')); + + const btn = screen.getByRole('link', { name: 'Verify now' }); + expect(btn).toBeInTheDocument(); + expect(btn.hasAttribute('href')); + expect(btn.hasAttribute('/account/proof-of-identity')); + }); +}); diff --git a/packages/account/src/Configs/__test__/personal-details-config.spec.ts b/packages/account/src/Configs/__test__/personal-details-config.spec.ts index fca6bf8183b9..38cc83d36ca1 100644 --- a/packages/account/src/Configs/__test__/personal-details-config.spec.ts +++ b/packages/account/src/Configs/__test__/personal-details-config.spec.ts @@ -11,75 +11,70 @@ jest.mock('@deriv/shared', () => ({ toMoment: jest.fn(), validLength: jest.fn(), })); + describe('personal-details-config', () => { - const mock_props = { + const mock_props: Parameters[0] = { residence_list: [ { - services: { - idv: { - documents_supported: {}, - has_visual_sample: 0, - is_country_supported: 0, - }, - onfido: { - documents_supported: { - passport: { - display_name: 'Passport', - }, - }, - is_country_supported: 0, - }, - }, - phone_idd: '93', - text: 'Afghanistan', - value: 'af', + phone_idd: '62', + text: 'Indonesia', + value: 'is', tin_format: [], disabled: '1', - }, - { - services: { - idv: { - documents_supported: {}, - has_visual_sample: 0, - is_country_supported: 0, - }, - onfido: { - documents_supported: { - driving_licence: { - display_name: 'Driving Licence', - }, - national_identity_card: { - display_name: 'National Identity Card', - }, - passport: { - display_name: 'Passport', - }, - residence_permit: { - display_name: 'Residence Permit', + identity: { + services: { + idv: { + documents_supported: {}, + has_visual_sample: 0, + is_country_supported: 0, + }, + onfido: { + documents_supported: { + driving_licence: { + display_name: 'Driving Licence', + }, + national_identity_card: { + display_name: 'National Identity Card', + }, + passport: { + display_name: 'Passport', + }, + residence_permit: { + display_name: 'Residence Permit', + }, }, + is_country_supported: 1, }, - is_country_supported: 1, }, - phone_idd: '93', - text: 'Indonesia', - value: 'af', - tin_format: [], - disabled: '1', }, }, ], account_settings: { - tax_residence: 'af', + tax_residence: 'id', residence: 'Indonesia', + document_type: '', + document_number: '', }, - is_appstore: false, real_account_signup_target: 'maltainvest', - account_status: { cashier_validation: ['system_maintenance'] }, + account_status: { + cashier_validation: ['system_maintenance'], + currency_config: { + USD: { + is_deposit_suspended: 0, + is_withdrawal_suspended: 0, + }, + }, + p2p_status: 'active', + prompt_client_to_authenticate: 0, + risk_classification: '', + status: [''], + }, + residence: 'af', }; it('should return account tax residence as default value if it is already set', () => { const personal_details = personal_details_config(mock_props); - expect(personal_details[0].tax_residence.default_value).toEqual('Afghanistan'); + expect(personal_details.tax_residence.default_value).toEqual('Indonesia'); }); it('should return residence as the default value for MF clients, If the account tax residence is not set', () => { @@ -91,7 +86,7 @@ describe('personal-details-config', () => { }, }; const personal_details = personal_details_config(new_props); - expect(personal_details[0].tax_residence.default_value).toEqual(new_props.account_settings.residence); + expect(personal_details.tax_residence.default_value).toEqual(new_props.account_settings.residence); }); it('should not set default value for CR clients, If the account tax residence is not set', () => { @@ -104,7 +99,7 @@ describe('personal-details-config', () => { }, }; const personal_details = personal_details_config(new_props); - expect(personal_details[0].tax_residence.default_value).toEqual(''); + expect(personal_details.tax_residence.default_value).toEqual(''); }); it('should include svg in additional fields if client is not high risk for mt5', () => { @@ -121,7 +116,7 @@ describe('personal-details-config', () => { 'account_opening_reason', ]; additional_fields.forEach(field => { - expect(personal_details[0][field].supported_in).toContain('svg'); + expect(personal_details[field].supported_in).toContain('svg'); }); }); }); diff --git a/packages/account/src/Configs/accept-risk-config.ts b/packages/account/src/Configs/accept-risk-config.ts index a2fd562e7191..1b609b76cd31 100644 --- a/packages/account/src/Configs/accept-risk-config.ts +++ b/packages/account/src/Configs/accept-risk-config.ts @@ -1,5 +1,5 @@ -import { getDefaultFields } from '@deriv/shared'; -import { TSchema } from 'Types'; +import React from 'react'; +import { getDefaultFields, TSchema } from '@deriv/shared'; const accept_risk_config: TSchema = { accept_risk: { diff --git a/packages/account/src/Configs/address-details-config.ts b/packages/account/src/Configs/address-details-config.ts index 46b8b85313a1..2ee5973910b1 100644 --- a/packages/account/src/Configs/address-details-config.ts +++ b/packages/account/src/Configs/address-details-config.ts @@ -1,4 +1,5 @@ -import { localize } from '@deriv/translations'; +import React from 'react'; +import { GetSettings } from '@deriv/api-types'; import { generateValidationFunction, getDefaultFields, @@ -7,8 +8,8 @@ import { address_permitted_special_characters_message, TSchema, } from '@deriv/shared'; +import { localize } from '@deriv/translations'; import { TUpgradeInfo } from 'Types'; -import { GetSettings } from '@deriv/api-types'; type TAddressDetailsConfigProps = { upgrade_info: TUpgradeInfo; @@ -160,18 +161,17 @@ const address_details_config: ({ const addressDetailsConfig = ( { upgrade_info, real_account_signup_target, residence, account_settings }: TAddressDetailsConfigProps, - AddressDetails: React.Component, - is_appstore: boolean + AddressDetails: React.Component ) => { const is_svg = upgrade_info?.can_upgrade_to === 'svg'; const config = address_details_config({ account_settings, is_svg }); const disabled_items = account_settings.immutable_fields; - const is_mf = real_account_signup_target === 'maltainvest'; + const is_gb_residence = residence === 'gb'; return { header: { - active_title: is_appstore ? localize('Where do you live?') : localize('Complete your address details'), - title: is_appstore ? localize('ADDRESS') : localize('Address'), + active_title: localize('Complete your address details'), + title: localize('Address'), }, body: AddressDetails, form_value: getDefaultFields(real_account_signup_target, config), @@ -180,9 +180,8 @@ const addressDetailsConfig = ( real_account_signup_target, transformConfig(transformForResidence(config, residence), real_account_signup_target) ), - is_svg, disabled_items, - is_mf, + is_gb_residence, }, passthrough: ['residence_list', 'is_fully_authenticated', 'has_real_account'], icon: 'IcDashboardAddress', @@ -191,10 +190,10 @@ const addressDetailsConfig = ( /** * Transform general rules based on residence - * - * @param {object} rules - Original rules - * @param {string} residence - Client's residence - * @return {object} rules - Transformed rules + * @name transformForResidence + * @param rules - Original rules + * @param residence - Client's residence + * @return rules - Transformed rules */ const transformForResidence = (rules: TSchema, residence: string) => { // Isle of Man Clients do not need to fill out state since API states_list is empty. diff --git a/packages/account/src/Configs/currency-selector-config.ts b/packages/account/src/Configs/currency-selector-config.ts index 19ce792fe4f6..05b58358c8ba 100644 --- a/packages/account/src/Configs/currency-selector-config.ts +++ b/packages/account/src/Configs/currency-selector-config.ts @@ -1,6 +1,6 @@ +import React from 'react'; +import { generateValidationFunction, getDefaultFields, TSchema } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import { generateValidationFunction, getDefaultFields } from '@deriv/shared'; -import { TSchema } from 'Types'; const currency_selector_config: TSchema = { currency: { @@ -12,13 +12,12 @@ const currency_selector_config: TSchema = { const currencySelectorConfig = ( { real_account_signup_target }: { real_account_signup_target: string }, - CurrencySelector: React.Component, - is_appstore: boolean + CurrencySelector: React.Component ) => { return { header: { - active_title: is_appstore ? localize('Select wallet currency') : localize('Please choose your currency'), - title: is_appstore ? localize('CURRENCY') : localize('Account currency'), + active_title: localize('Please choose your currency'), + title: localize('Account currency'), }, body: CurrencySelector, form_value: getDefaultFields(real_account_signup_target, currency_selector_config), diff --git a/packages/account/src/Configs/financial-details-config.ts b/packages/account/src/Configs/financial-details-config.ts index db4aa1cb9a05..489987fe8003 100644 --- a/packages/account/src/Configs/financial-details-config.ts +++ b/packages/account/src/Configs/financial-details-config.ts @@ -1,6 +1,7 @@ -import { localize } from '@deriv/translations'; -import { TSchema, generateValidationFunction, getDefaultFields } from '@deriv/shared'; +import React from 'react'; import { GetFinancialAssessment } from '@deriv/api-types'; +import { generateValidationFunction, getDefaultFields, TSchema } from '@deriv/shared'; +import { localize } from '@deriv/translations'; type TFinancialDetailsConfig = { real_account_signup_target: string; @@ -69,312 +70,247 @@ const financialDetailsConfig = ( form_value: getDefaultFields(real_account_signup_target, config), props: { validate: generateValidationFunction(real_account_signup_target, config), - account_turnover_enum: account_turnover_enum(), - binary_options_trading_experience_enum: binary_options_trading_experience_enum(), - binary_options_trading_frequency_enum: binary_options_trading_frequency_enum(), - cfd_trading_experience_enum: cfd_trading_experience_enum(), - cfd_trading_frequency_enum: cfd_trading_frequency_enum(), - education_level_enum: education_level_enum(), - employment_industry_enum: employment_industry_enum(), - employment_status_enum: employment_status_enum(), - forex_trading_experience_enum: forex_trading_experience_enum(), - forex_trading_frequency_enum: forex_trading_frequency_enum(), - estimated_worth_enum: estimated_worth_enum(), - income_source_enum: income_source_enum(), - net_income_enum: net_income_enum(), - occupation_enum: occupation_enum(), - other_instruments_trading_experience_enum: other_instruments_trading_experience_enum(), - other_instruments_trading_frequency_enum: other_instruments_trading_frequency_enum(), - source_of_wealth_enum: source_of_wealth_enum(), }, passthrough: ['residence_list', 'is_fully_authenticated'], }; }; -const account_turnover_enum = () => [ + +export const getAccountTurnoverList = () => [ { - value: 'Less than $25,000', text: localize('Less than $25,000'), + value: 'Less than $25,000', }, { - value: '$25,000 - $50,000', text: localize('$25,000 - $50,000'), + value: '$25,000 - $50,000', }, { - value: '$50,001 - $100,000', text: localize('$50,001 - $100,000'), + value: '$50,001 - $100,000', }, { - value: '$100,001 - $500,000', text: localize('$100,001 - $500,000'), + value: '$100,001 - $500,000', }, { - value: 'Over $500,000', text: localize('Over $500,000'), + value: 'Over $500,000', }, ]; -const binary_options_trading_experience_enum = () => [ - { - value: '0-1 year', - text: localize('0-1 year'), - }, - { - value: '1-2 years', - text: localize('1-2 years'), - }, - { - value: 'Over 3 years', - text: localize('Over 3 years'), - }, -]; -const binary_options_trading_frequency_enum = () => [ - { - value: '0-5 transactions in the past 12 months', - text: localize('0-5 transactions in the past 12 months'), - }, - { - value: '6-10 transactions in the past 12 months', - text: localize('6-10 transactions in the past 12 months'), - }, - { - value: '11-39 transactions in the past 12 months', - text: localize('11-39 transactions in the past 12 months'), - }, - { - value: '40 transactions or more in the past 12 months', - text: localize('40 transactions or more in the past 12 months'), - }, -]; -const cfd_trading_experience_enum = binary_options_trading_experience_enum; // Keeping alias to have a uniform readability -const cfd_trading_frequency_enum = binary_options_trading_frequency_enum; -const education_level_enum = () => [ - { - value: 'Primary', - text: localize('Primary'), - }, - { - value: 'Secondary', - text: localize('Secondary'), - }, - { - value: 'Tertiary', - text: localize('Tertiary'), - }, -]; -const employment_industry_enum = () => [ + +export const getEmploymentIndustryList = () => [ { - value: 'Construction', text: localize('Construction'), + value: 'Construction', }, { - value: 'Education', text: localize('Education'), + value: 'Education', }, { - value: 'Finance', text: localize('Finance'), + value: 'Finance', }, { - value: 'Health', text: localize('Health'), + value: 'Health', }, { - value: 'Tourism', text: localize('Tourism'), + value: 'Tourism', }, { - value: 'Information & Communications Technology', text: localize('Information & Communications Technology'), + value: 'Information & Communications Technology', }, { - value: 'Science & Engineering', text: localize('Science & Engineering'), + value: 'Science & Engineering', }, { - value: 'Legal', text: localize('Legal'), + value: 'Legal', }, { - value: 'Social & Cultural', text: localize('Social & Cultural'), + value: 'Social & Cultural', }, { - value: 'Agriculture', text: localize('Agriculture'), + value: 'Agriculture', }, { - value: 'Real Estate', text: localize('Real Estate'), + value: 'Real Estate', }, { - value: 'Food Services', text: localize('Food Services'), + value: 'Food Services', }, { - value: 'Manufacturing', text: localize('Manufacturing'), + value: 'Manufacturing', }, { - value: 'Unemployed', text: localize('Unemployed'), + value: 'Unemployed', }, ]; -const employment_status_enum = () => [ + +export const getOccupationList = () => [ { - value: 'Employed', - text: localize('Employed'), + text: localize('Chief Executives, Senior Officials and Legislators'), + value: 'Chief Executives, Senior Officials and Legislators', }, { - value: 'Pensioner', - text: localize('Pensioner'), + text: localize('Managers'), + value: 'Managers', }, { - value: 'Self-Employed', - text: localize('Self-Employed'), + text: localize('Professionals'), + value: 'Professionals', }, { - value: 'Student', - text: localize('Student'), + text: localize('Clerks'), + value: 'Clerks', }, { - value: 'Unemployed', - text: localize('Unemployed'), + text: localize('Personal Care, Sales and Service Workers'), + value: 'Personal Care, Sales and Service Workers', }, -]; -const estimated_worth_enum = () => [ { - value: 'Less than $100,000', - text: localize('Less than $100,000'), + text: localize('Agricultural, Forestry and Fishery Workers'), + value: 'Agricultural, Forestry and Fishery Workers', }, { - value: '$100,000 - $250,000', - text: localize('$100,000 - $250,000'), + text: localize('Craft, Metal, Electrical and Electronics Workers'), + value: 'Craft, Metal, Electrical and Electronics Workers', }, { - value: '$250,001 - $500,000', - text: localize('$250,001 - $500,000'), + text: localize('Plant and Machine Operators and Assemblers'), + value: 'Plant and Machine Operators and Assemblers', }, { - value: '$500,001 - $1,000,000', - text: localize('$500,001 - $1,000,000'), + text: localize('Cleaners and Helpers'), + value: 'Cleaners and Helpers', }, { - value: 'Over $1,000,000', - text: localize('Over $1,000,000'), + text: localize('Mining, Construction, Manufacturing and Transport Workers'), + value: 'Mining, Construction, Manufacturing and Transport Workers', }, -]; -const forex_trading_experience_enum = binary_options_trading_experience_enum; // Keeping alias to have a uniform readability -const forex_trading_frequency_enum = binary_options_trading_frequency_enum; -const income_source_enum = () => [ { - value: 'Salaried Employee', - text: localize('Salaried Employee'), + text: localize('Armed Forces'), + value: 'Armed Forces', }, { - value: 'Self-Employed', - text: localize('Self-Employed'), + text: localize('Government Officers'), + value: 'Government Officers', }, { - value: 'Investments & Dividends', - text: localize('Investments & Dividends'), + text: localize('Students'), + value: 'Students', }, { - value: 'Pension', - text: localize('Pension'), + text: localize('Unemployed'), + value: 'Unemployed', }, +]; + +export const getSourceOfWealthList = () => [ { - value: 'State Benefits', - text: localize('State Benefits'), + text: localize('Accumulation of Income/Savings'), + value: 'Accumulation of Income/Savings', }, { - value: 'Savings & Inheritance', - text: localize('Savings & Inheritance'), + text: localize('Cash Business'), + value: 'Cash Business', }, -]; -const net_income_enum = account_turnover_enum; -const occupation_enum = () => [ { - value: 'Chief Executives, Senior Officials and Legislators', - text: localize('Chief Executives, Senior Officials and Legislators'), + text: localize('Company Ownership'), + value: 'Company Ownership', }, { - value: 'Managers', - text: localize('Managers'), + text: localize('Divorce Settlement'), + value: 'Divorce Settlement', }, { - value: 'Professionals', - text: localize('Professionals'), + text: localize('Inheritance'), + value: 'Inheritance', }, { - value: 'Clerks', - text: localize('Clerks'), + text: localize('Investment Income'), + value: 'Investment Income', }, { - value: 'Personal Care, Sales and Service Workers', - text: localize('Personal Care, Sales and Service Workers'), + text: localize('Sale of Property'), + value: 'Sale of Property', }, +]; + +export const getEducationLevelList = () => [ { - value: 'Agricultural, Forestry and Fishery Workers', - text: localize('Agricultural, Forestry and Fishery Workers'), + text: localize('Primary'), + value: 'Primary', }, { - value: 'Craft, Metal, Electrical and Electronics Workers', - text: localize('Craft, Metal, Electrical and Electronics Workers'), + text: localize('Secondary'), + value: 'Secondary', }, { - value: 'Plant and Machine Operators and Assemblers', - text: localize('Plant and Machine Operators and Assemblers'), + text: localize('Tertiary'), + value: 'Tertiary', }, +]; + +export const getNetIncomeList = () => [...getAccountTurnoverList()]; + +export const getEstimatedWorthList = () => [ { - value: 'Cleaners and Helpers', - text: localize('Cleaners and Helpers'), + text: localize('Less than $100,000'), + value: 'Less than $100,000', }, { - value: 'Mining, Construction, Manufacturing and Transport Workers', - text: localize('Mining, Construction, Manufacturing and Transport Workers'), + text: localize('$100,000 - $250,000'), + value: '$100,000 - $250,000', }, { - value: 'Armed Forces', - text: localize('Armed Forces'), + text: localize('$250,001 - $500,000'), + value: '$250,001 - $500,000', }, { - value: 'Government Officers', - text: localize('Students'), + text: localize('$500,001 - $1,000,000'), + value: '$500,001 - $1,000,000', }, { - value: 'Unemployed', - text: localize('Unemployed'), + text: localize('Over $1,000,000'), + value: 'Over $1,000,000', }, ]; -const other_instruments_trading_experience_enum = binary_options_trading_experience_enum; // Keeping alias to have a uniform readability -const other_instruments_trading_frequency_enum = binary_options_trading_frequency_enum; -const source_of_wealth_enum = () => [ - { - value: 'Accumulation of Income/Savings', - text: localize('Accumulation of Income/Savings'), - }, + +export const getIncomeSourceList = () => [ { - value: 'Cash Business', - text: localize('Cash Business'), + text: localize('Salaried Employee'), + value: 'Salaried Employee', }, { - value: 'Company Ownership', - text: localize('Company Ownership'), + text: localize('Self-Employed'), + value: 'Self-Employed', }, { - value: 'Divorce Settlement', - text: localize('Divorce Settlement'), + text: localize('Investments & Dividends'), + value: 'Investments & Dividends', }, { - value: 'Inheritance', - text: localize('Inheritance'), + text: localize('Pension'), + value: 'Pension', }, { - value: 'Investment Income', - text: localize('Investment Income'), + text: localize('State Benefits'), + value: 'State Benefits', }, { - value: 'Sale of Property', - text: localize('Sale of Property'), + text: localize('Savings & Inheritance'), + value: 'Savings & Inheritance', }, ]; diff --git a/packages/account/src/Configs/get-status-badge-config.js b/packages/account/src/Configs/get-status-badge-config.tsx similarity index 92% rename from packages/account/src/Configs/get-status-badge-config.js rename to packages/account/src/Configs/get-status-badge-config.tsx index 5f05486e7666..7e394f01c8a4 100644 --- a/packages/account/src/Configs/get-status-badge-config.js +++ b/packages/account/src/Configs/get-status-badge-config.tsx @@ -3,7 +3,11 @@ import { Link } from 'react-router-dom'; import { Text } from '@deriv/components'; import { Localize } from '@deriv/translations'; -const getStatusBadgeConfig = (account_status, openFailedVerificationModal, selected_account_type) => { +const getStatusBadgeConfig = ( + account_status: string, + openFailedVerificationModal: (selected_account_type: string) => void, + selected_account_type: string +) => { switch (account_status) { case 'pending': return { diff --git a/packages/account/src/Configs/personal-details-config.ts b/packages/account/src/Configs/personal-details-config.ts index 1bc0691e9d24..27f134aa9e8a 100644 --- a/packages/account/src/Configs/personal-details-config.ts +++ b/packages/account/src/Configs/personal-details-config.ts @@ -1,3 +1,4 @@ +import { GetAccountStatus, GetSettings, ResidenceList } from '@deriv/api-types'; import { TSchema, generateValidationFunction, @@ -8,18 +9,17 @@ import { } from '@deriv/shared'; import { localize } from '@deriv/translations'; import { shouldShowIdentityInformation } from 'Helpers/utils'; -import { TResidenseList, TUpgradeInfo } from 'Types'; -import { GetAccountStatus, GetSettings } from '@deriv/api-types'; +import { TUpgradeInfo } from 'Types'; +import { PHONE_NUMBER_LENGTH } from 'Constants/personal-details'; type TPersonalDetailsConfig = { upgrade_info?: TUpgradeInfo; real_account_signup_target: string; - residence_list: TResidenseList[]; + residence_list: ResidenceList; account_settings: GetSettings & { document_type: string; document_number: string; }; - is_appstore?: boolean; residence: string; account_status: GetAccountStatus; is_high_risk_client_for_mt5?: boolean; @@ -28,7 +28,6 @@ type TPersonalDetailsConfig = { export const personal_details_config = ({ residence_list, account_settings, - is_appstore, real_account_signup_target, is_high_risk_client_for_mt5, }: TPersonalDetailsConfig) => { @@ -36,11 +35,7 @@ export const personal_details_config = ({ return {}; } - // minimum characters required is 9 numbers (excluding +- signs or space) - const min_phone_number = 9; - const max_phone_number = 35; - - const default_residence = real_account_signup_target === 'maltainvest' ? account_settings?.residence : ''; + const default_residence = (real_account_signup_target === 'maltainvest' && account_settings?.residence) || ''; const config = { account_opening_reason: { @@ -86,16 +81,18 @@ export const personal_details_config = ({ }, place_of_birth: { supported_in: ['maltainvest', 'iom', 'malta'], - default_value: account_settings.place_of_birth - ? residence_list.find(item => item.value === account_settings.place_of_birth)?.text - : '', + default_value: + (account_settings.place_of_birth && + residence_list.find(item => item.value === account_settings.place_of_birth)?.text) || + '', rules: [['req', localize('Place of birth is required.')]], }, citizen: { supported_in: ['iom', 'malta', 'maltainvest'], - default_value: account_settings.citizen - ? residence_list.find(item => item.value === account_settings.citizen)?.text - : '', + default_value: + (account_settings.citizen && + residence_list.find(item => item.value === account_settings.citizen)?.text) || + '', rules: [['req', localize('Citizenship is required')]], }, phone: { @@ -108,20 +105,22 @@ export const personal_details_config = ({ (value: string) => { // phone_trim uses regex that trims non-digits const phone_trim = value.replace(/\D/g, ''); - return validLength(phone_trim, { min: min_phone_number, max: max_phone_number }); + // minimum characters required is 9 numbers (excluding +- signs or space) + return validLength(phone_trim, { min: PHONE_NUMBER_LENGTH.MIN, max: PHONE_NUMBER_LENGTH.MAX }); }, localize('You should enter {{min}}-{{max}} numbers.', { - min: min_phone_number, - max: max_phone_number, + min: PHONE_NUMBER_LENGTH.MIN, + max: PHONE_NUMBER_LENGTH.MAX, }), ], ], }, tax_residence: { //if tax_residence is already set, we will use it as default value else for mf clients we will use residence as default value - default_value: account_settings?.tax_residence - ? residence_list.find(item => item.value === account_settings?.tax_residence)?.text ?? '' - : default_residence, + default_value: + (account_settings?.tax_residence && + residence_list.find(item => item.value === account_settings?.tax_residence)?.text) || + default_residence, supported_in: ['maltainvest'], rules: [['req', localize('Tax residence is required.')]], }, @@ -189,39 +188,26 @@ export const personal_details_config = ({ }, }; - const getConfig = () => { - // Need to check if client is high risk (only have SVG i.e. China & Russia) - // No need to get additinal details when client is high risk - if (!is_high_risk_client_for_mt5 && real_account_signup_target !== 'maltainvest') { - const properties_to_update: (keyof typeof config)[] = [ - 'place_of_birth', - 'tax_residence', - 'tax_identification_number', - 'account_opening_reason', - ]; - - properties_to_update.forEach(key => { - config[key].supported_in.push('svg'); - // Remove required rule for TIN and Tax residence from the config to make the fields optional - if (key === 'tax_identification_number' || key === 'tax_residence') { - config[key].rules = config[key].rules.filter(rule => rule[0] !== 'req'); - } - }); - } + // Need to check if client is high risk (only have SVG i.e. China & Russia) + // No need to get additinal details when client is high risk + if (!is_high_risk_client_for_mt5 && real_account_signup_target !== 'maltainvest') { + const properties_to_update: (keyof typeof config)[] = [ + 'place_of_birth', + 'tax_residence', + 'tax_identification_number', + 'account_opening_reason', + ]; - if (is_appstore) { - const allowed_fields = ['first_name', 'last_name', 'date_of_birth', 'phone']; - return Object.keys(config).reduce((new_config, key) => { - if (allowed_fields.includes(key)) { - new_config[key] = config[key]; - } - return new_config; - }, {}); - } - return config; - }; + properties_to_update.forEach(key => { + config[key].supported_in.push('svg'); + // Remove required rule for TIN and Tax residence from the config to make the fields optional + if (key === 'tax_identification_number' || key === 'tax_residence') { + config[key].rules = config[key].rules.filter(rule => rule[0] !== 'req'); + } + }); + } - return [getConfig()]; + return config; }; const personalDetailsConfig = ( @@ -237,10 +223,9 @@ const personalDetailsConfig = ( PersonalDetails: T, is_appstore = false ) => { - const [config] = personal_details_config({ + const config = personal_details_config({ residence_list, account_settings, - is_appstore, real_account_signup_target, residence, account_status, diff --git a/packages/account/src/Configs/proof-of-identity-config.ts b/packages/account/src/Configs/proof-of-identity-config.ts index e4ab20cbd145..cf1b0260e3ec 100644 --- a/packages/account/src/Configs/proof-of-identity-config.ts +++ b/packages/account/src/Configs/proof-of-identity-config.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { GetSettings } from '@deriv/api-types'; import { localize } from '@deriv/translations'; diff --git a/packages/account/src/Configs/terms-of-use-config.ts b/packages/account/src/Configs/terms-of-use-config.ts index 5e033268edf6..7cdc490913e8 100644 --- a/packages/account/src/Configs/terms-of-use-config.ts +++ b/packages/account/src/Configs/terms-of-use-config.ts @@ -1,7 +1,6 @@ -import { getDefaultFields, isDesktop } from '@deriv/shared'; - +import React from 'react'; +import { getDefaultFields, isDesktop, TSchema } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import { TSchema } from 'Types'; const terms_of_use_config: TSchema = { agreed_tos: { @@ -16,14 +15,13 @@ const terms_of_use_config: TSchema = { const termsOfUseConfig = ( { real_account_signup_target }: { real_account_signup_target: string }, - TermsOfUse: React.Component, - is_appstore = false + TermsOfUse: React.Component ) => { - const active_title = is_appstore ? localize('Our terms of use') : localize('Terms of use'); + const active_title = localize('Terms of use'); return { header: { active_title: isDesktop() ? active_title : null, - title: is_appstore ? localize('TERMS OF USE') : localize('Terms of use'), + title: active_title, }, body: TermsOfUse, form_value: getDefaultFields(real_account_signup_target, terms_of_use_config), diff --git a/packages/account/src/Configs/trading-assessment-config.ts b/packages/account/src/Configs/trading-assessment-config.tsx similarity index 99% rename from packages/account/src/Configs/trading-assessment-config.ts rename to packages/account/src/Configs/trading-assessment-config.tsx index 00343186ace8..1637c828fc5b 100644 --- a/packages/account/src/Configs/trading-assessment-config.ts +++ b/packages/account/src/Configs/trading-assessment-config.tsx @@ -1,11 +1,12 @@ +import React from 'react'; +import { GetFinancialAssessment, GetSettings } from '@deriv/api-types'; import { generateValidationFunction, getDefaultFields } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import { GetFinancialAssessment, GetSettings } from '@deriv/api-types'; type TTradingAssessmentConfig = { - real_account_signup_target: string; - financial_assessment: GetFinancialAssessment; account_settings: GetSettings; + financial_assessment: GetFinancialAssessment; + real_account_signup_target: string; setSubSectionIndex: number; }; diff --git a/packages/account/src/Constants/personal-details.ts b/packages/account/src/Constants/personal-details.ts new file mode 100644 index 000000000000..936d07ecec71 --- /dev/null +++ b/packages/account/src/Constants/personal-details.ts @@ -0,0 +1,4 @@ +export const PHONE_NUMBER_LENGTH = { + MIN: 9, + MAX: 35, +}; diff --git a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js index b89f9960a168..4564e04eb54d 100644 --- a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js +++ b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js @@ -45,6 +45,9 @@ describe('', () => { fetchResidenceList: fetchResidenceList, fetchStatesList: fetchStatesList, }, + ui: { + notification_messages_ui: jest.fn(() =>
Notification
), + }, }); const renderComponent = (modified_store = store) => { diff --git a/packages/account/src/Sections/Security/Passwords/__tests__/deriv-email.spec.tsx b/packages/account/src/Sections/Security/Passwords/__tests__/deriv-email.spec.tsx new file mode 100644 index 000000000000..da8d7dd4c548 --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/__tests__/deriv-email.spec.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { APIProvider } from '@deriv/api'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import DerivEmail from '../deriv-email'; + +describe('DerivEmail', () => { + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const store = mockStore({ client: { email: 'test@demo.com' } }); + + const renderComponent = ({ store_config = store }) => + render( + + + + + + ); + + it('should render email address in disabled form', () => { + renderComponent({}); + + const el_input_field = screen.getByRole('textbox', { name: /Email address\*/i }); + expect(el_input_field).toBeDisabled(); + }); + + it('should display button when it is not redirected from deriv-go', () => { + renderComponent({}); + + const el_button = screen.getByRole('button', { name: /Change email/i }); + expect(el_button).toBeInTheDocument(); + }); + + it('should not display button when it is redirected from deriv-go', () => { + renderComponent({ + store_config: mockStore({ + common: { is_from_derivgo: true }, + }), + }); + + const el_button = screen.queryByRole('button', { name: /Change email/i }); + expect(el_button).not.toBeInTheDocument(); + }); + + it('should not display unlink account modal when not associated with social media', async () => { + renderComponent({}); + const el_button = screen.getByRole('button', { name: /Change email/i }); + userEvent.click(el_button); + let el_modal; + await waitFor(() => { + el_modal = screen.getByText('We’ve sent you an email'); + }); + expect(el_modal).toBeInTheDocument(); + }); + + it('should display unlink account modal when button is clicked', async () => { + const store_config = mockStore({ client: { social_identity_provider: 'Google', is_social_signup: true } }); + renderComponent({ store_config }); + const el_button = screen.getByRole('button', { name: /Change email/i }); + userEvent.click(el_button); + let el_modal; + await waitFor(() => { + el_modal = screen.getByText('Change your login email'); + expect(el_modal).toBeInTheDocument(); + }); + const el_unlink_btn = screen.getByRole('button', { name: /Unlink from Google/i }); + userEvent.click(el_unlink_btn); + + await waitFor(() => { + el_modal = screen.getByText('We’ve sent you an email'); + expect(el_modal).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/account/src/Sections/Security/Passwords/__tests__/deriv-password.spec.js b/packages/account/src/Sections/Security/Passwords/__tests__/deriv-password.spec.tsx similarity index 63% rename from packages/account/src/Sections/Security/Passwords/__tests__/deriv-password.spec.js rename to packages/account/src/Sections/Security/Passwords/__tests__/deriv-password.spec.tsx index e74ee1804a48..072c0c3f96f6 100644 --- a/packages/account/src/Sections/Security/Passwords/__tests__/deriv-password.spec.js +++ b/packages/account/src/Sections/Security/Passwords/__tests__/deriv-password.spec.tsx @@ -1,27 +1,18 @@ import React from 'react'; -import { render, screen, waitFor, fireEvent, queryByText } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import DerivPassword from '../deriv-password'; -import { WS } from '@deriv/shared'; +import { APIProvider, useVerifyEmail } from '@deriv/api'; +import { mockStore, StoreProvider } from '@deriv/stores'; jest.mock('Assets/ic-brand-deriv-red.svg', () => () => 'BrandDerivRed'); -jest.mock('@deriv/shared', () => ({ - ...jest.requireActual('@deriv/shared'), - WS: { - verifyEmail: jest.fn(), - }, +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useVerifyEmail: jest.fn(() => ({ mutate: jest.fn() })), })); describe('', () => { - let mock_props = { - email: 'mf@deriv.com', - is_social_signup: false, - social_identity_provider: undefined, - is_eu_user: false, - financial_restricted_countries: false, - }; - - let modal_root_el; + let modal_root_el: HTMLDivElement; beforeAll(() => { modal_root_el = document.createElement('div'); @@ -32,8 +23,21 @@ describe('', () => { afterAll(() => { document.body.removeChild(modal_root_el); }); + + const store = mockStore({ client: { email: 'mf@deriv.com' } }); + + const renderComponent = ({ store_config = store }) => + render( + + + + + + ); + it('Should render properly', async () => { - render(); + renderComponent({}); + expect( screen.getByRole('heading', { name: /deriv password/i, @@ -44,11 +48,8 @@ describe('', () => { /use the to log in to deriv\.com, deriv go, deriv trader, smarttrader, deriv bot and deriv ctrader\./i ) ).toBeInTheDocument(); - // expect BrandDerivRed not to be in the document expect(screen.queryByText(/BrandDerivRed/i)).toBeInTheDocument(); - // expect button with text change password to be in the document expect(screen.getByRole('button', { name: /change password/i })).toBeInTheDocument(); - // expect button with text unlink from to not be in the document expect(screen.queryByText(/unlink from/i)).not.toBeInTheDocument(); const popover_wrapper = screen.getAllByTestId('dt_popover_wrapper'); @@ -56,7 +57,10 @@ describe('', () => { }); it('displays the correct platform information for non-MF clients & restricted countries', () => { - render(); + const store_config = mockStore({ + traders_hub: { financial_restricted_countries: true }, + }); + renderComponent({ store_config }); expect(screen.getByText(/use the to log in to deriv\.com, deriv trader and deriv go\./i)); @@ -68,27 +72,30 @@ describe('', () => { }); it('displays the correct platform information for MF clients', () => { - render(); + const store_config = mockStore({ + traders_hub: { is_eu_user: true }, + }); + renderComponent({ store_config }); expect(screen.getByText(/use the to log in to deriv\.com and deriv trader\./i)).toBeInTheDocument(); - const popover_wrapper = screen.getAllByTestId('dt_popover_wrapper'); - // expect popover to have length of 4 - expect(popover_wrapper).toHaveLength(1); - // expect button with text change password to be in the document + const popover_wrapper = screen.getByTestId('dt_popover_wrapper'); + + expect(popover_wrapper).toBeInTheDocument(); expect(screen.getByRole('button', { name: /change password/i })).toBeInTheDocument(); }); it('displays a change password button for non-social signups', () => { - render(); + renderComponent({}); const change_password_button = screen.getByRole('button', { name: /change password/i, }); + expect(change_password_button).toBeInTheDocument(); }); it('should invoke verifyEmail when change password is clicked', async () => { - render(); + renderComponent({}); const ele_change_btn = screen.getByRole('button', { name: /change password/i, }); @@ -96,23 +103,16 @@ describe('', () => { expect(screen.queryByText(/we’ve sent you an email/i)).toBeInTheDocument(); expect(screen.getByText(/please click on the link in the email to reset your password\./i)).toBeInTheDocument(); await waitFor(() => { - expect(WS.verifyEmail).toHaveBeenCalled(); + expect(useVerifyEmail).toHaveBeenCalled(); }); }); - it('displays a button to unlink social identity provider', async () => { - const social_props = { - ...mock_props, - is_social_signup: true, - social_identity_provider: 'apple', - }; - render(); + it('displays a button to unlink social identity provider', () => { + const store_config = mockStore({ client: { is_social_signup: true, social_identity_provider: 'apple' } }); + renderComponent({ store_config }); + const unlink_button = screen.getByText(/unlink from/i); - expect(unlink_button).toBeInTheDocument(); - fireEvent.click(unlink_button); - await waitFor(() => { - expect(WS.verifyEmail).toHaveBeenCalled(); - }); + expect(unlink_button).toBeInTheDocument(); }); }); diff --git a/packages/account/src/Sections/Security/Passwords/__tests__/passwords-platform.spec.tsx b/packages/account/src/Sections/Security/Passwords/__tests__/passwords-platform.spec.tsx new file mode 100644 index 000000000000..b8e084f1afe5 --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/__tests__/passwords-platform.spec.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { APIProvider } from '@deriv/api'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import PasswordsPlatform from '../passwords-platform'; + +describe('', () => { + const mock_props = { + has_dxtrade_accounts: false, + has_mt5_accounts: true, + }; + + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const store = mockStore({ client: { email: 'test@demo.com' } }); + + const renderComponent = ({ props = mock_props, store_config = store }) => + render( + + + + + + ); + + it('should render DX password section when platform is MT5', async () => { + renderComponent({}); + + expect(screen.getByText('Deriv MT5 Password')).toBeInTheDocument(); + }); + + it('should render DX password section when platform is DerivX', async () => { + const new_props: React.ComponentProps = { + ...mock_props, + has_dxtrade_accounts: true, + has_mt5_accounts: false, + }; + renderComponent({ props: new_props }); + + expect(screen.getByText('Deriv X Password')).toBeInTheDocument(); + }); + + it('should open Send email modal when Change password button is clicked', async () => { + renderComponent({}); + + userEvent.click(screen.getByRole('button', { name: /change password/i })); + await waitFor(() => { + const el_modal = screen.getByText('We’ve sent you an email'); + expect(el_modal).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/account/src/Sections/Security/Passwords/__tests__/passwords.spec.tsx b/packages/account/src/Sections/Security/Passwords/__tests__/passwords.spec.tsx new file mode 100644 index 000000000000..8d00d8bc3965 --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/__tests__/passwords.spec.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { APIProvider } from '@deriv/api'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import Passwords from '../passwords'; + +jest.mock('Assets/ic-brand-deriv-red.svg', () => jest.fn(() => 'mockedSVGIcon')); + +describe('', () => { + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + const store = mockStore({}); + + const renderComponent = ({ store_config = store }) => { + return render( + + + + + + ); + }; + + it('should render Email and password section', () => { + const store_config = mockStore({ + client: { is_dxtrade_password_not_set: true, is_mt5_password_not_set: true }, + }); + renderComponent({ store_config }); + + expect(screen.getByText('Email address')).toBeInTheDocument(); + expect(screen.getByText('mockedSVGIcon')).toBeInTheDocument(); + }); + + it('should render MT5 platform section', async () => { + const store_config = mockStore({ + client: { is_dxtrade_password_not_set: true }, + }); + renderComponent({ store_config }); + + const ele_mt5 = await screen.findByText('Deriv MT5 Password'); + expect(ele_mt5).toBeInTheDocument(); + }); + + it('should render DerivX platform section', async () => { + const store_config = mockStore({ + client: { is_mt5_password_not_set: true }, + }); + renderComponent({ store_config }); + + const ele_derivx = await screen.findByText('Deriv X Password'); + expect(ele_derivx).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Security/Passwords/__tests__/platform-partials.spec.tsx b/packages/account/src/Sections/Security/Passwords/__tests__/platform-partials.spec.tsx new file mode 100644 index 000000000000..9ebac639b42a --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/__tests__/platform-partials.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { CFD_PLATFORMS } from '@deriv/shared'; +import PlatformPartials from '../platform-partials'; + +describe('', () => { + const mock_props: React.ComponentProps = { + type: CFD_PLATFORMS.DXTRADE, + description:
Test description
, + handleClick: jest.fn(), + }; + + it('should render mt5 partials', () => { + render(); + + expect(screen.getByText('Test description')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /change password/i })).toBeInTheDocument(); + }); + + it('should call handleClick when button is clicked', async () => { + render(); + + const el_button = screen.getByRole('button', { name: /change password/i }); + userEvent.click(el_button); + + await waitFor(() => expect(mock_props.handleClick).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/packages/account/src/Sections/Security/Passwords/deriv-email.jsx b/packages/account/src/Sections/Security/Passwords/deriv-email.tsx similarity index 56% rename from packages/account/src/Sections/Security/Passwords/deriv-email.jsx rename to packages/account/src/Sections/Security/Passwords/deriv-email.tsx index a962f2526c33..c9455578d7b0 100644 --- a/packages/account/src/Sections/Security/Passwords/deriv-email.jsx +++ b/packages/account/src/Sections/Security/Passwords/deriv-email.tsx @@ -1,68 +1,70 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { Formik } from 'formik'; -import { withRouter } from 'react-router'; -import { WS, toTitleCase } from '@deriv/shared'; -import { Localize, localize } from '@deriv/translations'; import { Button, Text, Input } from '@deriv/components'; +import { useVerifyEmail } from '@deriv/api'; +import { toTitleCase } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { Localize, localize } from '@deriv/translations'; import FormSubHeader from 'Components/form-sub-header'; import SentEmailModal from 'Components/sent-email-modal'; import UnlinkAccountModal from 'Components/unlink-account-modal'; -import { observer, useStore } from '@deriv/stores'; -const DerivEmail = observer(({ email, social_identity_provider, is_social_signup }) => { - const { common } = useStore(); - const { is_from_derivgo } = common; +type TVerifyEmailPayload = Parameters['mutate']>[0]; + +/** + * Display the user's email address and a button to change it. + * @name DerivEmail + * @returns {React.ReactNode} + */ +const DerivEmail = observer(() => { + const { + common: { is_from_derivgo }, + client: { social_identity_provider, is_social_signup, email }, + } = useStore(); + const { mutate } = useVerifyEmail(); const [is_unlink_account_modal_open, setIsUnlinkAccountModalOpen] = React.useState(false); const [is_send_email_modal_open, setIsSendEmailModalOpen] = React.useState(false); + const payload: TVerifyEmailPayload = { verify_email: email, type: 'request_email' }; + const onClickChangeEmail = () => { if (is_social_signup) { setIsUnlinkAccountModalOpen(true); } else { - WS.verifyEmail(email, 'request_email'); + mutate(payload); setIsSendEmailModalOpen(true); } }; const onClickSendEmail = () => { - WS.verifyEmail(email, 'request_email'); + mutate(payload); setIsUnlinkAccountModalOpen(false); setIsSendEmailModalOpen(true); }; - const onClickResendEmail = () => { - WS.verifyEmail(email, 'request_email'); - }; - return (
- - - - - + + +
- -
- -
-
+
+ +
{!is_from_derivgo && (
); -}; - -DerivPassword.propTypes = { - email: PropTypes.string, - is_dark_mode_on: PropTypes.bool, - is_eu_user: PropTypes.bool, - financial_restricted_countries: PropTypes.bool, - is_social_signup: PropTypes.bool, - social_identity_provider: PropTypes.string, -}; +}); export default DerivPassword; diff --git a/packages/account/src/Sections/Security/Passwords/index.js b/packages/account/src/Sections/Security/Passwords/index.js deleted file mode 100644 index 0c13bc42a994..000000000000 --- a/packages/account/src/Sections/Security/Passwords/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Passwords from './passwords.jsx'; - -export default Passwords; diff --git a/packages/account/src/Sections/Security/Passwords/index.ts b/packages/account/src/Sections/Security/Passwords/index.ts new file mode 100644 index 000000000000..04e64f927ceb --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/index.ts @@ -0,0 +1,3 @@ +import Passwords from './passwords'; + +export default Passwords; diff --git a/packages/account/src/Sections/Security/Passwords/passwords-platform.jsx b/packages/account/src/Sections/Security/Passwords/passwords-platform.jsx deleted file mode 100644 index 400884fb9178..000000000000 --- a/packages/account/src/Sections/Security/Passwords/passwords-platform.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, Icon, Popover, Text } from '@deriv/components'; -import { CFD_PLATFORMS, WS, getPlatformSettings } from '@deriv/shared'; -import { localize, Localize } from '@deriv/translations'; -import FormSubHeader from 'Components/form-sub-header'; -import SentEmailModal from 'Components/sent-email-modal'; - -const PasswordsPlatform = ({ email, has_dxtrade_accounts, has_mt5_accounts }) => { - const [identifier, setIdenifier] = React.useState(''); - const [is_sent_email_modal_open, setIsSentEmailModalOpen] = React.useState(false); - - const platform_name_dxtrade = getPlatformSettings('dxtrade').name; - - const getPlatformTitle = () => { - let title = ''; - if (has_mt5_accounts) { - title = localize('Deriv MT5 Password'); - } else if (has_dxtrade_accounts) { - title = localize('{{platform_name_dxtrade}} Password', { platform_name_dxtrade }); - } - return title; - }; - - const onClickSendEmail = cfd_platform => { - const password_reset_code = - cfd_platform === CFD_PLATFORMS.MT5 - ? 'trading_platform_mt5_password_reset' - : 'trading_platform_dxtrade_password_reset'; - - WS.verifyEmail(email, password_reset_code, { - url_parameters: { - redirect_to: 3, - }, - }); - setIdenifier(cfd_platform); - setIsSentEmailModalOpen(true); - }; - - return ( - - -
- {has_mt5_accounts && ( - - - - -
- - - -
-
- )} - {has_dxtrade_accounts && ( - - - - -
- - - -
-
- )} - setIsSentEmailModalOpen(false)} - onClickSendEmail={onClickSendEmail} - is_modal_when_mobile - /> - {/* - setIsSuccessDialogOpen(false)} - /> */} -
-
- ); -}; - -PasswordsPlatform.propTypes = { - email: PropTypes.string, - has_dxtrade_accounts: PropTypes.bool, - has_mt5_accounts: PropTypes.bool, -}; - -export default PasswordsPlatform; diff --git a/packages/account/src/Sections/Security/Passwords/passwords-platform.tsx b/packages/account/src/Sections/Security/Passwords/passwords-platform.tsx new file mode 100644 index 000000000000..a26e5f06b57b --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/passwords-platform.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { useMutation } from '@deriv/api'; +import { CFD_PLATFORMS, getPlatformSettings } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { Localize, localize } from '@deriv/translations'; +import FormSubHeader from 'Components/form-sub-header'; +import SentEmailModal from 'Components/sent-email-modal'; +import PlatformPartials from './platform-partials'; + +type TPasswordsPlatformProps = { + has_dxtrade_accounts?: boolean; + has_mt5_accounts?: boolean; +}; + +/** + * Displays a change password instructions for MT5 and/or DXTrade. + * @name PasswordsPlatform + * @param [has_dxtrade_accounts=false] - Whether the user has DXTrade accounts. + * @param [has_mt5_accounts=false] - Whether the user has MT5 accounts. + * @returns React.ReactNode + */ +const PasswordsPlatform = observer( + ({ has_dxtrade_accounts = false, has_mt5_accounts = false }: TPasswordsPlatformProps) => { + const { mutate } = useMutation('verify_email'); + + const { + client: { email }, + } = useStore(); + + const [identifier, setIdentifier] = React.useState(''); + const [is_sent_email_modal_open, setIsSentEmailModalOpen] = React.useState(false); + + const platform_name_dxtrade = getPlatformSettings('dxtrade').name; + + const getPlatformTitle = () => { + let title = ''; + if (has_mt5_accounts) { + title = localize('Deriv MT5 Password'); + } else if (has_dxtrade_accounts) { + title = localize('{{platform_name_dxtrade}} Password', { platform_name_dxtrade }); + } + return title; + }; + + const onClickSendEmail = (cfd_platform?: string) => { + const password_reset_code = + cfd_platform === CFD_PLATFORMS.MT5 + ? 'trading_platform_mt5_password_reset' + : 'trading_platform_dxtrade_password_reset'; + + mutate({ + payload: { + verify_email: email, + type: password_reset_code, + + url_parameters: { + redirect_to: 3, + }, + }, + }); + + setIdentifier(cfd_platform ?? ''); + setIsSentEmailModalOpen(true); + }; + + return ( + + +
+ {has_mt5_accounts && ( + + } + /> + )} + {has_dxtrade_accounts && ( + + } + /> + )} + setIsSentEmailModalOpen(false)} + onClickSendEmail={onClickSendEmail} + is_modal_when_mobile + /> +
+
+ ); + } +); +export default PasswordsPlatform; diff --git a/packages/account/src/Sections/Security/Passwords/passwords.jsx b/packages/account/src/Sections/Security/Passwords/passwords.jsx deleted file mode 100644 index 719d784a160c..000000000000 --- a/packages/account/src/Sections/Security/Passwords/passwords.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Loading } from '@deriv/components'; -import { observer, useStore } from '@deriv/stores'; -import DerivPassword from './deriv-password.jsx'; -import DerivEmail from './deriv-email.jsx'; -import PasswordsPlatform from './passwords-platform.jsx'; - -const Passwords = observer(() => { - const [is_loading, setIsLoading] = React.useState(true); - const { client, ui, common, traders_hub } = useStore(); - const { - is_populating_mt5_account_list, - is_populating_dxtrade_account_list, - is_social_signup, - email, - social_identity_provider, - mt5_login_list, - is_mt5_password_not_set, - dxtrade_accounts_list, - is_dxtrade_password_not_set, - } = client; - const { is_from_derivgo } = common; - const { is_eu_user, financial_restricted_countries } = traders_hub; - const { is_dark_mode_on } = ui; - React.useEffect(() => { - if ( - is_populating_mt5_account_list === false && - is_populating_dxtrade_account_list === false && - is_social_signup !== undefined - ) { - setIsLoading(false); - } - }, [is_populating_mt5_account_list, is_populating_dxtrade_account_list, is_social_signup]); - - if (is_loading) { - return ; - } - - return ( -
- - - {!is_from_derivgo && (mt5_login_list?.length > 0 || !is_mt5_password_not_set) && ( - 0 || !is_mt5_password_not_set} - /> - )} - {!is_from_derivgo && (dxtrade_accounts_list?.length > 0 || !is_dxtrade_password_not_set) && ( - 0 || !is_dxtrade_password_not_set} - /> - )} -
- ); -}); - -export default Passwords; diff --git a/packages/account/src/Sections/Security/Passwords/passwords.tsx b/packages/account/src/Sections/Security/Passwords/passwords.tsx new file mode 100644 index 000000000000..59a4dfde3931 --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/passwords.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Loading } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; +import DerivPassword from './deriv-password'; +import DerivEmail from './deriv-email'; +import PasswordsPlatform from './passwords-platform'; + +/** + * Displays the Email, Password, section under Account settings. + * @name Passwords + * @returns {React.ReactNode} + */ +const Passwords = observer(() => { + const { client, common } = useStore(); + const { + is_populating_mt5_account_list, + is_populating_dxtrade_account_list, + is_social_signup, + mt5_login_list, + is_mt5_password_not_set, + dxtrade_accounts_list, + is_dxtrade_password_not_set, + } = client; + const { is_from_derivgo } = common; + + const [is_loading, setIsLoading] = React.useState(true); + const has_mt5_accounts = mt5_login_list?.length > 0 || !is_mt5_password_not_set; + const has_dxtrade_accounts = dxtrade_accounts_list?.length > 0 || !is_dxtrade_password_not_set; + + React.useEffect(() => { + if ( + is_populating_mt5_account_list === false && + is_populating_dxtrade_account_list === false && + is_social_signup !== undefined + ) { + setIsLoading(false); + } + }, [is_populating_mt5_account_list, is_populating_dxtrade_account_list, is_social_signup]); + + if (is_loading) { + return ; + } + + return ( +
+ + + {!is_from_derivgo && has_mt5_accounts && } + {!is_from_derivgo && has_dxtrade_accounts && ( + + )} +
+ ); +}); + +export default Passwords; diff --git a/packages/account/src/Sections/Security/Passwords/platform-description.tsx b/packages/account/src/Sections/Security/Passwords/platform-description.tsx new file mode 100644 index 000000000000..ecbfbff8dad2 --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/platform-description.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Localize } from '@deriv/translations'; + +type TPlatformDescription = { + brand_website_name: string; + platform_values: { + platform_name_trader: string; + platform_name_dbot: string; + platform_name_smarttrader: string; + platform_name_go: string; + platform_name_ctrader: string; + }; + is_eu_user: boolean; + financial_restricted_countries: boolean; +}; + +/** + * Renders description for the platforms. + * @name PlatformDescription + * @param brand_website_name - Name of the website + * @param platform_values - Object containing platform names + * @param is_eu_user - Boolean value to check if user is from EU + * @param financial_restricted_countries - Boolean value to check if user is from a restricted country + * @returns Returns a react node + */ +const PlatformDescription = ({ + brand_website_name, + platform_values, + is_eu_user, + financial_restricted_countries, +}: TPlatformDescription) => { + const { + platform_name_trader, + platform_name_dbot, + platform_name_smarttrader, + platform_name_go, + platform_name_ctrader, + } = platform_values; + if (is_eu_user) { + return ( + Deriv password to log in to {{brand_website_name}} and {{platform_name_trader}}.' + } + components={[]} + values={{ + brand_website_name, + platform_name_trader, + }} + /> + ); + } else if (financial_restricted_countries) { + return ( + Deriv password to log in to {{brand_website_name}}, {{platform_name_trader}} and {{platform_name_go}}.' + } + components={[]} + values={{ + brand_website_name, + platform_name_trader, + platform_name_go, + }} + /> + ); + } + return ( + Deriv password to log in to {{brand_website_name}}, {{platform_name_go}}, {{platform_name_trader}}, {{platform_name_smarttrader}}, {{platform_name_dbot}} and {{platform_name_ctrader}}.' + } + components={[]} + values={{ + brand_website_name, + platform_name_trader, + platform_name_dbot, + platform_name_smarttrader, + platform_name_go, + platform_name_ctrader, + }} + /> + ); +}; + +export default PlatformDescription; diff --git a/packages/account/src/Sections/Security/Passwords/platform-partials.tsx b/packages/account/src/Sections/Security/Passwords/platform-partials.tsx new file mode 100644 index 000000000000..92e03a2f108c --- /dev/null +++ b/packages/account/src/Sections/Security/Passwords/platform-partials.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Button, Icon, Popover, Text } from '@deriv/components'; +import { getPlatformSettings, CFD_PLATFORMS } from '@deriv/shared'; +import { localize } from '@deriv/translations'; + +type TPlatformPartialsProps = { + description: JSX.Element; + type: typeof CFD_PLATFORMS[keyof typeof CFD_PLATFORMS]; + handleClick: (type: typeof CFD_PLATFORMS[keyof typeof CFD_PLATFORMS]) => void; +}; + +/** + * Component for displaying a change password instructions for a platform + * @name PlatformPartials + * @param description - The description of the platform + * @param type - The type of the platform + * @param handleClick - The function to call when the button is clicked + * @returns React.ReactNode + */ +const PlatformPartials = ({ description, type, handleClick }: TPlatformPartialsProps) => { + const platform_config = getPlatformSettings(type); + + return ( + + + {description} + +
+ + + +
+
+ ); +}; + +export default PlatformPartials; diff --git a/packages/account/src/Types/common.type.ts b/packages/account/src/Types/common.type.ts index eb813d8eb788..dcf351c133a4 100644 --- a/packages/account/src/Types/common.type.ts +++ b/packages/account/src/Types/common.type.ts @@ -1,7 +1,7 @@ /** Add types that are shared between components */ import React from 'react'; -import { Authorize, IdentityVerificationAddDocumentResponse } from '@deriv/api-types'; import { Redirect } from 'react-router-dom'; +import { Authorize, IdentityVerificationAddDocumentResponse } from '@deriv/api-types'; import { Platforms } from '@deriv/shared'; export type TToken = { @@ -104,15 +104,6 @@ type TIdentity = { }; }; -export type TResidenseList = { - identity: TIdentity; - phone_idd: string; - tin_format: string[]; - disabled: string; - text: string; - value: string; -}; - export type TFile = { path: string; lastModified: number; diff --git a/packages/api/src/hooks/index.ts b/packages/api/src/hooks/index.ts index 676110c7fc2a..db9c782955b0 100644 --- a/packages/api/src/hooks/index.ts +++ b/packages/api/src/hooks/index.ts @@ -35,3 +35,4 @@ export { default as useTradingPlatformPasswordChange } from './useTradingPlatfor export { default as useTransactions } from './useTransactions'; export { default as useTransferBetweenAccounts } from './useTransferBetweenAccounts'; export { default as useWalletAccountsList } from './useWalletAccountsList'; +export { default as useVerifyEmail } from './useVerifyEmail'; diff --git a/packages/components/src/components/date-picker/date-picker.tsx b/packages/components/src/components/date-picker/date-picker.tsx index f3934fad7ec6..48f18e94678e 100644 --- a/packages/components/src/components/date-picker/date-picker.tsx +++ b/packages/components/src/components/date-picker/date-picker.tsx @@ -7,12 +7,8 @@ import MobileWrapper from '../mobile-wrapper'; import DesktopWrapper from '../desktop-wrapper'; import { useOnClickOutside } from '../../hooks/use-onclickoutside'; import { MomentInput } from 'moment'; +import { TDatePickerOnChangeEvent } from '../types'; -type TDatePickerOnChangeEvent = { - date?: string; - duration?: number | null | string; - target?: { name: string; value: number | string | moment.Moment | null }; -}; type TDatePicker = Omit< React.ComponentProps & React.ComponentProps & React.ComponentProps, 'value' | 'onSelect' | 'is_datepicker_visible' | 'placement' | 'style' | 'calendar_el_ref' | 'parent_ref' diff --git a/packages/components/src/components/date-picker/index.ts b/packages/components/src/components/date-picker/index.ts index e1fd3c52ef90..adfc2a303a54 100644 --- a/packages/components/src/components/date-picker/index.ts +++ b/packages/components/src/components/date-picker/index.ts @@ -1,4 +1,3 @@ import DatePicker from './date-picker'; import './date-picker.scss'; - export default DatePicker; diff --git a/packages/components/src/components/send-email-template/send-email-template.tsx b/packages/components/src/components/send-email-template/send-email-template.tsx index 955916de4707..e3273b37337d 100644 --- a/packages/components/src/components/send-email-template/send-email-template.tsx +++ b/packages/components/src/components/send-email-template/send-email-template.tsx @@ -11,8 +11,8 @@ type TSendEmailTemplate = { live_chat?: React.ReactNode; onClickSendEmail: () => void; resend_timeout?: number; - subtitle?: string; - title: string; + subtitle?: React.ReactNode; + title: React.ReactNode; txt_resend_in: string; txt_resend: string; }; diff --git a/packages/components/src/components/types/common.types.ts b/packages/components/src/components/types/common.types.ts index 65048a670689..df3b6a82b70a 100644 --- a/packages/components/src/components/types/common.types.ts +++ b/packages/components/src/components/types/common.types.ts @@ -23,3 +23,9 @@ export type TTableRowItem = export type TRow = { [key: string]: any }; export type TPassThrough = { isTopUp: (item: TRow) => boolean }; + +export type TDatePickerOnChangeEvent = { + date?: string; + duration?: number | null | string; + target?: { name?: string; value?: number | string | moment.Moment | null }; +}; diff --git a/packages/components/src/components/types/index.ts b/packages/components/src/components/types/index.ts index 7911e1a631d9..98187945e4e1 100644 --- a/packages/components/src/components/types/index.ts +++ b/packages/components/src/components/types/index.ts @@ -2,7 +2,7 @@ import { TIconsManifest, TIconProps } from './icons.types'; import { TAccordionProps, TAccordionItem } from './accordion.types'; import { TMultiStepProps, TMultiStepRefProps } from './multi-step.types'; import { TPopoverProps } from './popover.types'; -import { TGetCardLables, TGetContractTypeDisplay } from './common.types'; +import { TGetCardLables, TGetContractTypeDisplay, TDatePickerOnChangeEvent } from './common.types'; import { TErrorMessages, TGetContractPath, TToastConfig } from './contract.types'; export type { @@ -18,4 +18,5 @@ export type { TGetContractPath, TToastConfig, TGetContractTypeDisplay, + TDatePickerOnChangeEvent, }; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index c2e7729236ed..56937ff875a7 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -114,3 +114,4 @@ export { default as UnhandledErrorModal } from './components/unhandled-error-mod export { default as VerticalTab } from './components/vertical-tab'; export { default as Wizard } from './components/wizard'; export * from './hooks'; +export * from './components/types'; diff --git a/packages/core/src/App/Containers/RealAccountSignup/account-wizard-form.js b/packages/core/src/App/Containers/RealAccountSignup/account-wizard-form.js index 7fb55cf09287..c412cda01bd2 100644 --- a/packages/core/src/App/Containers/RealAccountSignup/account-wizard-form.js +++ b/packages/core/src/App/Containers/RealAccountSignup/account-wizard-form.js @@ -1,4 +1,5 @@ import { + FinancialDetails, PersonalDetails, TermsOfUse, TradingAssessmentNewUser, @@ -10,18 +11,15 @@ import { tradingAssessmentConfig, } from '@deriv/account'; -import AddressDetails from './address-details'; -import CurrencySelector from './currency-selector.jsx'; -import FinancialDetails from './financial-details.jsx'; +import AddressDetails from '@deriv/account/src/Components/address-details'; +import CurrencySelector from '@deriv/account/src/Components/currency-selector'; const isMaltaAccount = ({ real_account_signup_target }) => real_account_signup_target === 'maltainvest'; -const shouldShowPersonalAndAddressDetailsAndCurrency = ({ real_account_signup_target }) => - real_account_signup_target !== 'samoa'; export const getItems = props => [ - ...(shouldShowPersonalAndAddressDetailsAndCurrency(props) ? [currencySelectorConfig(props, CurrencySelector)] : []), - ...(shouldShowPersonalAndAddressDetailsAndCurrency(props) ? [personalDetailsConfig(props, PersonalDetails)] : []), - ...(shouldShowPersonalAndAddressDetailsAndCurrency(props) ? [addressDetailsConfig(props, AddressDetails)] : []), + currencySelectorConfig(props, CurrencySelector), + personalDetailsConfig(props, PersonalDetails), + addressDetailsConfig(props, AddressDetails), ...(isMaltaAccount(props) ? [tradingAssessmentConfig(props, TradingAssessmentNewUser)] : []), ...(isMaltaAccount(props) ? [financialDetailsConfig(props, FinancialDetails)] : []), termsOfUseConfig(props, TermsOfUse), diff --git a/packages/core/src/App/Containers/RealAccountSignup/address-details.jsx b/packages/core/src/App/Containers/RealAccountSignup/address-details.jsx deleted file mode 100644 index 7b91511c5fe6..000000000000 --- a/packages/core/src/App/Containers/RealAccountSignup/address-details.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import { AddressDetails } from '@deriv/account'; -import { connect } from 'Stores/connect'; - -export default connect(({ client }) => ({ - is_gb_residence: client.residence === 'gb', - fetchStatesList: client.fetchStatesList, - states_list: client.states_list, -}))(AddressDetails); diff --git a/packages/core/src/App/Containers/RealAccountSignup/currency-selector.jsx b/packages/core/src/App/Containers/RealAccountSignup/currency-selector.jsx deleted file mode 100644 index a8f23d58b27d..000000000000 --- a/packages/core/src/App/Containers/RealAccountSignup/currency-selector.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import { CurrencySelector } from '@deriv/account'; -import { connect } from 'Stores/connect'; -import './currency-selector.scss'; - -export default connect(({ client, ui }) => ({ - currencies: client.currencies_list, - has_currency: !!client.currency, - has_real_account: client.has_active_real_account, - legal_allowed_currencies: client.upgradeable_currencies, - real_account_signup: ui.real_account_signup, - resetRealAccountSignupParams: ui.resetRealAccountSignupParams, - selectable_currencies: client.selectable_currencies, - available_crypto_currencies: client.available_crypto_currencies, - real_account_signup_target: ui.real_account_signup_target, - is_dxtrade_allowed: client.is_dxtrade_allowed, - is_mt5_allowed: client.is_mt5_allowed, - has_fiat: client.has_fiat, - accounts: client.accounts, - is_eu: client.is_eu, -}))(CurrencySelector); diff --git a/packages/core/src/App/Containers/RealAccountSignup/financial-details.jsx b/packages/core/src/App/Containers/RealAccountSignup/financial-details.jsx deleted file mode 100644 index 274cfa2e9824..000000000000 --- a/packages/core/src/App/Containers/RealAccountSignup/financial-details.jsx +++ /dev/null @@ -1,8 +0,0 @@ -import { FinancialDetails } from '@deriv/account'; -import { connect } from 'Stores/connect'; - -export default connect(({ client }) => ({ - is_gb_residence: client.residence === 'gb', - fetchStatesList: client.fetchStatesList, - states_list: client.states_list, -}))(FinancialDetails); diff --git a/packages/core/src/App/Containers/RealAccountSignup/set-currency.jsx b/packages/core/src/App/Containers/RealAccountSignup/set-currency.jsx index 93367176e2c1..08a2fef65240 100644 --- a/packages/core/src/App/Containers/RealAccountSignup/set-currency.jsx +++ b/packages/core/src/App/Containers/RealAccountSignup/set-currency.jsx @@ -5,7 +5,7 @@ import { currencySelectorConfig } from '@deriv/account'; import { website_name, generateValidationFunction } from '@deriv/shared'; import { Text } from '@deriv/components'; import { connect } from 'Stores/connect'; -import CurrencySelector from './currency-selector'; +import CurrencySelector from '@deriv/account/src/Components/currency-selector'; import LoadingModal from './real-account-signup-loader.jsx'; import 'Sass/set-currency.scss'; import 'Sass/change-account.scss'; diff --git a/packages/core/src/Stores/ui-store.js b/packages/core/src/Stores/ui-store.js index 37cf48411a20..0944dcd95815 100644 --- a/packages/core/src/Stores/ui-store.js +++ b/packages/core/src/Stores/ui-store.js @@ -303,6 +303,7 @@ export default class UIStore extends BaseStore { init: action.bound, installWithDeferredPrompt: action.bound, is_account_switcher_disabled: computed, + is_desktop: computed, is_mobile: computed, is_tablet: computed, is_warning_scam_message_modal_visible: computed, @@ -479,6 +480,11 @@ export default class UIStore extends BaseStore { return MAX_MOBILE_WIDTH < this.screen_width && this.screen_width <= MAX_TABLET_WIDTH; } + get is_desktop() { + // TODO: remove tablet once there is a design for the specific size. + return this.is_tablet || this.screen_width > MAX_TABLET_WIDTH; + } + get is_account_switcher_disabled() { return !!this.account_switcher_disabled_message; } diff --git a/packages/hooks/src/__tests__/useIsAccountStatusPresent.spec.tsx b/packages/hooks/src/__tests__/useIsAccountStatusPresent.spec.tsx index eeffe186e82a..05153b421de5 100644 --- a/packages/hooks/src/__tests__/useIsAccountStatusPresent.spec.tsx +++ b/packages/hooks/src/__tests__/useIsAccountStatusPresent.spec.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { mockStore, StoreProvider } from '@deriv/stores'; import { renderHook } from '@testing-library/react-hooks'; -import { useIsAccountStatusPresent } from '../useIsAccountStatusPresent'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import useIsAccountStatusPresent from '../useIsAccountStatusPresent'; describe('useIsAccountStatusPresent', () => { it('should return false when the status is not present', () => { diff --git a/packages/hooks/src/__tests__/useStatesList.spec.tsx b/packages/hooks/src/__tests__/useStatesList.spec.tsx new file mode 100644 index 000000000000..6d174de438ba --- /dev/null +++ b/packages/hooks/src/__tests__/useStatesList.spec.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { APIProvider, useFetch } from '@deriv/api'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import useStatesList from '../useStatesList'; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useFetch: jest.fn(), +})); + +const mockUseFetch = useFetch as jest.MockedFunction>; + +describe('useStatesList', () => { + const mock = mockStore({}); + + const wrapper = ({ children }: { children: JSX.Element }) => ( + + {children} + + ); + + it('should return an empty array when the store is not ready', () => { + // @ts-expect-error need to come up with a way to mock the return type of useFetch + mockUseFetch.mockReturnValue({ + data: { + states_list: [], + }, + }); + const { result } = renderHook(() => useStatesList('in'), { wrapper }); + + expect(result.current.data).toHaveLength(0); + }); + + it('should return data fetched along with correct status', () => { + // @ts-expect-error need to come up with a way to mock the return type of useFetch + mockUseFetch.mockReturnValue({ + data: { + states_list: [ + { text: 'state 1', value: 's1' }, + { text: 'state 2', value: 's2' }, + ], + }, + isFetched: true, + }); + const { result } = renderHook(() => useStatesList('in'), { wrapper }); + expect(result.current.isFetched).toBeTruthy(); + }); +}); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 4b22cbce43d4..fd9e68b0f16c 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -46,7 +46,8 @@ export { default as usePlatformRealAccounts } from './usePlatformRealAccounts'; export { default as useRealSTPAccount } from './useRealSTPAccount'; export { default as useTotalAccountBalance } from './useTotalAccountBalance'; export { default as useVerifyEmail } from './useVerifyEmail'; -export { useIsAccountStatusPresent } from './useIsAccountStatusPresent'; +export { default as useIsAccountStatusPresent } from './useIsAccountStatusPresent'; +export { default as useStatesList } from './useStatesList'; export { default as useP2PConfig } from './useP2PConfig'; export { default as useIsClientHighRiskForMT5 } from './useIsClientHighRiskForMT5'; export { default as useCFDCanGetMoreMT5Accounts } from './useCFDCanGetMoreMT5Accounts'; diff --git a/packages/hooks/src/useIsAccountStatusPresent.ts b/packages/hooks/src/useIsAccountStatusPresent.ts index f6b467cd0089..db9144b6e058 100644 --- a/packages/hooks/src/useIsAccountStatusPresent.ts +++ b/packages/hooks/src/useIsAccountStatusPresent.ts @@ -48,7 +48,13 @@ const AccountStatusList = [ type TAccountStatus = typeof AccountStatusList[number]; -export const useIsAccountStatusPresent = (status: TAccountStatus) => { +/** + * Custom hook to check if a particular account status is present. + * @name useIsAccountStatusPresent + * @param status of the account to check + * @returns boolean + */ +const useIsAccountStatusPresent = (status: TAccountStatus) => { const { client: { account_status }, } = useStore(); @@ -57,3 +63,5 @@ export const useIsAccountStatusPresent = (status: TAccountStatus) => { return React.useMemo(() => status_list?.includes(status) ?? false, [status_list, status]); }; + +export default useIsAccountStatusPresent; diff --git a/packages/hooks/src/useStatesList.ts b/packages/hooks/src/useStatesList.ts new file mode 100644 index 000000000000..ad5086745a1c --- /dev/null +++ b/packages/hooks/src/useStatesList.ts @@ -0,0 +1,16 @@ +import { useFetch } from '@deriv/api'; +/** + * Custom hook to get states list for a particular country. + * @returns an object with the states list and the options to manage API response. + */ +const useStatesList = (country: string) => { + const { data, ...rest } = useFetch('states_list', { + // @ts-expect-error The `states_list` type from `@deriv/api-types` is not correct. + // The type should be `string`, but it's an alias to string type. + payload: { states_list: country }, + }); + + return { ...rest, data: data?.states_list ?? [] }; +}; + +export default useStatesList; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 963f4e3419bb..2faa767f9621 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -35,3 +35,4 @@ export * from './services'; export * from './utils/helpers'; export * from './utils/constants'; export * from './utils/loader-handler'; +export * from './utils/types'; diff --git a/packages/shared/src/utils/config/adapters.ts b/packages/shared/src/utils/config/adapters.ts index 0b962cb0d005..7ec4efc3e265 100644 --- a/packages/shared/src/utils/config/adapters.ts +++ b/packages/shared/src/utils/config/adapters.ts @@ -21,6 +21,7 @@ type TIDVFormValues = { /** * Formats the IDV form values to be sent to the API + * @name formatIDVFormValues * @param idv_form_value - Formik values of the IDV form * @param country_code - Country code of the user * @returns IDV form values diff --git a/packages/shared/src/utils/currency/currency.ts b/packages/shared/src/utils/currency/currency.ts index d26b7eb207e8..6bf986764e61 100644 --- a/packages/shared/src/utils/currency/currency.ts +++ b/packages/shared/src/utils/currency/currency.ts @@ -42,7 +42,7 @@ const crypto_currencies_display_order = [ 'USDK', ]; -export const reorderCurrencies = (list: Array<{ value: string; type: string }>, type = 'fiat') => { +export const reorderCurrencies = (list: Array, type = 'fiat') => { const new_order = type === 'fiat' ? fiat_currencies_display_order : crypto_currencies_display_order; return list.sort((a, b) => { diff --git a/packages/shared/src/utils/types.ts b/packages/shared/src/utils/types.ts new file mode 100644 index 000000000000..51ebd1123f8f --- /dev/null +++ b/packages/shared/src/utils/types.ts @@ -0,0 +1,3 @@ +import { Jurisdiction } from './constants'; + +export type TBrokerCodes = typeof Jurisdiction[keyof typeof Jurisdiction]; diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index be3884422813..546f4bc0984b 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -19,7 +19,6 @@ const mock = (): TStores & { is_mock: boolean } => { account_settings: {}, account_type: 'virtual', accounts: {}, - is_social_signup: false, active_account_landing_company: '', trading_platform_available_accounts: [], account_limits: {}, @@ -125,6 +124,7 @@ const mock = (): TStores & { is_mock: boolean } => { setCFDScore: jest.fn(), getLimits: jest.fn(), has_active_real_account: false, + has_fiat: false, has_logged_out: false, has_maltainvest_account: false, has_restricted_mt5_account: false, @@ -133,6 +133,7 @@ const mock = (): TStores & { is_mock: boolean } => { is_authorize: false, is_deposit_lock: false, is_dxtrade_allowed: false, + is_dxtrade_password_not_set: false, is_eu: false, is_eu_country: false, has_residence: false, @@ -144,7 +145,10 @@ const mock = (): TStores & { is_mock: boolean } => { is_landing_company_loaded: false, is_logged_in: false, is_logging_in: false, + is_mt5_password_not_set: false, is_pending_proof_of_ownership: false, + is_populating_dxtrade_account_list: false, + is_social_signup: false, is_single_currency: false, is_switching: false, is_tnc_needed: false, @@ -206,6 +210,7 @@ const mock = (): TStores & { is_mock: boolean } => { setVisibilityRealityCheck: jest.fn(), setP2pAdvertiserInfo: jest.fn(), setPreSwitchAccount: jest.fn(), + social_identity_provider: '', switched: false, switch_broadcast: false, switchEndSignal: jest.fn(), @@ -224,6 +229,7 @@ const mock = (): TStores & { is_mock: boolean } => { is_populating_mt5_account_list: false, landing_companies: {}, landing_company: {}, + upgradeable_currencies: [], getChangeableFields: jest.fn(), isAccountOfTypeDisabled: jest.fn(), is_mt5_allowed: false, @@ -307,9 +313,13 @@ const mock = (): TStores & { is_mock: boolean } => { is_closing_create_real_account_modal: false, is_dark_mode_on: false, is_language_settings_modal_on: false, + is_desktop: false, + is_app_disabled: false, + has_only_forward_starting_contracts: false, header_extension: null, is_link_expired_modal_visible: false, is_mobile: false, + is_tablet: false, is_mobile_language_menu_open: false, is_positions_drawer_on: false, is_reports_visible: false, @@ -324,8 +334,16 @@ const mock = (): TStores & { is_mock: boolean } => { toggleCashier: jest.fn(), setDarkMode: jest.fn(), setReportsTabIndex: jest.fn(), - has_only_forward_starting_contracts: false, has_real_account_signup_ended: false, + real_account_signup_target: '', + real_account_signup: { + active_modal_index: -1, + previous_currency: '', + current_currency: '', + success_message: '', + error_message: '', + }, + resetRealAccountSignupParams: jest.fn(), notification_messages_ui: jest.fn(), openPositionsDrawer: jest.fn(), openRealAccountSignup: jest.fn(), @@ -352,7 +370,6 @@ const mock = (): TStores & { is_mock: boolean } => { setSubSectionIndex: jest.fn(), sub_section_index: 0, toggleReadyToDepositModal: jest.fn(), - is_tablet: false, is_ready_to_deposit_modal_visible: false, is_real_acc_signup_on: false, is_need_real_account_for_cashier_modal_visible: false, diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 7d3d8555799d..3b4c60be66d5 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -14,6 +14,7 @@ import type { SetFinancialAssessmentRequest, SetFinancialAssessmentResponse, StatesList, + WebsiteStatus, ContractUpdateHistory, Transaction, ActiveSymbols, @@ -242,6 +243,15 @@ type TDXTraderStatusServerType = Record<'all' | 'demo' | 'real', number>; type TMt5StatusServer = Record<'demo' | 'real', TMt5StatusServerType[]>; +type RealAccountSignupSettings = { + active_modal_index: number; + current_currency: string; + error_code?: string; + error_details?: string | Record; + error_message: string; + previous_currency: string; + success_message: string; +}; type TCountryStandpoint = { is_belgium: boolean; is_france: boolean; @@ -264,7 +274,7 @@ type TClientStore = { }; account_list: TAccountsList; account_status: GetAccountStatus; - available_crypto_currencies: string[]; + available_crypto_currencies: Array; balance?: string | number; can_change_fiat_currency: boolean; clients_country: string; @@ -293,6 +303,7 @@ type TClientStore = { is_social_signup: boolean; has_residence: boolean; is_authorize: boolean; + is_dxtrade_password_not_set: boolean; is_financial_account: boolean; is_financial_assessment_needed: boolean; is_financial_information_incomplete: boolean; @@ -301,7 +312,9 @@ type TClientStore = { is_logged_in: boolean; is_logging_in: boolean; is_low_risk: boolean; + is_mt5_password_not_set: boolean; is_pending_proof_of_ownership: boolean; + is_populating_dxtrade_account_list: boolean; is_switching: boolean; is_tnc_needed: boolean; is_trading_experience_incomplete: boolean; @@ -336,9 +349,11 @@ type TClientStore = { setP2pAdvertiserInfo: () => void; setPreSwitchAccount: (status?: boolean) => void; switchAccount: (value?: string) => Promise; + social_identity_provider: string; switched: boolean; switch_broadcast: boolean; switchEndSignal: () => void; + upgradeable_currencies: Array; verification_code: { payment_agent_withdraw: string; payment_withdraw: string; @@ -392,6 +407,7 @@ type TClientStore = { has_account_error_in_mt5_demo_list: boolean; has_account_error_in_dxtrade_real_list: boolean; has_account_error_in_dxtrade_demo_list: boolean; + has_fiat: boolean; is_fully_authenticated: boolean; states_list: StatesList; /** @deprecated Use `useCurrencyConfig` or `useCurrentCurrencyConfig` from `@deriv/hooks` package instead. */ @@ -478,8 +494,11 @@ type TUiStore = { is_reports_visible: boolean; is_route_modal_on: boolean; is_language_settings_modal_on: boolean; + is_desktop: boolean; + is_app_disabled: boolean; is_link_expired_modal_visible: boolean; is_mobile: boolean; + is_tablet: boolean; is_mobile_language_menu_open: boolean; is_positions_drawer_on: boolean; is_services_error_visible: boolean; @@ -523,7 +542,6 @@ type TUiStore = { toggleServicesErrorModal: (is_visible: boolean) => void; toggleSetCurrencyModal: () => void; toggleShouldShowRealAccountsList: (value: boolean) => void; - is_tablet: boolean; removeToast: (key: string) => void; is_ready_to_deposit_modal_visible: boolean; reports_route_tab_index: number; @@ -540,6 +558,7 @@ type TUiStore = { is_top_up_virtual_open: boolean; is_top_up_virtual_in_progress: boolean; is_top_up_virtual_success: boolean; + real_account_signup_target: string; closeSuccessTopUpModal: () => void; closeTopUpModal: () => void; is_cfd_reset_password_modal_enabled: boolean; @@ -548,6 +567,8 @@ type TUiStore = { is_accounts_switcher_on: boolean; openTopUpModal: () => void; is_reset_trading_password_modal_visible: boolean; + real_account_signup: RealAccountSignupSettings; + resetRealAccountSignupParams: () => void; setResetTradingPasswordModalOpen: () => void; populateHeaderExtensions: (header_items: JSX.Element | null) => void; populateSettingsExtensions: (menu_items: Array | null) => void;