From 8a1b112a5773df4e9bc2c9b00ca111eec155b9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cyauheni-kryzhyk-deriv=E2=80=9D?= <“yauheni@deriv.me”> Date: Thu, 1 Jun 2023 17:29:11 +0300 Subject: [PATCH 001/115] chore: new pr for wall 400 --- .../account/src/Assets/ic-poi-dob-example.svg | 1 + .../src/Assets/ic-poi-name-example.svg | 1 + .../src/Components/form-body/form-body.tsx | 11 +- .../forms/__tests__/idv-form.spec.tsx | 124 +++ .../account/src/Components/forms/idv-form.tsx | 23 +- .../forms/personal-details-form.jsx | 58 +- .../__tests__/personal-details.spec.js | 98 +++ .../personal-details/personal-details.jsx | 21 +- .../__tests__/idv-document-submit.spec.tsx | 23 +- .../idv-document-submit.tsx | 163 ++-- .../__tests__/idv-expired.spec.tsx | 54 -- .../idv-status/idv-expired/idv-expired.tsx | 36 - .../poi/idv-status/idv-expired/index.js | 3 - .../idv-failed/__tests__/idv-failed.spec.tsx | 94 +++ .../poi/idv-status/idv-failed/idv-failed.tsx | 377 +++++++++ .../poi/idv-status/idv-failed/index.js | 3 + .../__tests__/idv-limited.spec.tsx | 8 +- .../idv-status/idv-limited/idv-limited.tsx | 6 +- .../poi/idv-status/idv-limited/index.js | 4 +- .../__tests__/idv-rejected.spec.tsx | 22 - .../idv-status/idv-rejected/idv-rejected.tsx | 32 - .../poi/idv-status/idv-rejected/index.js | 3 - .../__tests__/idv-submit-complete.spec.tsx | 91 ++- .../idv-submit-complete.tsx | 43 +- ...oi-confirm-with-example-form-container.tsx | 32 +- .../idv-doc-submit-on-signup.tsx | 56 +- packages/account/src/Helpers/utils.ts | 19 +- .../Verification/ProofOfIdentity/idv.jsx | 18 +- .../proof-of-identity-container-for-mt5.jsx | 7 - .../proof-of-identity-container.jsx | 13 +- .../proof-of-identity-submission-for-mt5.jsx | 31 +- .../proof-of-identity-submission.jsx | 27 +- .../proof-of-identity-utils.ts | 12 +- packages/account/src/Styles/account.scss | 425 ----------- .../account/src/Types/common-prop.type.ts | 43 +- .../components/cfds-listing/cfds-listing.scss | 26 + .../components/autocomplete/autocomplete.jsx | 3 + .../_common/components/onfido-container.scss | 713 +++++++++++++++--- .../src/utils/helpers/format-response.ts | 30 + 39 files changed, 1729 insertions(+), 1025 deletions(-) create mode 100644 packages/account/src/Assets/ic-poi-dob-example.svg create mode 100644 packages/account/src/Assets/ic-poi-name-example.svg create mode 100644 packages/account/src/Components/forms/__tests__/idv-form.spec.tsx delete mode 100644 packages/account/src/Components/poi/idv-status/idv-expired/__tests__/idv-expired.spec.tsx delete mode 100644 packages/account/src/Components/poi/idv-status/idv-expired/idv-expired.tsx delete mode 100644 packages/account/src/Components/poi/idv-status/idv-expired/index.js create mode 100644 packages/account/src/Components/poi/idv-status/idv-failed/__tests__/idv-failed.spec.tsx create mode 100644 packages/account/src/Components/poi/idv-status/idv-failed/idv-failed.tsx create mode 100644 packages/account/src/Components/poi/idv-status/idv-failed/index.js delete mode 100644 packages/account/src/Components/poi/idv-status/idv-rejected/__tests__/idv-rejected.spec.tsx delete mode 100644 packages/account/src/Components/poi/idv-status/idv-rejected/idv-rejected.tsx delete mode 100644 packages/account/src/Components/poi/idv-status/idv-rejected/index.js diff --git a/packages/account/src/Assets/ic-poi-dob-example.svg b/packages/account/src/Assets/ic-poi-dob-example.svg new file mode 100644 index 000000000000..7ad66a00fa5b --- /dev/null +++ b/packages/account/src/Assets/ic-poi-dob-example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/account/src/Assets/ic-poi-name-example.svg b/packages/account/src/Assets/ic-poi-name-example.svg new file mode 100644 index 000000000000..eaaa9453759c --- /dev/null +++ b/packages/account/src/Assets/ic-poi-name-example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/account/src/Components/form-body/form-body.tsx b/packages/account/src/Components/form-body/form-body.tsx index a216f6f7ef3a..6f0c84df4c5e 100644 --- a/packages/account/src/Components/form-body/form-body.tsx +++ b/packages/account/src/Components/form-body/form-body.tsx @@ -1,21 +1,26 @@ import React from 'react'; import { ScrollbarsContainer } from 'Components/scrollbars-container/scrollbars-container'; import { Div100vhContainer, DesktopWrapper, MobileWrapper } from '@deriv/components'; +import classNames from 'classnames'; type TFormBody = { scroll_offset?: string; + className?: string; }; -export const FormBody = ({ children, scroll_offset }: React.PropsWithChildren) => ( +export const FormBody = ({ children, scroll_offset, className }: React.PropsWithChildren) => ( - + {children} {children} diff --git a/packages/account/src/Components/forms/__tests__/idv-form.spec.tsx b/packages/account/src/Components/forms/__tests__/idv-form.spec.tsx new file mode 100644 index 000000000000..fdc4666d6b6b --- /dev/null +++ b/packages/account/src/Components/forms/__tests__/idv-form.spec.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import IDVForm from '../idv-form'; +import { Formik } from 'formik'; + +jest.mock('Helpers/utils', () => ({ + ...jest.requireActual('Helpers/utils'), + getDocumentData: jest.fn((country_code, key) => { + const data = { + tc: { + document_1: { + new_display_name: '', + example_format: '5436454364243', + sample_image: '', + }, + document_2: { + new_display_name: '', + example_format: 'A-52431', + sample_image: '', + }, + }, + }; + return data[country_code][key]; + }), +})); + +jest.mock('formik', () => ({ + ...jest.requireActual('formik'), + useFormikContext: jest.fn(() => ({ + values: { + document_type: { + // display_name: 'Test document 1 name', + // format: '5436454364243', + // id: '1', + // value: 'Test document 1 name', + }, + document_number: '', + }, + errors: {}, + touched: {}, + setFieldValue: jest.fn(), + setFieldTouched: jest.fn(), + validateForm: jest.fn(), + validateField: jest.fn(), + handleBlur: jest.fn(), + handleChange: jest.fn(), + })), +})); + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + isDesktop: jest.fn(() => true), + isMobile: jest.fn(() => false), +})); + +describe('', () => { + const mock_props = { + selected_country: { + value: 'tc', + identity: { + services: { + idv: { + documents_supported: { + document_1: { + display_name: 'Test document 1 name', + format: '5436454364243', + }, + document_2: { + display_name: 'Test document 2 name', + format: 'A54321', + }, + }, + has_visual_sample: '1', + }, + }, + }, + }, + }; + + const mock_values = { + document_type: { + display_name: 'Test document 1 name', + format: '5436454364243', + id: '1', + value: 'document_1', + }, + document_number: '5436454364243', + }; + + it('should render IDVForm component', () => { + render(, { + wrapper: ({ children }) => ( + undefined}> + {() => children} + + ), + }); + + const document_type_input = screen.getByLabelText('Choose the document type'); + const document_number_input = screen.getByPlaceholderText('Enter your document number'); + + expect(document_type_input).toBeInTheDocument(); + expect(document_number_input).toBeInTheDocument(); + }); + + it('Should change the document type value when document type is changed', async () => { + render(, { + wrapper: ({ children }) => ( + undefined}> + {() => children} + + ), + }); + + const document_type_input = screen.getByLabelText('Choose the document type'); + + fireEvent.click(document_type_input); + expect(await screen.findByText('Test document 1 name')).toBeInTheDocument(); + fireEvent.blur(document_type_input); + await waitFor(() => { + expect(screen.queryByText('Test document 1 name')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/account/src/Components/forms/idv-form.tsx b/packages/account/src/Components/forms/idv-form.tsx index 48a5a5fd0c1d..4cb31bed1e26 100644 --- a/packages/account/src/Components/forms/idv-form.tsx +++ b/packages/account/src/Components/forms/idv-form.tsx @@ -1,6 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import { Field, FormikProps, FormikHandlers, FieldProps } from 'formik'; +import { Field, FormikProps, FieldProps, useFormikContext } from 'formik'; import { ResidenceList } from '@deriv/api-types'; import { localize } from '@deriv/translations'; import { formatInput, IDV_NOT_APPLICABLE_OPTION } from '@deriv/shared'; @@ -27,28 +27,18 @@ type TIDVForm = { selected_country: ResidenceList[0]; hide_hint?: boolean; class_name?: string; - can_skip_document_verification: boolean; -} & Partial & - FormikProps; + can_skip_document_verification?: boolean; +}; -const IDVForm = ({ - errors, - touched, - values, - handleBlur, - handleChange, - setFieldValue, - class_name, - selected_country, - hide_hint, - can_skip_document_verification = false, -}: TIDVForm) => { +const IDVForm = ({ class_name, selected_country, hide_hint, can_skip_document_verification = false }: TIDVForm) => { const [document_list, setDocumentList] = React.useState([]); const [document_image, setDocumentImage] = React.useState(null); const [selected_doc, setSelectedDoc] = React.useState(''); const { documents_supported: document_data, has_visual_sample } = selected_country?.identity?.services?.idv ?? {}; + const { errors, touched, values, handleBlur, handleChange, setFieldValue }: FormikProps = + useFormikContext(); const default_document = { id: '', text: '', @@ -156,7 +146,6 @@ const IDVForm = ({
{ +const PersonalDetailsForm = props => { const { is_virtual, is_mf, is_svg, is_qualified_for_idv, should_hide_helper_image, + inline_note_text, is_appstore, editable_fields = [], has_real_account, @@ -48,16 +38,19 @@ const PersonalDetailsForm = ({ account_opening_reason_list, closeRealAccountSignup, salutation_list, - is_rendered_for_onfido, + is_qualified_for_onfido, should_close_tooltip, setShouldCloseTooltip, + warning_items, + side_note, } = props; const autocomplete_value = 'none'; - const PoiNameDobExampleIcon = PoiNameDobExample; const [is_tax_residence_popover_open, setIsTaxResidencePopoverOpen] = React.useState(false); const [is_tin_popover_open, setIsTinPopoverOpen] = React.useState(false); + const { errors, touched, values, setFieldValue, handleChange, handleBlur, setFieldTouched } = useFormikContext(); + React.useEffect(() => { if (should_close_tooltip) { handleToolTipStatus(); @@ -66,7 +59,7 @@ const PersonalDetailsForm = ({ }, [should_close_tooltip, handleToolTipStatus, setShouldCloseTooltip]); const getNameAndDobLabels = () => { - const is_asterisk_needed = is_svg || is_mf || is_rendered_for_onfido || is_qualified_for_idv; + const is_asterisk_needed = is_svg || is_mf || is_qualified_for_onfido || is_qualified_for_idv; const first_name_label = is_appstore || is_asterisk_needed ? localize('First name*') : localize('First name'); const last_name_label = is_appstore ? localize('Family name*') @@ -86,7 +79,7 @@ const PersonalDetailsForm = ({ return ( ]} - /> - ); - return (
- {(is_qualified_for_idv || is_rendered_for_onfido) && !should_hide_helper_image && ( - + {(is_qualified_for_idv || is_qualified_for_onfido) && !should_hide_helper_image && ( + )} } + has_side_note={(is_qualified_for_idv || is_qualified_for_onfido) && !should_hide_helper_image} + side_note={side_note} >
{'salutation' in values && ( @@ -144,7 +130,7 @@ const PersonalDetailsForm = ({
)} - {!is_qualified_for_idv && !is_appstore && !is_rendered_for_onfido && ( + {!is_qualified_for_idv && !is_appstore && !is_qualified_for_onfido && ( )} {'salutation' in values && ( @@ -196,7 +182,7 @@ const PersonalDetailsForm = ({ data-testid='last_name' /> )} - {!is_appstore && !is_qualified_for_idv && !is_rendered_for_onfido && ( + {!is_appstore && !is_qualified_for_idv && !is_qualified_for_onfido && ( )} {'date_of_birth' in values && ( @@ -221,9 +207,7 @@ const PersonalDetailsForm = ({ jest.fn(() => 'PoiNameDobExampleImage')); jest.mock('Assets/ic-poi-name-dob-example.svg', () => jest.fn(() => 'PoiNameDobExampleImage')); @@ -26,6 +29,13 @@ jest.mock('../../real-account-signup/helpers/utils.ts', () => ({ })), })); +jest.mock('Helpers/utils', () => ({ + ...jest.requireActual('Helpers/utils'), + isDocumentTypeValid: jest.fn(() => undefined), + shouldShowIdentityInformation: jest.fn(() => false), + isAdditionalDocumentValid: jest.fn(() => undefined), +})); + const mock_warnings = {}; const mock_errors = { account_opening_reason: 'Account opening reason is required.', @@ -744,4 +754,92 @@ describe('', () => { const el_tax_residence = screen.getByTestId('selected_value'); expect(el_tax_residence).toHaveTextContent('Malta'); }); + + it('should validate idv values when a document type is selected', async () => { + shouldShowIdentityInformation.mockReturnValue(true); + const new_props = { + ...props, + is_mf: false, + value: { + ...props.value, + document_type: { + value: 'national_id', + text: 'National ID', + }, + document_number: '123456789', + }, + residence_list: [ + { + value: 'tc', + identity: { + services: { + idv: { + documents_supported: { + document_1: { + display_name: 'Test document 1 name', + format: '5436454364243', + }, + document_2: { + display_name: 'Test document 2 name', + format: 'A54321', + }, + }, + has_visual_sample: true, + }, + }, + }, + }, + ], + }; + renderwithRouter(); + + await waitFor(() => { + expect(isDocumentTypeValid).toHaveBeenCalled(); + expect(isAdditionalDocumentValid).not.toHaveBeenCalled(); + }); + }); + + it('should validate idv values along with additional docuement number when a document type is selected', async () => { + shouldShowIdentityInformation.mockReturnValue(true); + const new_props = { + ...props, + is_mf: false, + value: { + ...props.value, + document_type: { + value: 'national_id', + text: 'National ID', + additional: '12345', + }, + document_number: '123456789', + }, + residence_list: [ + { + value: 'tc', + identity: { + services: { + idv: { + documents_supported: { + document_1: { + display_name: 'Test document 1 name', + format: '5436454364243', + }, + document_2: { + display_name: 'Test document 2 name', + format: 'A54321', + }, + }, + has_visual_sample: true, + }, + }, + }, + }, + ], + }; + renderwithRouter(); + + await waitFor(() => { + expect(isAdditionalDocumentValid).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index dfd5045bee50..97345f3214a8 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -136,7 +136,7 @@ const PersonalDetails = ({ onSubmit(getCurrentStep() - 1, values, actions.setSubmitting, goToNextStep); }} > - {({ handleSubmit, errors, setFieldValue, setFieldTouched, touched, values, handleChange, handleBlur }) => ( + {({ handleSubmit, errors, values }) => ( {({ setRef, height }) => (
@@ -201,13 +195,6 @@ const PersonalDetails = ({ })} > ]} + /> + } />
diff --git a/packages/account/src/Components/poi/idv-document-submit/__tests__/idv-document-submit.spec.tsx b/packages/account/src/Components/poi/idv-document-submit/__tests__/idv-document-submit.spec.tsx index cd91cddbcb05..54dcdc1c6eab 100644 --- a/packages/account/src/Components/poi/idv-document-submit/__tests__/idv-document-submit.spec.tsx +++ b/packages/account/src/Components/poi/idv-document-submit/__tests__/idv-document-submit.spec.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { isDesktop, isMobile } from '@deriv/shared'; import IdvDocumentSubmit from '../idv-document-submit'; +import { isDocumentNumberValid } from 'Helpers/utils'; jest.mock('react-router'); jest.mock('Assets/ic-document-submit-icon.svg', () => jest.fn(() => 'DocumentSubmitLogo')); -jest.mock('Helpers/utils.ts', () => ({ - ...jest.requireActual('Helpers/utils.ts'), +jest.mock('Helpers/utils', () => ({ + ...jest.requireActual('Helpers/utils'), getDocumentData: jest.fn((country_code, key) => { const data = { tc: { @@ -24,7 +25,7 @@ jest.mock('Helpers/utils.ts', () => ({ }; return data[country_code][key]; }), - getRegex: jest.fn(() => /5436454364243/i), + isDocumentNumberValid: jest.fn(() => undefined), })); jest.mock('@deriv/shared', () => ({ @@ -58,8 +59,14 @@ describe('', () => { services: { idv: { documents_supported: { - document_1: { display_name: 'Test document 1 name', format: '5436454364243' }, - document_2: { display_name: 'Test document 2 name', format: 'A54321' }, + document_1: { + display_name: 'Test document 1 name', + format: '5436454364243', + }, + document_2: { + display_name: 'Test document 2 name', + format: 'A54321', + }, }, has_visual_sample: true, }, @@ -129,15 +136,15 @@ describe('', () => { fireEvent.change(document_type_input, { target: { value: 'Test document 2 name' } }); expect(document_number_input).toBeEnabled(); expect(screen.queryByText(/please enter the correct format/i)).not.toBeInTheDocument(); - + (isDocumentNumberValid as jest.Mock).mockReturnValueOnce('please enter your document number'); fireEvent.blur(document_number_input); expect(await screen.findByText(/please enter your document number/i)).toBeInTheDocument(); - fireEvent.keyUp(document_number_input); + (isDocumentNumberValid as jest.Mock).mockReturnValueOnce('please enter the correct format'); fireEvent.change(document_number_input, { target: { value: 'A-32523' } }); expect(await screen.findByText(/please enter the correct format/i)).toBeInTheDocument(); - fireEvent.change(document_number_input, { target: { value: '5436454364243' } }); + fireEvent.change(document_number_input, { target: { value: '5436454364234' } }); await waitFor(() => { expect(screen.queryByText(/please enter the correct format/i)).not.toBeInTheDocument(); expect(screen.queryByText(/please enter a valid ID number/i)).not.toBeInTheDocument(); diff --git a/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx b/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx index 347cb848e396..2e1ab6abab9a 100644 --- a/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx +++ b/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { Form, Formik, FormikHelpers } from 'formik'; +import { GetSettings, ResidenceList, IdentityVerificationAddDocumentResponse } from '@deriv/api-types'; import { Button } from '@deriv/components'; -import { Formik } from 'formik'; -import { localize } from '@deriv/translations'; +import { Localize, localize } from '@deriv/translations'; import { WS, IDV_NOT_APPLICABLE_OPTION, @@ -12,23 +12,46 @@ import { isDesktop, removeEmptyPropertiesFromObject, } from '@deriv/shared'; -import { documentAdditionalError, getRegex, validate, makeSettingsRequest, validateName } from 'Helpers/utils'; -import FormFooter from 'Components/form-footer'; import BackButtonIcon from 'Assets/ic-poi-back-btn.svg'; +import PoiNameDobExample from 'Assets/ic-poi-name-dob-example.svg'; +import FormFooter from 'Components/form-footer'; import IDVForm from 'Components/forms/idv-form'; import PersonalDetailsForm from 'Components/forms/personal-details-form'; import FormSubHeader from 'Components/form-sub-header'; +import { + validate, + makeSettingsRequest, + validateName, + shouldHideHelperImage, + isDocumentTypeValid, + isAdditionalDocumentValid, + isDocumentNumberValid, +} from 'Helpers/utils'; +import { TIDVForm, TPersonalDetailsForm } from 'Types'; + +type TIdvDocumentSubmit = { + handleBack: () => void; + handleViewComplete: () => void; + selected_country: ResidenceList[0]; + account_settings: GetSettings; + getChangeableFields: () => Array; +}; + +type TIdvDocumentSubmitForm = TIDVForm & TPersonalDetailsForm; const IdvDocumentSubmit = ({ handleBack, handleViewComplete, selected_country, - is_from_external, account_settings, getChangeableFields, -}) => { +}: TIdvDocumentSubmit) => { const visible_settings = ['first_name', 'last_name', 'date_of_birth']; - const form_initial_values = filterObjProperties(account_settings, visible_settings) || {}; + const side_note_image = ; + + const form_initial_values = filterObjProperties(account_settings, visible_settings) as { + [Property in keyof TPersonalDetailsForm]: string; + }; if (form_initial_values.date_of_birth) { form_initial_values.date_of_birth = toMoment(form_initial_values.date_of_birth).format('YYYY-MM-DD'); @@ -36,7 +59,7 @@ const IdvDocumentSubmit = ({ const changeable_fields = [...getChangeableFields()]; - const initial_values = { + const initial_values: TIdvDocumentSubmitForm = { document_type: { id: '', text: '', @@ -48,43 +71,8 @@ const IdvDocumentSubmit = ({ ...form_initial_values, }; - const getExampleFormat = example_format => { - return example_format ? localize('Example: ') + example_format : ''; - }; - - const shouldHideHelperImage = document_id => document_id === IDV_NOT_APPLICABLE_OPTION.id; - - const isDocumentTypeValid = document_type => { - if (!document_type?.text) { - return localize('Please select a document type.'); - } - return undefined; - }; - - const isAdditionalDocumentValid = (document_type, document_additional) => { - const error_message = documentAdditionalError(document_additional, document_type.additional?.format); - if (error_message) { - return localize(error_message) + getExampleFormat(document_type.additional?.example_format); - } - return undefined; - }; - - const isDocumentNumberValid = (document_number, document_type) => { - const is_document_number_invalid = document_number === document_type.example_format; - if (!document_number) { - return localize('Please enter your document number. ') + getExampleFormat(document_type.example_format); - } else if (is_document_number_invalid) { - return localize('Please enter a valid ID number.'); - } - const format_regex = getRegex(document_type.value); - if (!format_regex.test(document_number)) { - return localize('Please enter the correct format. ') + getExampleFormat(document_type.example_format); - } - return undefined; - }; - - const validateFields = values => { - const errors = {}; + const validateFields = (values: TIdvDocumentSubmitForm) => { + const errors: Record = {}; const { document_type, document_number, document_additional } = values; const needs_additional_document = !!document_type.additional; @@ -109,7 +97,15 @@ const IdvDocumentSubmit = ({ return removeEmptyPropertiesFromObject(errors); }; - const submitHandler = async (values, { setSubmitting, setErrors }) => { + const submitHandler = async ( + values: TIdvDocumentSubmitForm, + { + setSubmitting, + setErrors, + }: Pick, 'setSubmitting'> & { + setErrors: (props: Record) => void; + } + ) => { setSubmitting(true); const request = makeSettingsRequest(values, changeable_fields); @@ -138,46 +134,25 @@ const IdvDocumentSubmit = ({ if (submit_data.document_type === IDV_NOT_APPLICABLE_OPTION.id) { return; } - WS.send(submit_data).then(response => { - setSubmitting(false); - if (response.error) { - setErrors({ error_message: response.error.message }); - return; + WS.send(submit_data).then( + (response: IdentityVerificationAddDocumentResponse & { error: { message: string } }) => { + setSubmitting(false); + if (response.error) { + setErrors({ error_message: response.error.message }); + return; + } + handleViewComplete(); } - handleViewComplete(); - }); + ); }; return ( - {({ - dirty, - errors, - handleBlur, - handleChange, - handleSubmit, - isSubmitting, - isValid, - setFieldValue, - setFieldTouched, - touched, - values, - }) => ( -
+ {({ dirty, isSubmitting, isValid, values }) => ( +
- +
]} + /> + } />
@@ -211,7 +186,6 @@ const IdvDocumentSubmit = ({
+ )}
); }; -IdvDocumentSubmit.propTypes = { - account_settings: PropTypes.object, - getChangeableFields: PropTypes.func, - handleBack: PropTypes.func, - handleViewComplete: PropTypes.func, - is_from_external: PropTypes.bool, - selected_country: PropTypes.object, -}; - export default IdvDocumentSubmit; diff --git a/packages/account/src/Components/poi/idv-status/idv-expired/__tests__/idv-expired.spec.tsx b/packages/account/src/Components/poi/idv-status/idv-expired/__tests__/idv-expired.spec.tsx deleted file mode 100644 index a0f151090751..000000000000 --- a/packages/account/src/Components/poi/idv-status/idv-expired/__tests__/idv-expired.spec.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { screen, render, fireEvent } from '@testing-library/react'; -import { isDesktop, isMobile } from '@deriv/shared'; -import IdvExpired from '../idv-expired'; - -jest.mock('@deriv/components', () => { - const original_module = jest.requireActual('@deriv/components'); - return { - ...original_module, - Icon: jest.fn(() => mockedIcon), - }; -}); - -jest.mock('@deriv/shared', () => ({ - ...jest.requireActual('@deriv/shared'), - isDesktop: jest.fn(), - isMobile: jest.fn(), -})); - -beforeEach(() => { - (isDesktop as jest.Mock).mockReturnValue(true); - (isMobile as jest.Mock).mockReturnValue(false); - jest.clearAllMocks(); -}); - -describe('', () => { - const props = { - handleRequireSubmission: jest.fn(), - }; - - const testComponentRender = () => { - render(); - expect(screen.getByTestId('idv_expired_container')).toBeInTheDocument(); - expect(screen.getByText('mockedIcon')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument(); - }; - - it('should render IdvExpired component on desktop', () => { - testComponentRender(); - }); - - it('should render IdvExpired component on mobile', () => { - (isDesktop as jest.Mock).mockReturnValue(false); - (isMobile as jest.Mock).mockReturnValue(true); - testComponentRender(); - }); - - it('should call handleRequireSubmission when try_again button is clicked', () => { - render(); - const try_again_btn = screen.getByRole('button', { name: /try again/i }); - fireEvent.click(try_again_btn); - expect(props.handleRequireSubmission).toBeCalledTimes(1); - }); -}); diff --git a/packages/account/src/Components/poi/idv-status/idv-expired/idv-expired.tsx b/packages/account/src/Components/poi/idv-status/idv-expired/idv-expired.tsx deleted file mode 100644 index bac7ec561865..000000000000 --- a/packages/account/src/Components/poi/idv-status/idv-expired/idv-expired.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Button, Icon, Text } from '@deriv/components'; -import { isMobile } from '@deriv/shared'; -import { localize } from '@deriv/translations'; - -type TIdvExpired = { - handleRequireSubmission: () => void; -}; - -const IdvExpired = ({ handleRequireSubmission }: TIdvExpired) => { - return ( -
- - - {isMobile() ? localize('ID verification failed') : localize('Verification of document number failed')} - - - {isMobile() - ? localize('The ID you submitted is expired.') - : localize( - 'It looks like your identity document has expired. Please try again with a valid document.' - )} - -
- ); -}; - -export default IdvExpired; diff --git a/packages/account/src/Components/poi/idv-status/idv-expired/index.js b/packages/account/src/Components/poi/idv-status/idv-expired/index.js deleted file mode 100644 index 6a966351c5b5..000000000000 --- a/packages/account/src/Components/poi/idv-status/idv-expired/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import IdvExpired from './idv-expired'; - -export default IdvExpired; diff --git a/packages/account/src/Components/poi/idv-status/idv-failed/__tests__/idv-failed.spec.tsx b/packages/account/src/Components/poi/idv-status/idv-failed/__tests__/idv-failed.spec.tsx new file mode 100644 index 000000000000..f85898bef5b6 --- /dev/null +++ b/packages/account/src/Components/poi/idv-status/idv-failed/__tests__/idv-failed.spec.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import { act, render, screen, waitFor } from '@testing-library/react'; +import IdvFailed from '../idv-failed'; +import { idv_error_statuses } from '@deriv/shared'; + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + WS: { + wait: jest.fn().mockResolvedValue(true), + }, + filterObjProperties: jest.fn(() => ({ + document_type: { + id: '', + text: '', + value: '', + example_format: '', + sample_image: '', + }, + document_number: '', + first_name: '', + last_name: '', + date_of_birth: '', + })), +})); + +jest.mock('Components/forms/personal-details-form', () => jest.fn(() =>
PersonalDetailsForm
)); +jest.mock('Components/forms/idv-form', () => jest.fn(() =>
IDVForm
)); +jest.mock('Assets/ic-poi-name-example.svg', () => jest.fn(() => 'PoiNameExample')); +jest.mock('Assets/ic-poi-dob-example.svg', () => jest.fn(() => 'PoiDobExample')); +jest.mock('Assets/ic-poi-name-dob-example.svg', () => jest.fn(() => 'PoiNameDobExample')); + +describe('', () => { + const mock_props = { + getChangeableFields: jest.fn(() => []), + is_from_external: false, + residence_list: [], + handleSubmit: jest.fn(), + account_settings: { + citizen: 'gh', + }, + mismatch_status: idv_error_statuses.poi_name_mismatch, + }; + + it('should render IDVfailed component with name mismatch message', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId(idv_error_statuses.poi_name_mismatch)).toBeInTheDocument(); + expect(screen.getByText('PersonalDetailsForm')).toBeInTheDocument(); + }); + }); + + it('should render IDVfailed component with dob mismatch message', async () => { + const new_props = { ...mock_props, mismatch_status: idv_error_statuses.poi_dob_mismatch }; + render(); + + await waitFor(() => { + expect(screen.getByTestId(idv_error_statuses.poi_dob_mismatch)).toBeInTheDocument(); + expect(screen.queryByText('IDVForm')).not.toBeInTheDocument(); + }); + }); + + it('should render IDVfailed component with name & DOB mismatch message', async () => { + const new_props = { ...mock_props, mismatch_status: idv_error_statuses.poi_name_dob_mismatch }; + render(); + + await waitFor(() => { + expect(screen.getByTestId(idv_error_statuses.poi_name_dob_mismatch)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Update profile/i })).toBeInTheDocument(); + }); + }); + + it('should render IDVfailed component with expired message', async () => { + const new_props = { ...mock_props, mismatch_status: idv_error_statuses.poi_expired }; + render(); + + await waitFor(() => { + expect(screen.getByTestId(idv_error_statuses.poi_expired)).toBeInTheDocument(); + expect(screen.getByText('IDVForm')).toBeInTheDocument(); + expect(screen.getByText('PersonalDetailsForm')).toBeInTheDocument(); + }); + }); + + it('should render IDVfailed component with verification failed message', async () => { + const new_props = { ...mock_props, mismatch_status: idv_error_statuses.poi_failed }; + render(); + + await waitFor(() => { + expect(screen.getByTestId(idv_error_statuses.poi_failed)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Verify/i })).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/account/src/Components/poi/idv-status/idv-failed/idv-failed.tsx b/packages/account/src/Components/poi/idv-status/idv-failed/idv-failed.tsx new file mode 100644 index 000000000000..6594b618f47a --- /dev/null +++ b/packages/account/src/Components/poi/idv-status/idv-failed/idv-failed.tsx @@ -0,0 +1,377 @@ +import React from 'react'; +import { Form, Formik, FormikHelpers, FormikValues } from 'formik'; +import { GetSettings, IdentityVerificationAddDocumentResponse, ResidenceList } from '@deriv/api-types'; +import { Button, DesktopWrapper, HintBox, Loading, Text } from '@deriv/components'; +import { + filterObjProperties, + isEmptyObject, + removeEmptyPropertiesFromObject, + toMoment, + WS, + idv_error_statuses, + TIDVErrorStatus, + IDV_NOT_APPLICABLE_OPTION, + isMobile, +} from '@deriv/shared'; +import { Localize, localize } from '@deriv/translations'; +import PoiNameExample from 'Assets/ic-poi-name-example.svg'; +import PoiDobExample from 'Assets/ic-poi-dob-example.svg'; +import PoiNameDobExample from 'Assets/ic-poi-name-dob-example.svg'; +import FormBody from 'Components/form-body'; +import PersonalDetailsForm from 'Components/forms/personal-details-form'; +import LoadErrorMessage from 'Components/load-error-message'; +import { + isAdditionalDocumentValid, + isDocumentNumberValid, + isDocumentTypeValid, + makeSettingsRequest, + shouldHideHelperImage, + validate, + validateName, +} from 'Helpers/utils'; +import { TIDVForm, TPersonalDetailsForm } from 'Types'; +import FormSubHeader from 'Components/form-sub-header'; +import IDVForm from 'Components/forms/idv-form'; +import classNames from 'classnames'; +import FormFooter from 'Components/form-footer'; + +type TRestState = { + api_error: string; + errors?: boolean; + form_initial_values: TIdvFailedForm; + changeable_fields: string[]; +}; + +type TIdvFailed = { + account_settings: GetSettings; + getChangeableFields: () => string[]; + handleSubmit: () => void; + is_from_external: boolean; + mismatch_status: TIDVErrorStatus; + residence_list: ResidenceList; +}; + +type TIDVFailureConfig = { + required_fields: string[]; + side_note_image: JSX.Element; + failure_message: React.ReactNode; + inline_note_text: React.ReactNode; +}; + +type TIdvFailedForm = Partial & Partial; + +const IdvFailed = ({ + getChangeableFields, + is_from_external, + residence_list, + account_settings, + handleSubmit, + mismatch_status = idv_error_statuses.poi_failed, +}: TIdvFailed) => { + const [idv_failure, setIdvFailure] = React.useState({ + required_fields: [], + side_note_image: , + failure_message: null, + inline_note_text: null, + }); + const [is_loading, setIsLoading] = React.useState(true); + const [rest_state, setRestState] = React.useState({ + api_error: '', + errors: false, + form_initial_values: {}, + changeable_fields: [], + }); + + const is_document_upload_required = React.useMemo( + () => [idv_error_statuses.poi_expired, idv_error_statuses.poi_failed].includes(mismatch_status), + [mismatch_status] + ); + + React.useEffect(() => { + const generateIDVError = () => { + switch (mismatch_status) { + case idv_error_statuses.poi_name_dob_mismatch: + return { + required_fields: ['first_name', 'last_name', 'date_of_birth'], + side_note_image: , + inline_note_text: ( + ]} + /> + ), + failure_message: ( + ]} + /> + ), + }; + case idv_error_statuses.poi_name_mismatch: + return { + required_fields: ['first_name', 'last_name'], + side_note_image: , + inline_note_text: ( + ]} + /> + ), + failure_message: ( + ]} + /> + ), + }; + case idv_error_statuses.poi_dob_mismatch: + return { + required_fields: ['date_of_birth'], + side_note_image: , + inline_note_text: ( + ]} + /> + ), + failure_message: ( + ]} + /> + ), + }; + default: + return { + required_fields: ['first_name', 'last_name', 'date_of_birth'], + side_note_image: , + inline_note_text: ( + ]} + /> + ), + failure_message: ( + + ), + }; + } + }; + + const initializeFormValues = async (required_fields: string[]) => { + await WS.wait('get_settings'); + const form_data = filterObjProperties(account_settings, required_fields); + if (form_data.date_of_birth) { + form_data.date_of_birth = toMoment(form_data.date_of_birth).format('YYYY-MM-DD'); + } + let initial_form_values = form_data; + if (is_document_upload_required) { + initial_form_values = { + document_type: { + id: '', + text: '', + value: '', + example_format: '', + sample_image: '', + }, + document_number: '', + ...initial_form_values, + }; + } + setRestState({ + form_initial_values: { ...initial_form_values }, + changeable_fields: [...getChangeableFields()], + api_error: '', + }); + setIsLoading(false); + }; + + const error_config = generateIDVError(); + setIdvFailure(error_config); + initializeFormValues(error_config?.required_fields ?? []).catch(e => { + setRestState({ + form_initial_values: {}, + changeable_fields: [], + api_error: e?.error?.message, + }); + }); + }, [mismatch_status, account_settings, is_document_upload_required, getChangeableFields]); + + const onSubmit = async (values: TIdvFailedForm, { setStatus, setSubmitting }: FormikHelpers) => { + setSubmitting(true); + setStatus({ error_msg: '' }); + const { document_number, document_type } = values; + const request = makeSettingsRequest( + values, + rest_state?.changeable_fields ? [...rest_state.changeable_fields] : [] + ); + const data = await WS.setSettings(request); + + if (data.error) { + setStatus({ error_msg: data.error.message }); + setSubmitting(false); + } else { + const response = await WS.authorized.storage.getSettings(); + if (response.error) { + setRestState(prev_state => ({ ...prev_state, api_error: response.error.message })); + setSubmitting(false); + return; + } + const submit_data = { + identity_verification_document_add: 1, + document_number, + document_type: document_type?.id, + issuing_country: selected_country.value, + }; + + if (!submit_data.document_type || submit_data.document_type === IDV_NOT_APPLICABLE_OPTION.id) { + setSubmitting(false); + handleSubmit(); + return; + } + WS.send(submit_data).then((resp: IdentityVerificationAddDocumentResponse) => { + setSubmitting(false); + if (resp.error) { + return; + } + handleSubmit(); + }); + } + }; + + const validateFields = (values: TIdvFailedForm) => { + const errors: Record = {}; + if (is_document_upload_required) { + const { document_type, document_number, document_additional } = values; + const needs_additional_document = !!document_type?.additional; + errors.document_type = isDocumentTypeValid(document_type as FormikValues); + if (!shouldHideHelperImage(document_type?.id as string)) { + if (needs_additional_document) { + errors.document_additional = isAdditionalDocumentValid(document_type, document_additional); + } + errors.document_number = isDocumentNumberValid(document_number ?? '', document_type as FormikValues); + } + } + + const validateValues = validate(errors, values); + + validateValues(val => val, idv_failure?.required_fields ?? [], localize('This field is required')); + + if (values.first_name) { + errors.first_name = validateName(values.first_name); + } + if (values.last_name) { + errors.last_name = validateName(values.last_name); + } + + setRestState(prev_state => ({ + ...prev_state, + errors: !isEmptyObject(removeEmptyPropertiesFromObject(errors)), + })); + return removeEmptyPropertiesFromObject(errors); + }; + + const citizen = account_settings?.citizen; + const selected_country = residence_list.find(residence_data => residence_data.value === citizen) || {}; + + if (rest_state?.api_error) return ; + + if (is_loading && Object.keys(rest_state?.form_initial_values ?? {}).length === 0) { + return ; + } + + return ( + + {({ isSubmitting, isValid, dirty }) => ( +
+ + + + + + {idv_failure?.failure_message} + + } + is_danger + /> + {is_document_upload_required && ( + + + + + + + + + )} + + + {!is_from_external && ( +