From e8b890c1f06f9e0bf3f5e43bd80ad3ce44faf8de Mon Sep 17 00:00:00 2001 From: Likhith Kolayari <98398322+likhith-deriv@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:43:37 +0400 Subject: [PATCH] yauheni/likhith/Combined/wall 400/idv error message handled (#8884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: new pr for wall 400 * feat: :sparkles: incorporated change to display the id document name * chore: country selector warning if failed * fix: :recycle: resolved comments * refactor: css file rename * feat: resolved code smell * refactor: expired and failed different warnings * refactor: failed messaged moved to country selector * chore: :green_heart: trigger build * refactor: review comments * refactor: review comments css * fix: :bug: incorporated message to display document type * fix: resolved code smell * fix: :bug: incorporated generic error message * fix: :bug: added missing dependency * chore: :twisted_rightwards_arrows: synced with master * fix: :recycle: incorporated review comments * fix: resolved un necessary render issue * fix: :bug: resolved issue with expired status * style: fix side container width in poi (#39) * style: container height fix for POI error messages when content in not overflowing (#40) * fix: onfido message alignment * fix: :bug: incorporated continue trade button * fix: onfido page mobile * style: no padding on fields container in idv screen (#41) * refactor: onfido hint center alignment fix * fix: odd translate value fix * fix: :bug: minor CSS fix * fix: green message responsive position * fix: green message full width * fix: :white_check_mark: updated testcases * fix: failing tests * refactor: code and ts fixes * fix: display of IDV error * chore: add duplication account error * refactor: added undefined check to response data * chore: review comments * chore: review comments * fix: :recycle: incorporated review comments * fix: barriers test fix form master * fix: barriers test * chore: refactored object.freeze to as const * chore: optional chaining to document type * chore: update spec file for idv-form * test: update personal-details spec * chore: review comments * refactor: css structure * fix: barriers test * fix: :recycle: incorporated review comments * refactor: rename properties * refactor: remove is_appstore from tests * refactor: styles refactor * fix: :zap: reduced duplicate data * fix: incorporated review comments * fix: :recycle: incorporated review comments * chore: Trigger Build * fix: :truck: modified path * chore: remove unused fila and variable, updated icons * revert: :beers: removed icon changes * refactor: removed formatting * fix: removed assignment of default values * fix: :bug: unable to select a new doc supporting country * fix: :bug: resolved re-render issue * fix: :zap: resolved message issue * refactor: :truck: renamed varibales * fix: :test_tube: failing testcase * fix: :bug: resolved issues with failure messages * fix: :bug: resolved issue with DIEL accounts * fix: :bug: fixed issue for DIEL clients * fix: styling issues * fix: ts error for format-response * fix: :truck: fixed path * fix: :truck: fixed path * fix: :bug: resolved styling bugs * fix: :bug: resolved styling bugs * fix: :fire: incorporated new styles to fix icon size * fix: :fire: incorporated new styles to fix icon size * fix: :white_check_mark: fixed failing testcase * fix: :green_heart: eslint issues * fix: layout styles * fix: style issues * fix: styles * fix: moved common styles to core * fix: :lipstick: updated styles * refactor: incorporated review comments * Merge branch 'master' into combined/wall-400/IDV-error-message-handled --------- Co-authored-by: “yauheni-kryzhyk-deriv” <“yauheni@deriv.me”> Co-authored-by: yauheni-deriv <103182683+yauheni-deriv@users.noreply.github.com> Co-authored-by: Shahzaib --- .../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 | 120 +++ .../account/src/Components/forms/idv-form.tsx | 96 +- .../forms/personal-details-form.jsx | 39 +- .../__tests__/personal-details.spec.js | 344 ++++--- .../personal-details/personal-details.jsx | 35 +- .../continue-trading-button.tsx | 19 +- .../Components/poa/poa-button/poa-button.tsx | 12 +- .../__tests__/idv-document-submit.spec.tsx | 44 +- .../idv-document-submit.tsx | 145 ++- .../__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 | 406 ++++++++ .../poi/idv-status/idv-failed/index.ts | 3 + .../__tests__/idv-limited.spec.tsx | 8 +- .../idv-status/idv-limited/idv-limited.tsx | 8 +- .../poi/idv-status/idv-limited/index.js | 3 - .../poi/idv-status/idv-limited/index.ts | 3 + .../__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 | 155 +++- .../idv-submit-complete.tsx | 104 ++- .../idv-status/idv-verified/idv-verified.tsx | 23 +- ...oi-confirm-with-example-form-container.tsx | 34 +- .../__tests__/poi-country-selector.spec.tsx | 12 +- .../poi-country-selector.tsx | 74 +- .../idv-doc-submit-on-signup.tsx | 50 +- .../account/src/Configs/poi-error-config.tsx | 13 + .../account/src/Constants/api-error-codes.ts | 6 + packages/account/src/Helpers/utils.tsx | 42 +- .../PersonalDetails/personal-details.jsx | 7 +- .../ProofOfAddress/proof-of-address-form.tsx | 2 - .../proof-of-identity-container.spec.js | 73 +- .../Verification/ProofOfIdentity/idv.jsx | 18 +- .../ProofOfIdentity/onfido-sdk-view.tsx | 4 - .../proof-of-identity-container.jsx | 400 ++++---- .../proof-of-identity-submission-for-mt5.jsx | 40 +- .../proof-of-identity-submission.jsx | 40 +- packages/account/src/Styles/account.scss | 424 --------- packages/account/src/Types/common.type.ts | 41 +- .../components/cfds-listing/cfds-listing.scss | 26 + .../src/components/hint-box/hint-box.scss | 6 + packages/core/src/sass/app.scss | 2 +- .../_common/components/account-common.scss | 873 ++++++++++++++++++ .../_common/components/onfido-container.scss | 441 --------- .../core/src/sass/real-account-signup.scss | 10 +- packages/shared/src/utils/constants/error.ts | 9 + .../src/utils/constants/idv-failure-codes.ts | 7 + packages/shared/src/utils/constants/index.ts | 1 + .../src/utils/helpers/format-response.ts | 34 +- 55 files changed, 2742 insertions(+), 1771 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.ts delete mode 100644 packages/account/src/Components/poi/idv-status/idv-limited/index.js create mode 100644 packages/account/src/Components/poi/idv-status/idv-limited/index.ts 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 create mode 100644 packages/account/src/Configs/poi-error-config.tsx create mode 100644 packages/account/src/Constants/api-error-codes.ts create mode 100644 packages/core/src/sass/app/_common/components/account-common.scss delete mode 100644 packages/core/src/sass/app/_common/components/onfido-container.scss create mode 100644 packages/shared/src/utils/constants/idv-failure-codes.ts 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 6806f15b33d8..1bff72577005 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 '../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..82ba1e082c7d --- /dev/null +++ b/packages/account/src/Components/forms/__tests__/idv-form.spec.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +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: {}, + 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 }) => ( + + {() => children} + + ), + }); + + const document_type_input = screen.getByLabelText('Choose the document type'); + const document_number_input = screen.getByLabelText('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 }) => ( + + {() => children} + + ), + }); + + const document_type_input = screen.getByLabelText('Choose the document type'); + + userEvent.click(document_type_input); + expect(await screen.findByText('Test document 1 name')).toBeInTheDocument(); + userEvent.tab(); + 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 f81b083fb5a0..6c9566928cb6 100644 --- a/packages/account/src/Components/forms/idv-form.tsx +++ b/packages/account/src/Components/forms/idv-form.tsx @@ -1,35 +1,39 @@ import React from 'react'; import classNames from 'classnames'; -import { Field, FieldProps } from 'formik'; -import { localize } from '@deriv/translations'; -import { formatInput, getIDVNotApplicableOption } from '@deriv/shared'; +import { Field, FormikProps, FieldProps, useFormikContext } from 'formik'; +import { ResidenceList } from '@deriv/api-types'; import { Autocomplete, DesktopWrapper, Input, MobileWrapper, SelectNative, Text } from '@deriv/components'; +import { formatInput, getIDVNotApplicableOption } from '@deriv/shared'; +import { localize } from '@deriv/translations'; import { getDocumentData, preventEmptyClipboardPaste, generatePlaceholderText, getExampleFormat, } from '../../Helpers/utils'; -import { TDocument, TIDVForm } from 'Types'; +import { TDocument, TIDVFormValues } from '../../Types'; + +type TIDVFormProps = { + selected_country: ResidenceList[0]; + hide_hint?: boolean; + class_name?: string; + 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 [document_list, setDocumentList] = React.useState([]); +}: TIDVFormProps) => { + 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: '', @@ -93,10 +97,12 @@ const IDVForm = ({ }; const onKeyUp = (e: { target: HTMLInputElement }, document_name: string) => { - const { example_format } = - document_name === 'document_number' ? values.document_type : values.document_type.additional; + const example_format = + document_name === 'document_number' + ? values?.document_type?.example_format + : values?.document_type?.additional?.example_format; let current_input: string | null = null; - current_input = example_format.includes('-') + current_input = example_format?.includes('-') ? formatInput(example_format, current_input ?? e.target.value, '-') : e.target.value; setFieldValue(document_name, current_input, true); @@ -135,38 +141,32 @@ const IDVForm = ({ {({ field }: FieldProps) => ( -
- { - handleBlur(e); - if (!getDocument(e.target.value)) { - resetDocumentItemSelected(); - } - }} - onChange={handleChange} - onItemSelection={(item: TDocument) => { - if ( - item.text === 'No results found' || - !item.text - ) { - setSelectedDoc(''); - resetDocumentItemSelected(); - } else { - bindDocumentData(item); - } - }} - required - /> -
+ { + handleBlur(e); + if (!getDocument(e.target.value)) { + resetDocumentItemSelected(); + } + }} + onChange={handleChange} + onItemSelection={(item: TDocument) => { + if (item.text === 'No results found' || !item.text) { + setSelectedDoc(''); + resetDocumentItemSelected(); + } else { + bindDocumentData(item); + } + }} + required + />
- {values.document_type.id !== IDV_NOT_APPLICABLE_OPTION.id && ( + {values?.document_type?.id !== IDV_NOT_APPLICABLE_OPTION.id && (
{ const { + inline_note_text, is_virtual, is_mf, is_svg, - is_qualified_for_idv, + is_rendered_for_idv, should_hide_helper_image, editable_fields = [], has_real_account, @@ -47,10 +46,10 @@ const PersonalDetailsForm = props => { setShouldCloseTooltip, class_name, states_list, + side_note, no_confirmation_needed, } = 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); @@ -72,7 +71,7 @@ const PersonalDetailsForm = props => { }, [no_confirmation_needed, setStatus, status]); 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_rendered_for_onfido || is_rendered_for_idv; const first_name_label = is_asterisk_needed ? localize('First name*') : localize('First name'); const last_name_label = is_asterisk_needed ? localize('Last name*') : localize('Last name'); const dob_label = is_asterisk_needed ? localize('Date of birth*') : localize('Date of birth'); @@ -84,8 +83,10 @@ const PersonalDetailsForm = props => { }; }; + const is_rendered_for_idv_or_onfido = is_rendered_for_idv || is_rendered_for_onfido; + const getFieldHint = field_name => - is_qualified_for_idv || is_rendered_for_onfido ? ( + is_rendered_for_idv_or_onfido ? ( { } }; - const name_dob_clarification_message = ( - ]} - /> - ); - const poa_clarification_message = ( ); @@ -135,14 +129,11 @@ const PersonalDetailsForm = props => {
- {(is_qualified_for_idv || is_rendered_for_onfido) && !should_hide_helper_image && ( - + {is_rendered_for_idv_or_onfido && !should_hide_helper_image && ( + )} {is_qualified_for_poa && ( { /> )} } + has_side_note={is_rendered_for_idv_or_onfido && !should_hide_helper_image} + side_note={side_note} side_note_position='right' type='image' > @@ -181,7 +172,7 @@ const PersonalDetailsForm = props => {
)} - {!is_qualified_for_idv && !is_rendered_for_onfido && !is_qualified_for_poa && ( + {!is_rendered_for_idv_or_onfido && !is_qualified_for_poa && ( @@ -239,7 +230,7 @@ const PersonalDetailsForm = props => { data-testid='last_name' /> )} - {!is_qualified_for_idv && !is_rendered_for_onfido && !is_qualified_for_poa && ( + {!is_rendered_for_idv_or_onfido && !is_qualified_for_poa && ( )} {'date_of_birth' in values && ( @@ -546,7 +537,7 @@ const PersonalDetailsForm = props => { )}
- {!no_confirmation_needed && is_qualified_for_idv && ( + {!no_confirmation_needed && is_rendered_for_idv && ( jest.fn(() => 'PoiNameDobExampleImage')); @@ -28,6 +29,13 @@ jest.mock('../../real-account-signup/helpers/utils.ts', () => ({ })), })); +jest.mock('Helpers/utils', () => ({ + ...jest.requireActual('Helpers/utils'), + isDocumentTypeValid: jest.fn(), + shouldShowIdentityInformation: jest.fn(() => false), + isAdditionalDocumentValid: jest.fn(), +})); + const mock_warnings = {}; const mock_errors = { account_opening_reason: 'Account opening reason is required.', @@ -106,6 +114,43 @@ const runCommonFormfieldsTests = is_svg => { }; describe('', () => { + const idv_document_data = { + document_type: { + value: 'national_id', + text: 'National ID', + }, + document_number: '1234567890123', + }; + + const default_IDV_config = { + documents_supported: {}, + has_visual_sample: 0, + is_country_supported: 0, + }; + + const default_residence_details = [ + { + 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, + }, + }, + }, + }, + ]; + const props = { is_svg: true, is_high_risk: false, @@ -139,11 +184,7 @@ describe('', () => { { identity: { services: { - idv: { - documents_supported: {}, - has_visual_sample: 0, - is_country_supported: 0, - }, + idv: default_IDV_config, onfido: { documents_supported: { passport: { @@ -161,11 +202,7 @@ describe('', () => { { identity: { services: { - idv: { - documents_supported: {}, - has_visual_sample: 0, - is_country_supported: 0, - }, + idv: default_IDV_config, onfido: { documents_supported: {}, is_country_supported: 0, @@ -236,6 +273,10 @@ describe('', () => { afterAll(() => ReactDOM.createPortal.mockClear()); + afterEach(() => { + jest.clearAllMocks(); + }); + const renderwithRouter = component => { const mock_store = mockStore({}); render( @@ -245,6 +286,135 @@ describe('', () => { ); }; + it('should have validation errors on form fields', async () => { + isMobile.mockReturnValue(false); + isDesktop.mockReturnValue(true); + + renderwithRouter(); + + const first_name = screen.getByTestId('first_name'); + const last_name = screen.getByTestId('last_name'); + const date_of_birth = await screen.getByTestId('date_of_birth'); + const place_of_birth = screen.getByTestId('place_of_birth'); + const citizenship = screen.getByTestId('citizenship'); + const phone = screen.getByTestId('phone'); + const tax_residence = screen.getByTestId('tax_residence'); + const tax_identification_number = screen.getByTestId('tax_identification_number'); + + fireEvent.blur(first_name); + fireEvent.blur(last_name); + fireEvent.blur(date_of_birth); + fireEvent.blur(place_of_birth); + fireEvent.blur(citizenship); + fireEvent.blur(phone); + fireEvent.blur(tax_residence); + fireEvent.blur(tax_identification_number); + + expect(await screen.findByText(/first name is required\./i)).toBeInTheDocument(); + expect(await screen.findByText(/last name is required\./i)).toBeInTheDocument(); + expect(await screen.findByText(/date of birth is required\./i)).toBeInTheDocument(); + expect(await screen.findByText(/place of birth is required\./i)).toBeInTheDocument(); + expect(await screen.findByText(/citizenship is required/i)).toBeInTheDocument(); + expect(await screen.findByText(/phone is required\./i)).toBeInTheDocument(); + expect(await screen.findByText(/tax residence is required\./i)).toBeInTheDocument(); + expect(await screen.findByText(/tax identification number is required\./i)).toBeInTheDocument(); + splitValidationResultTypes.mockReturnValue({ + ...mock_warnings, + errors: { + ...mock_errors.errors, + first_name: 'letters, spaces, periods, hyphens, apostrophes only', + last_name: 'last name should be between 2 and 50 characters.', + date_of_birth: 'You must be 18 years old and above.', + tax_identification_number: "Tax Identification Number can't be longer than 25 characters.", + }, + }); + fireEvent.change(first_name, { target: { value: '123' } }); + fireEvent.change(last_name, { target: { value: 'a' } }); + fireEvent.change(date_of_birth, { target: { value: '2021-04-13' } }); + fireEvent.change(tax_identification_number, { target: { value: '123456789012345678901234567890' } }); + + expect(await screen.findByText(/letters, spaces, periods, hyphens, apostrophes only/i)).toBeInTheDocument(); + expect(await screen.findByText(/last name should be between 2 and 50 characters/i)).toBeInTheDocument(); + expect(await screen.findByText(/you must be 18 years old and above\./i)).toBeInTheDocument(); + expect( + await screen.findByText(/tax Identification Number can't be longer than 25 characters\./i) + ).toBeInTheDocument(); + }); + + it('submit button should be enabled if TIN or tax_residence is optional in case of CR accounts', () => { + const new_props = { + ...props, + is_mf: false, + is_svg: true, + value: { + first_name: '', + last_name: '', + date_of_birth: '', + place_of_birth: '', + phone: '+34', + tax_residence: '', + tax_identification_number: '', + document_type: 'none', + }, + }; + renderwithRouter(); + + const first_name = screen.getByTestId('first_name'); + const last_name = screen.getByTestId('last_name'); + const date_of_birth = screen.getByTestId('date_of_birth'); + const phone = screen.getByTestId('phone'); + + userEvent.type(first_name, 'test firstname'); + userEvent.type(last_name, 'test lastname'); + userEvent.type(date_of_birth, '2000-12-12'); + userEvent.type(phone, '+49123456789012'); + expect(screen.getByRole('button', { name: /next/i })).toBeEnabled(); + }); + + it('should not display confirmation checkbox if opt-out of IDV', async () => { + splitValidationResultTypes.mockReturnValue({ warnings: {}, errors: {} }); + const new_props = { + ...props, + value: { + first_name: '', + last_name: '', + date_of_birth: '', + phone: '+93', + account_opening_reason: '', + place_of_birth: '', + document_type: 'none', + }, + }; + + renderwithRouter(); + + const first_name = screen.getByTestId('first_name'); + const last_name = screen.getByTestId('last_name'); + const date_of_birth = screen.getByTestId('date_of_birth'); + const phone = screen.getByTestId('phone'); + + userEvent.type(first_name, 'test firstname'); + userEvent.type(last_name, 'test lastname'); + userEvent.type(date_of_birth, '2000-12-12'); + userEvent.type(phone, '+49123456789012'); + + const previous_btn = screen.getByRole('button', { name: /previous/i }); + const next_btn = screen.getByRole('button', { name: /next/i }); + + const confirmation_checkbox = screen.queryByLabelText( + /i confirm that the name and date of birth above match my chosen identity document/i + ); + expect(confirmation_checkbox).not.toBeInTheDocument(); + + expect(previous_btn).toBeEnabled(); + expect(next_btn).toBeEnabled(); + userEvent.click(next_btn); + + await waitFor(() => { + expect(new_props.onSubmit).toBeCalled(); + }); + }); + it('should autopopulate tax_residence for MF clients', () => { const new_props = { ...props, @@ -437,61 +607,6 @@ describe('', () => { expect(getByText('Afghanistan')).toBeInTheDocument(); }); - it('should have validation errors on form fields', async () => { - isMobile.mockReturnValue(false); - isDesktop.mockReturnValue(true); - - renderwithRouter(); - - const first_name = screen.getByTestId('first_name'); - const last_name = screen.getByTestId('last_name'); - const date_of_birth = await screen.getByTestId('date_of_birth'); - const place_of_birth = screen.getByTestId('place_of_birth'); - const citizenship = screen.getByTestId('citizenship'); - const phone = screen.getByTestId('phone'); - const tax_residence = screen.getByTestId('tax_residence'); - const tax_identification_number = screen.getByTestId('tax_identification_number'); - - fireEvent.blur(first_name); - fireEvent.blur(last_name); - fireEvent.blur(date_of_birth); - fireEvent.blur(place_of_birth); - fireEvent.blur(citizenship); - fireEvent.blur(phone); - fireEvent.blur(tax_residence); - fireEvent.blur(tax_identification_number); - - expect(await screen.findByText(/first name is required\./i)).toBeInTheDocument(); - expect(await screen.findByText(/last name is required\./i)).toBeInTheDocument(); - expect(await screen.findByText(/date of birth is required\./i)).toBeInTheDocument(); - expect(await screen.findByText(/place of birth is required\./i)).toBeInTheDocument(); - expect(await screen.findByText(/citizenship is required/i)).toBeInTheDocument(); - expect(await screen.findByText(/phone is required\./i)).toBeInTheDocument(); - expect(await screen.findByText(/tax residence is required\./i)).toBeInTheDocument(); - expect(await screen.findByText(/tax identification number is required\./i)).toBeInTheDocument(); - splitValidationResultTypes.mockReturnValue({ - ...mock_warnings, - errors: { - ...mock_errors.errors, - first_name: 'letters, spaces, periods, hyphens, apostrophes only', - last_name: 'last name should be between 2 and 50 characters.', - date_of_birth: 'You must be 18 years old and above.', - tax_identification_number: "Tax Identification Number can't be longer than 25 characters.", - }, - }); - fireEvent.change(first_name, { target: { value: '123' } }); - fireEvent.change(last_name, { target: { value: 'a' } }); - fireEvent.change(date_of_birth, { target: { value: '2021-04-13' } }); - fireEvent.change(tax_identification_number, { target: { value: '123456789012345678901234567890' } }); - - expect(await screen.findByText(/letters, spaces, periods, hyphens, apostrophes only/i)).toBeInTheDocument(); - expect(await screen.findByText(/last name should be between 2 and 50 characters/i)).toBeInTheDocument(); - expect(await screen.findByText(/you must be 18 years old and above\./i)).toBeInTheDocument(); - expect( - await screen.findByText(/tax Identification Number can't be longer than 25 characters\./i) - ).toBeInTheDocument(); - }); - it('should show error for invalid TIN', async () => { const newvalidate = { errors: { @@ -541,6 +656,8 @@ describe('', () => { ); expect(checkbox).not.toBeInTheDocument(); + screen.debug(); + expect(previous_btn).toBeEnabled(); expect(next_btn).toBeEnabled(); fireEvent.click(next_btn); @@ -704,92 +821,63 @@ describe('', () => { expect(screen.queryByRole('link', { name: 'here' })).not.toBeInTheDocument(); }); - it('should disable tax_residence field if it is immutable from BE', () => { - isMobile.mockReturnValue(false); - isDesktop.mockReturnValue(true); + it('should validate idv values when a document type is selected', async () => { + shouldShowIdentityInformation.mockReturnValue(true); const new_props = { ...props, - is_mf: true, + is_mf: false, value: { ...props.value, - tax_residence: 'France', + ...idv_document_data, }, - disabled_items: ['salutation', 'first_name', 'last_name', 'date_of_birth', 'tax_residence'], + residence_list: default_residence_details, }; renderwithRouter(); - expect(screen.getByTestId('tax_residence')).toBeDisabled(); + + await waitFor(() => { + expect(isDocumentTypeValid).toHaveBeenCalled(); + expect(isAdditionalDocumentValid).not.toHaveBeenCalled(); + }); }); - it('submit button should be enabled if TIN or tax_residence is optional in case of CR accounts', () => { + it('should validate idv values along with additional document number when a document type is selected', async () => { + shouldShowIdentityInformation.mockReturnValue(true); + + const new_document_data = { + ...idv_document_data, + document_type: { ...idv_document_data.document_type, additional: '12345' }, + }; + const new_props = { ...props, is_mf: false, - is_svg: true, value: { - first_name: '', - last_name: '', - date_of_birth: '', - place_of_birth: '', - phone: '+34', - tax_residence: '', - tax_identification_number: '', + ...props.value, + ...new_document_data, }, + residence_list: default_residence_details, }; renderwithRouter(); - const first_name = screen.getByTestId('first_name'); - const last_name = screen.getByTestId('last_name'); - const date_of_birth = screen.getByTestId('date_of_birth'); - const phone = screen.getByTestId('phone'); - - userEvent.type(first_name, 'test firstname'); - userEvent.type(last_name, 'test lastname'); - userEvent.type(date_of_birth, '2000-12-12'); - userEvent.type(phone, '+49123456789012'); - expect(screen.getByRole('button', { name: /next/i })).toBeEnabled(); + await waitFor(() => { + expect(isAdditionalDocumentValid).toHaveBeenCalled(); + }); }); - it('should not display confirmation checkbox if opt-out of IDV', async () => { - splitValidationResultTypes.mockReturnValue({ warnings: {}, errors: {} }); + it('should disable tax_residence field if it is immutable from BE', () => { + isMobile.mockReturnValue(false); + isDesktop.mockReturnValue(true); const new_props = { ...props, + is_mf: true, value: { - first_name: '', - last_name: '', - date_of_birth: '', - phone: '+93', - account_opening_reason: '', - place_of_birth: '', - document_type: 'none', + ...props.value, + tax_residence: 'France', + document_type: idv_document_data, }, + disabled_items: ['salutation', 'first_name', 'last_name', 'date_of_birth', 'tax_residence'], }; - renderwithRouter(); - - const first_name = screen.getByTestId('first_name'); - const last_name = screen.getByTestId('last_name'); - const date_of_birth = screen.getByTestId('date_of_birth'); - const phone = screen.getByTestId('phone'); - - userEvent.type(first_name, 'test firstname'); - userEvent.type(last_name, 'test lastname'); - userEvent.type(date_of_birth, '2000-12-12'); - userEvent.type(phone, '+49123456789012'); - - const previous_btn = screen.getByRole('button', { name: /previous/i }); - const next_btn = screen.getByRole('button', { name: /next/i }); - - const confirmation_checkbox = screen.queryByLabelText( - /i confirm that the name and date of birth above match my chosen identity document/i - ); - expect(confirmation_checkbox).not.toBeInTheDocument(); - - expect(previous_btn).toBeEnabled(); - expect(next_btn).toBeEnabled(); - userEvent.click(next_btn); - - await waitFor(() => { - expect(new_props.onSubmit).toBeCalled(); - }); + expect(screen.getByTestId('tax_residence')).toBeDisabled(); }); }); diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index f78c111a7fcb..106864344042 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -20,6 +20,7 @@ import { shouldHideHelperImage, shouldShowIdentityInformation, } from 'Helpers/utils'; +import PoiNameDobExample from '../../Assets/ic-poi-name-dob-example.svg'; import FormSubHeader from '../form-sub-header'; import IDVForm from '../forms/idv-form'; @@ -52,6 +53,8 @@ const PersonalDetails = ({ const [should_close_tooltip, setShouldCloseTooltip] = React.useState(false); const is_submit_disabled_ref = React.useRef(true); + const PoiNameDobExampleIcon = PoiNameDobExample; + const isSubmitDisabled = errors => { return selected_step_ref?.current?.isSubmitting || Object.keys(errors).length > 0; }; @@ -71,7 +74,8 @@ const PersonalDetails = ({ onCancel(current_step, goToPreviousStep); }; - const is_qualified_for_idv = shouldShowIdentityInformation({ + //is_rendered_for_idv is used for configuring the components when they are used in idv page + const is_rendered_for_idv = shouldShowIdentityInformation({ account_status, account_settings, residence, @@ -99,7 +103,7 @@ const PersonalDetails = ({ const handleValidate = values => { let idv_error = {}; - if (is_qualified_for_idv) { + if (is_rendered_for_idv) { idv_error = validateIDV(values); } const { errors } = splitValidationResultTypes(validate(values)); @@ -124,7 +128,7 @@ const PersonalDetails = ({ if (IDV_NOT_APPLICABLE_OPTION.id === selected_document_type_id) return editable_fields; - if (is_confirmed && is_qualified_for_idv) { + if (is_confirmed && is_rendered_for_idv) { return editable_fields.filter(field => !['first_name', 'last_name', 'date_of_birth'].includes(field)); } @@ -138,12 +142,12 @@ const PersonalDetails = ({ validate={handleValidate} validateOnMount enableReinitialize - initialStatus={{ is_confirmed: !is_qualified_for_idv }} + initialStatus={{ is_confirmed: !is_rendered_for_idv }} onSubmit={(values, actions) => { onSubmit(getCurrentStep() - 1, values, actions.setSubmitting, goToNextStep); }} > - {({ handleSubmit, errors, setFieldValue, touched, values, handleChange, handleBlur, status }) => ( + {({ handleSubmit, errors, values, status }) => ( {({ setRef, height }) => (
- {!is_qualified_for_idv && ( + {!is_rendered_for_idv && ( - {is_qualified_for_idv && ( + {is_rendered_for_idv && ( @@ -192,13 +190,14 @@ const PersonalDetails = ({ } + is_rendered_for_idv={is_rendered_for_idv} editable_fields={getEditableFields( status?.is_confirmed, values?.document_type?.id @@ -212,6 +211,12 @@ const PersonalDetails = ({ should_close_tooltip={should_close_tooltip} setShouldCloseTooltip={setShouldCloseTooltip} should_hide_helper_image={shouldHideHelperImage(values?.document_type?.id)} + inline_note_text={ + ]} + /> + } no_confirmation_needed={ values?.document_type?.id === IDV_NOT_APPLICABLE_OPTION.id } diff --git a/packages/account/src/Components/poa/continue-trading-button/continue-trading-button.tsx b/packages/account/src/Components/poa/continue-trading-button/continue-trading-button.tsx index 3d7f70a4c12a..60e2c1ef75cb 100644 --- a/packages/account/src/Components/poa/continue-trading-button/continue-trading-button.tsx +++ b/packages/account/src/Components/poa/continue-trading-button/continue-trading-button.tsx @@ -1,11 +1,20 @@ -import { ButtonLink, Text } from '@deriv/components'; -import { localize } from '@deriv/translations'; import React from 'react'; +import classNames from 'classnames'; +import { ButtonLink, Text } from '@deriv/components'; +import { Localize } from '@deriv/translations'; + +type TContinueTradingButtonProps = { className?: string }; -export const ContinueTradingButton = () => ( - +/** + * Renders a button that redirects to the trading platform + * @name ContinueTradingButton + * @param className - Styles to be applied to the button + * @returns React Element + */ +export const ContinueTradingButton = ({ className }: TContinueTradingButtonProps) => ( + - {localize('Continue trading')} + ); diff --git a/packages/account/src/Components/poa/poa-button/poa-button.tsx b/packages/account/src/Components/poa/poa-button/poa-button.tsx index 0a3780dc0f46..49d89e35f935 100644 --- a/packages/account/src/Components/poa/poa-button/poa-button.tsx +++ b/packages/account/src/Components/poa/poa-button/poa-button.tsx @@ -1,14 +1,18 @@ import React from 'react'; import { ButtonLink, Text } from '@deriv/components'; import { routes } from '@deriv/shared'; -import { localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; +import classNames from 'classnames'; type TPoaButton = { - custom_text?: string; + custom_text?: JSX.Element; + class_name?: string; }; -const PoaButton = ({ custom_text = localize('Submit proof of address') }: TPoaButton) => ( - +const DEFAULT_BTN_TEXT = ; + +const PoaButton = ({ custom_text = DEFAULT_BTN_TEXT, class_name }: TPoaButton) => ( + {custom_text} 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 7599e31bd4c7..207c0b95ae0b 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 @@ -1,8 +1,10 @@ import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { isDesktop, isMobile } from '@deriv/shared'; -import IdvDocumentSubmit from '../idv-document-submit'; import { StoreProvider, mockStore } from '@deriv/stores'; +import { isDocumentNumberValid } from 'Helpers/utils'; +import IdvDocumentSubmit from '../idv-document-submit'; const mock_store = mockStore({ client: { @@ -10,7 +12,6 @@ const mock_store = mockStore({ }, }); -jest.mock('react-router'); jest.mock('Assets/ic-document-submit-icon.svg', () => jest.fn(() => 'DocumentSubmitLogo')); jest.mock('Helpers/utils', () => ({ ...jest.requireActual('Helpers/utils'), @@ -29,9 +30,11 @@ jest.mock('Helpers/utils', () => ({ }, }, }; - return data[country_code][key]; + + const document = data[country_code as keyof typeof data]; + return document[key as keyof typeof document]; }), - getRegex: jest.fn(() => /5436454364243/i), + isDocumentNumberValid: jest.fn(), })); jest.mock('@deriv/shared', () => ({ @@ -56,7 +59,7 @@ jest.mock('@deriv/shared', () => ({ })); describe('', () => { - const mock_props = { + const mock_props: React.ComponentProps = { handleBack: jest.fn(), handleViewComplete: jest.fn(), selected_country: { @@ -65,14 +68,21 @@ 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: 1, }, }, }, }, + getChangeableFields: jest.fn(() => []), is_from_external: false, }; @@ -102,7 +112,7 @@ describe('', () => { ); const backBtn = screen.getByRole('button', { name: /go back/i }); - fireEvent.click(backBtn); + userEvent.click(backBtn); expect(mock_props.handleBack).toHaveBeenCalledTimes(1); const document_type_input = screen.getByLabelText('Choose the document type'); @@ -111,12 +121,12 @@ describe('', () => { expect(screen.queryByText('Test document 1 name')).not.toBeInTheDocument(); expect(screen.queryByText('Test document 2 name')).not.toBeInTheDocument(); - fireEvent.click(document_type_input); + userEvent.click(document_type_input); expect(await screen.findByText('Test document 1 name')).toBeInTheDocument(); expect(await screen.findByText('Test document 2 name')).toBeInTheDocument(); expect(screen.queryByText('Please select a document type.')).not.toBeInTheDocument(); - fireEvent.blur(document_type_input); + userEvent.tab(); expect(await screen.findByText('Please select a document type.')).toBeInTheDocument(); await waitFor(() => { expect(screen.queryByText('Test document 1 name')).not.toBeInTheDocument(); @@ -145,18 +155,20 @@ describe('', () => { expect(document_number_input.name).toBe('document_number'); expect(document_number_input).toBeDisabled(); - fireEvent.change(document_type_input, { target: { value: 'Test document 2 name' } }); - expect(document_number_input).toBeEnabled(); + userEvent.selectOptions(document_type_input, 'Test document 2 name'); + await waitFor(() => { + 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: 'A54321' } }); + userEvent.type(document_number_input, '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(); @@ -165,7 +177,7 @@ describe('', () => { fireEvent.click(confirmation_checkbox); expect(verifyBtn).toBeEnabled(); - fireEvent.click(verifyBtn); + userEvent.click(verifyBtn); await waitFor(() => { expect(mock_props.handleViewComplete).toHaveBeenCalledTimes(1); }); 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 461078949cd7..584237b42360 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,48 +1,59 @@ import React from 'react'; import classNames from 'classnames'; -import { Button } from '@deriv/components'; -import { Formik } from 'formik'; -import { localize } from '@deriv/translations'; +import { Form, Formik, FormikErrors, FormikHelpers } from 'formik'; +import { ResidenceList, IdentityVerificationAddDocumentResponse } from '@deriv/api-types'; +import { Button, HintBox, Text } from '@deriv/components'; +import { Localize, localize } from '@deriv/translations'; import { WS, - getIDVNotApplicableOption, toMoment, filterObjProperties, isDesktop, removeEmptyPropertiesFromObject, formatIDVFormValues, + isMobile, } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import BackButtonIcon from 'Assets/ic-poi-back-btn.svg'; +import PoiNameDobExample from 'Assets/ic-poi-name-dob-example.svg'; +import FormBody from 'Components/form-body'; +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 { - documentAdditionalError, - isDocumentNumberValid, validate, makeSettingsRequest, validateName, - getExampleFormat, + shouldHideHelperImage, + isDocumentTypeValid, + isAdditionalDocumentValid, + isDocumentNumberValid, } from 'Helpers/utils'; -import FormFooter from 'Components/form-footer'; -import BackButtonIcon from 'Assets/ic-poi-back-btn.svg'; -import IDVForm from 'Components/forms/idv-form'; -import PersonalDetailsForm from 'Components/forms/personal-details-form'; -import FormSubHeader from 'Components/form-sub-header'; -import { observer, useStore } from '@deriv/stores'; -import { ResidenceList, IdentityVerificationAddDocumentResponse } from '@deriv/api-types'; -import { TDocument, TInputFieldValues, TIDVFormValues } from 'Types'; +import { DUPLICATE_ACCOUNT_ERROR_MESSAGE, GENERIC_ERROR_MESSAGE } from 'Configs/poi-error-config'; +import { TIDVFormValues, TPersonalDetailsForm } from 'Types'; type TIDVDocumentSubmitProps = { handleBack: React.MouseEventHandler; handleViewComplete: () => void; is_from_external: boolean; selected_country: ResidenceList[0]; + getChangeableFields: () => Array; }; +type TIdvDocumentSubmitForm = TIDVFormValues & TPersonalDetailsForm; + const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_country }: TIDVDocumentSubmitProps) => { const { client: { account_settings, getChangeableFields }, } = useStore(); 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'); @@ -50,7 +61,7 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c const changeable_fields = getChangeableFields(); - const initial_values = { + const initial_values: TIdvDocumentSubmitForm = { document_type: { id: '', text: '', @@ -62,27 +73,8 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c ...form_initial_values, }; - const IDV_NOT_APPLICABLE_OPTION = React.useMemo(() => getIDVNotApplicableOption(), []); - - const shouldHideHelperImage = (document_id: string) => document_id === IDV_NOT_APPLICABLE_OPTION.id; - - const isDocumentTypeValid = (document_type: TDocument) => { - if (!document_type?.text) { - return localize('Please select a document type.'); - } - return undefined; - }; - - const isAdditionalDocumentValid = (document_type: TDocument, document_additional: string) => { - 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 validateFields = (values: TIDVFormValues) => { - const errors: Partial = {}; + const validateFields = (values: TIdvDocumentSubmitForm) => { + const errors: FormikErrors> = {}; const { document_type, document_number, document_additional } = values; const needs_additional_document = !!document_type.additional; @@ -107,21 +99,26 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c return removeEmptyPropertiesFromObject(errors); }; - const submitHandler = async (values, { setSubmitting, setErrors }) => { + const submitHandler = async ( + values: TIdvDocumentSubmitForm, + { setSubmitting, setStatus }: FormikHelpers + ) => { setSubmitting(true); const request = makeSettingsRequest(values, changeable_fields); const data = await WS.setSettings(request); - if (data.error) { - setErrors({ error_message: data.error.message }); + if (data?.error) { + const response_error = + data.error?.code === 'DuplicateAccount' ? DUPLICATE_ACCOUNT_ERROR_MESSAGE : GENERIC_ERROR_MESSAGE; + setStatus({ error_message: response_error }); setSubmitting(false); return; } - const get_settings = WS.authorized.storage.getSettings(); - if (get_settings.error) { - setErrors({ error_message: data.error.message }); + const get_settings = await WS.authorized.storage.getSettings(); + if (get_settings?.error) { + setStatus({ error_message: get_settings?.error?.message ?? GENERIC_ERROR_MESSAGE }); setSubmitting(false); return; } @@ -135,7 +132,7 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c (response: IdentityVerificationAddDocumentResponse & { error: { message: string } }) => { setSubmitting(false); if (response.error) { - setErrors({ error_message: response.error.message }); + setStatus({ error_message: response?.error?.message ?? GENERIC_ERROR_MESSAGE }); return; } handleViewComplete(); @@ -152,33 +149,25 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c }} onSubmit={submitHandler} > - {({ - dirty, - errors, - handleBlur, - handleChange, - handleSubmit, - isSubmitting, - isValid, - setFieldValue, - touched, - values, - status, - }) => ( -
-
+ {({ dirty, isSubmitting, isValid, values, status }) => ( + + {status?.error_message && ( +
+ + {status.error_message} + + } + is_danger + className='hint-box-layout' + /> +
+ )} + - + ]} + /> + } /> -
+ {isDesktop() && (
+ )} ); 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..30f58c16ebd3 --- /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 { 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: React.ComponentProps = { + getChangeableFields: jest.fn(() => []), + is_from_external: false, + residence_list: [], + handleSubmit: jest.fn(), + account_settings: { + citizen: 'gh', + }, + mismatch_status: idv_error_statuses.poi_name_mismatch, + latest_status: {}, + }; + + 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..808c9a3546af --- /dev/null +++ b/packages/account/src/Components/poi/idv-status/idv-failed/idv-failed.tsx @@ -0,0 +1,406 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Form, Formik, FormikHelpers, FormikValues } from 'formik'; +import { + GetAccountStatus, + GetSettings, + IdentityVerificationAddDocumentResponse, + ResidenceList, +} from '@deriv/api-types'; +import { Button, DesktopWrapper, HintBox, Loading, Text } from '@deriv/components'; +import { + filterObjProperties, + getIDVNotApplicableOption, + getPropertyValue, + idv_error_statuses, + isEmptyObject, + isMobile, + removeEmptyPropertiesFromObject, + TIDVErrorStatus, + toMoment, + WS, +} 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 '../../../form-body'; +import IDVForm from '../../../forms/idv-form'; +import FormFooter from '../../../form-footer'; +import FormSubHeader from '../../../form-sub-header'; +import PersonalDetailsForm from '../../../forms/personal-details-form.jsx'; +import { + getIDVDocumentType, + isAdditionalDocumentValid, + isDocumentNumberValid, + isDocumentTypeValid, + makeSettingsRequest, + shouldHideHelperImage, + validate, + validateName, +} from '../../../../Helpers/utils'; +import { GENERIC_ERROR_MESSAGE, DUPLICATE_ACCOUNT_ERROR_MESSAGE } from '../../../../Configs/poi-error-config'; +import { API_ERROR_CODES } from '../../../../Constants/api-error-codes'; +import { TIDVFormValues, TPersonalDetailsForm } from '../../../../Types'; +import LoadErrorMessage from '../../../load-error-message'; + +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; + latest_status: DeepRequired['authentication']['attempts']['latest']; + selected_country?: ResidenceList[0]; +}; + +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, + latest_status, + selected_country, +}: 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] + ); + + /** + * If user needs to resubmit IDV document, the country should be the new selected country + * If user needs to update Personal info, the country should be the country of the latest status + */ + const chosen_country = React.useMemo( + () => + is_document_upload_required + ? selected_country ?? {} + : residence_list.find(residence_data => residence_data.value === latest_status?.country_code) ?? {}, + [selected_country, is_document_upload_required, latest_status?.country_code, residence_list] + ); + + const IDV_NOT_APPLICABLE_OPTION = React.useMemo(() => getIDVNotApplicableOption(), []); + + const generateIDVError = React.useCallback(() => { + const document_name = is_document_upload_required + ? getPropertyValue(chosen_country, ['identity', 'services', 'idv', 'documents_supported']) + : getIDVDocumentType(latest_status, chosen_country); + 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: ( + ]} + values={{ document_name }} + /> + ), + failure_message: ( + ]} + /> + ), + }; + case idv_error_statuses.poi_name_mismatch: + return { + required_fields: ['first_name', 'last_name'], + side_note_image: , + inline_note_text: ( + ]} + values={{ document_name }} + /> + ), + failure_message: ( + ]} + /> + ), + }; + case idv_error_statuses.poi_dob_mismatch: + return { + required_fields: ['date_of_birth'], + side_note_image: , + inline_note_text: ( + ]} + values={{ document_name }} + /> + ), + failure_message: ( + ]} + /> + ), + }; + default: + return { + required_fields: ['first_name', 'last_name', 'date_of_birth'], + side_note_image: , + inline_note_text: ( + ]} + values={{ document_name }} + /> + ), + failure_message: ( + + ), + }; + } + }, [latest_status, mismatch_status, chosen_country]); + + React.useEffect(() => { + 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, generateIDVError]); + + const onSubmit = async (values: TIdvFailedForm, { setStatus, setSubmitting }: FormikHelpers) => { + setSubmitting(true); + setStatus({ error_msg: null }); + 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) { + const response_error = + data.error?.code === API_ERROR_CODES.DUPLICATE_ACCOUNT + ? DUPLICATE_ACCOUNT_ERROR_MESSAGE + : GENERIC_ERROR_MESSAGE; + setStatus({ error_msg: response_error }); + 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: chosen_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); + }; + + if (rest_state?.api_error) return ; + + if (is_loading && Object.keys(rest_state?.form_initial_values ?? {}).length === 0) { + return ; + } + + return ( + + {({ isSubmitting, isValid, dirty, status }) => ( +
+ + + + + {(status?.error_msg || idv_failure?.failure_message) && ( + + {status?.error_msg ?? idv_failure?.failure_message} + + } + is_danger + /> + )} + {is_document_upload_required && ( + + + + + + + + + )} + + + {!is_from_external && ( + - ); - - if (identity_status === identity_status_codes.none || has_require_submission || allow_poi_resubmission) { - return ( - + + const verification_status = populateVerificationStatus(account_status); + const { + idv, + allow_poi_resubmission, + identity_last_attempt, + identity_status, + is_age_verified, + is_idv_disallowed, + manual, + needs_poa, + onfido, + } = verification_status; + const should_ignore_idv = is_high_risk && is_withdrawal_lock; + + if (!should_allow_authentication && !is_age_verified) { + return ; + } + + const onClickRedirectButton = () => { + const platform = platforms[from_platform.ref]; + const { is_hard_redirect = false, url = '' } = platform ?? {}; + if (is_hard_redirect) { + window.location.href = url; + window.sessionStorage.removeItem('config.platform'); + } else { + routeBackTo(from_platform.route); + } + }; + + const redirect_button = should_show_redirect_btn && ( + ); - } else if ( - !identity_last_attempt || - // Prioritise verified status from back office. How we know this is if there is mismatch between current statuses (Should be refactored) - (identity_status === identity_status_codes.verified && identity_status !== identity_last_attempt.status) - ) { - switch (identity_status) { - case identity_status_codes.pending: + + const should_show_mismatch_form = + idv.submissions_left > 0 && + [identity_status_codes.rejected, identity_status_codes.suspected, identity_status_codes.expired].includes( + idv.status + ); + + if ( + identity_status === identity_status_codes.none || + has_require_submission || + allow_poi_resubmission || + should_show_mismatch_form + ) { + return ( + + ); + } else if ( + !identity_last_attempt || + // Prioritise verified status from back office. How we know this is if there is mismatch between current statuses (Should be refactored) + (identity_status === identity_status_codes.verified && identity_status !== identity_last_attempt.status) + ) { + switch (identity_status) { + case identity_status_codes.pending: + return ( + + ); + case identity_status_codes.verified: + return ( + + ); + case identity_status_codes.expired: + return ( + + ); + case identity_status_codes.rejected: + case identity_status_codes.suspected: + return ; + default: + break; + } + } + + switch (identity_last_attempt.service) { + case service_code.idv: return ( - ); - case identity_status_codes.verified: + case service_code.onfido: return ( - ); - case identity_status_codes.expired: + case service_code.manual: return ( - ); - case identity_status_codes.rejected: - case identity_status_codes.suspected: - return ; default: - break; + return null; } } - - switch (identity_last_attempt.service) { - case service_code.idv: - return ( - - ); - case service_code.onfido: - return ( - - ); - case service_code.manual: - return ( - - ); - default: - return null; - } -}); +); export default ProofOfIdentityContainer; diff --git a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission-for-mt5.jsx b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission-for-mt5.jsx index 371aafc87833..f7993a0b0790 100644 --- a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission-for-mt5.jsx +++ b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission-for-mt5.jsx @@ -1,17 +1,28 @@ import React from 'react'; import { AutoHeightWrapper } from '@deriv/components'; -import { WS, isVerificationServiceSupported, formatIDVFormValues } from '@deriv/shared'; +import { WS, isVerificationServiceSupported, formatIDVFormValues, formatIDVError } from '@deriv/shared'; import { useStore, observer } from '@deriv/stores'; import Unsupported from '../../../Components/poi/status/unsupported'; import OnfidoUpload from './onfido-sdk-view-container'; import { identity_status_codes, submission_status_code, service_code } from './proof-of-identity-utils'; +import IdvFailed from '../../../Components/poi/idv-status/idv-failed'; import { IdvDocSubmitOnSignup } from '../../../Components/poi/poi-form-on-signup/idv-doc-submit-on-signup/idv-doc-submit-on-signup'; import { makeSettingsRequest } from '../../../Helpers/utils'; const POISubmissionForMT5 = observer( - ({ idv, is_idv_disallowed, onfido, onStateChange, citizen_data, has_idv_error, residence_list }) => { + ({ + idv, + is_idv_disallowed, + onfido, + onStateChange, + citizen_data, + is_from_external, + residence_list, + identity_last_attempt, + }) => { const [submission_status, setSubmissionStatus] = React.useState(); // submitting const [submission_service, setSubmissionService] = React.useState(); + const [idv_mismatch_status, setIdvMismatchStatus] = React.useState(null); const { client, notifications, traders_hub } = useStore(); const { account_settings, getChangeableFields } = client; @@ -20,12 +31,21 @@ const POISubmissionForMT5 = observer( React.useEffect(() => { if (citizen_data) { - const { submissions_left: idv_submissions_left } = idv; + const { submissions_left: idv_submissions_left, last_rejected, status } = idv; const { submissions_left: onfido_submissions_left } = onfido; const is_idv_supported = isVerificationServiceSupported(residence_list, account_settings, 'idv'); const is_onfido_supported = isVerificationServiceSupported(residence_list, account_settings, 'onfido'); if (is_idv_supported && Number(idv_submissions_left) > 0 && !is_idv_disallowed && !is_eu_user) { setSubmissionService(service_code.idv); + if ( + [ + identity_status_codes.rejected, + identity_status_codes.suspected, + identity_status_codes.expired, + ].includes(status) + ) { + setIdvMismatchStatus(formatIDVError(last_rejected, status)); + } } else if (onfido_submissions_left && is_onfido_supported) { setSubmissionService(service_code.onfido); } else { @@ -78,15 +98,23 @@ const POISubmissionForMT5 = observer( handlePOIComplete(); }); }; - if (submission_status === submission_status_code.submitting) { switch (submission_service) { case service_code.idv: - return ( + return idv_mismatch_status ? ( + + ) : ( diff --git a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx index abf5f9c59326..6ebef32102d8 100644 --- a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx +++ b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx @@ -1,9 +1,10 @@ import React from 'react'; -import { WS } from '@deriv/shared'; +import { formatIDVError, WS, idv_error_statuses } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; -import PoiCountrySelector from 'Components/poi/poi-country-selector'; +import CountrySelector from 'Components/poi/poi-country-selector'; import IdvDocumentSubmit from 'Components/poi/idv-document-submit'; -import IdvUploadComplete from 'Components/poi/idv-status/idv-submit-complete'; +import IdvFailed from 'Components/poi/idv-status/idv-failed'; +import IdvSubmitComplete from 'Components/poi/idv-status/idv-submit-complete'; import Unsupported from 'Components/poi/status/unsupported'; import UploadComplete from 'Components/poi/status/upload-complete'; import OnfidoUpload from './onfido-sdk-view-container'; @@ -24,15 +25,15 @@ const POISubmission = observer( redirect_button, residence_list, setIsCfdPoiCompleted, + should_show_mismatch_form, }) => { const [submission_status, setSubmissionStatus] = React.useState(); // selecting, submitting, complete const [submission_service, setSubmissionService] = React.useState(); const [selected_country, setSelectedCountry] = React.useState({}); - const { client, common, notifications } = useStore(); + const { client, notifications } = useStore(); const { account_settings, getChangeableFields } = client; - const { current_language } = common; const { refreshNotifications } = notifications; const handleSelectionNext = () => { @@ -72,6 +73,8 @@ const POISubmission = observer( [residence_list] ); + const mismatch_status = formatIDVError(idv.last_rejected, idv.status); + React.useEffect(() => { if (submission_status !== submission_status_code.complete) { if ((has_require_submission || allow_poi_resubmission) && identity_last_attempt) { @@ -103,6 +106,13 @@ const POISubmission = observer( default: break; } + } else if ( + mismatch_status && + ![idv_error_statuses.poi_expired, idv_error_statuses.poi_failed].includes(mismatch_status) && + idv.submissions_left > 0 + ) { + setSubmissionService(service_code.idv); + setSubmissionStatus(submission_status_code.submitting); } else { setSubmissionStatus(submission_status_code.selecting); } @@ -119,20 +129,31 @@ const POISubmission = observer( switch (submission_status) { case submission_status_code.selecting: { return ( - ); } + case submission_status_code.submitting: { switch (submission_service) { case service_code.idv: - return ( + return should_show_mismatch_form ? ( + + ) : ( diff --git a/packages/account/src/Styles/account.scss b/packages/account/src/Styles/account.scss index 09181ba539aa..d3e31860e448 100644 --- a/packages/account/src/Styles/account.scss +++ b/packages/account/src/Styles/account.scss @@ -200,57 +200,6 @@ $MIN_HEIGHT_FLOATING: calc( overflow: hidden; } - &__scrollbars_container { - height: 100%; - padding-top: 2.4rem; - padding-bottom: 6.4rem; - - &-wrapper { - overflow-x: hidden; - overflow-y: auto; - } - - &--grid-layout { - display: grid; - grid-gap: 4px; - - .dc-input { - margin-bottom: 0; - } - - .dc-dropdown-container { - .dc-dropdown__display-text, - .dc-list__item-text { - text-transform: unset; - } - } - - @include mobile { - padding: 0 1.6rem; - overflow-x: hidden; - overflow-y: auto; - grid-template-rows: auto; - - & .account-form__header { - &:first-child { - padding-top: 2.4rem; - margin-bottom: 3.2rem; - } - } - } - } - - @include desktop { - padding-left: 16px; - padding-right: 16px; - } - - @include mobile { - height: unset; - padding-top: unset; - padding-bottom: unset; - } - } &-form { overflow: hidden; @@ -416,144 +365,6 @@ $MIN_HEIGHT_FLOATING: calc( margin-left: 0; } - &__footer { - height: 80px; - position: absolute; - bottom: 0; - left: 0; - display: flex; - width: 100%; - padding: 16px 24px; - align-items: center; - justify-content: flex-end; - z-index: 4; - border-radius: 0 $BORDER_RADIUS $BORDER_RADIUS 0; - border-top: 1px solid var(--general-section-1); - background-color: var(--general-main-1); - - &--reset { - padding: 2.4rem 0; - position: relative; - margin-top: 2rem; - } - - .back-btn { - margin-right: 8px; - - .back-btn-icon { - margin-right: 0.8rem; - } - - span { - display: inline-flex; - align-items: center; - } - } - - &-note { - color: var(--text-prominent); - font-size: var(--text-size-xxs); - line-height: 1.5; - text-align: right; - min-width: 27.6rem; - max-width: 36.6rem; - height: 3.6rem; - - @include mobile { - width: auto; - text-align: center; - align-self: center; - - &--dashboard { - margin: 0 1.6rem; - text-align: left; - } - } - } - - &-btn { - height: 4rem; - margin: 0 0 0 1.6rem; - - &-wrapper { - align-items: normal; - display: flex; - flex-direction: row; - - @include mobile { - flex-direction: column; - } - } - - &-dashboard { - .dc-btn__text { - color: var(--text-prominent); - } - } - - &-dashboard:hover { - background-color: unset !important; - } - - @include desktop { - margin-left: 1.6rem; - - &-wrapper--dashboard { - display: flex; - justify-content: space-between; - } - } - - @include mobile { - margin: 0 2.4rem; - - &--has-bottom-margin { - margin-bottom: 1.6rem; - } - } - } - - &--dashboard { - width: 68.5rem; - margin-left: 6rem; - - @include mobile { - width: 100vw; - margin-left: unset; - - & .account-form__footer-btn { - min-width: 35%; - - &-wrapper { - flex-direction: row; - } - } - - & > div { - display: flex; - justify-content: space-around; - } - } - } - - &-poa { - margin-left: unset; - } - - &-all-fields-required { - padding-bottom: 2.4rem; - padding-top: 0.8rem; - } - - @include mobile { - padding: 16px 0; - flex-direction: column; - height: auto; - align-items: initial; - border-top: 2px solid var(--general-section-1); - } - } - &__error-message { padding: 1rem 0; position: unset; @@ -2033,241 +1844,6 @@ $MIN_HEIGHT_FLOATING: calc( } } -/* stylelint-enable */ - -/** @define proof-of-identity */ -.proof-of-identity { - width: 100%; - height: 100%; - - &__main-container { - max-width: 68.2rem; - } - - .min-height { - min-height: 50vh; - - @include mobile { - min-height: unset; - } - } - - &__container { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - - @include mobile { - min-height: unset; - overflow-y: scroll; - justify-content: center; - } - - &--reset { - align-items: normal; - @include mobile { - overflow-y: unset; - padding: 1.6rem; - } - .proof-of-identity__submit-button { - @include mobile { - margin: unset; - width: 100%; - } - } - .account-form__footer { - @include mobile { - padding: 1.6rem 2.4rem; - } - } - .form-body { - @include desktop { - z-index: 5; - } - } - } - - .icon { - width: 128px; - height: 128px; - margin-top: 5.2rem; - margin-bottom: 2.6rem; - - @include mobile { - width: 72px; - height: 72px; - margin-top: 2.6rem; - } - } - - .external-dropdown { - .dc-dropdown-list { - z-index: 5; - } - } - } - - &__redirection { - .dc-btn { - margin-top: 3.2rem; - height: 4rem; - - @include mobile { - margin: 1.6rem 0; - padding: 1.6rem; - width: 100%; - } - } - } - - &__country-text { - margin-bottom: 1.6rem; - } - - &__text { - @include mobile { - width: 94%; - text-align: center; - } - } - - &__dropdown-container { - margin-top: 1.6rem; - } - - &__inner-container { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: 100%; - - @include mobile { - flex-direction: column; - } - } - - &__header { - margin: 5.4rem 0 1.6rem; - - @include mobile { - margin: 2.4rem 0 0.8rem; - } - } - - &__footer.external-footer { - padding: unset; - position: unset; - top: unset; - bottom: unset; - border: none; - height: fit-content; - z-index: 4; - } - - &__footer { - @include mobile { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; - position: fixed; - bottom: 0; - } - } - - &__footer-alert { - margin-right: auto; - } - - &__submit-button { - @include mobile { - margin-right: 1.2rem; - } - } - - &__fieldset { - width: 39.5rem; - - @include mobile { - width: 94%; - margin: 0.4rem 0 3.2rem; - } - - .document-dropdown { - margin: 0.4rem 0 3.2rem; - } - - .country-dropdown { - min-height: 35.2rem; - } - } - - &__fieldset-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - @include mobile { - width: 100%; - } - } - - &__fieldset-input { - width: 39.5rem; - - @include mobile { - width: 94%; - margin-top: 0.4rem; - } - } - - &__sample-container { - margin-left: 1.6rem; - - @include mobile { - margin-left: unset; - width: 94%; - } - } - - &__sample-container-external { - margin-top: 2.8rem; - } - - &__image-container { - width: fit-content; - height: fit-content; - padding: 8px; - border-radius: 4px; - background-color: $color-grey-2; - } - - &__image { - max-width: 24.5rem; - border-radius: 4px; - object-fit: contain; - - @include mobile { - max-width: calc(95vw - 16px); - } - } - - &__redirect { - width: auto !important; - } - - .text { - display: block; - } - - .btm-spacer { - margin-bottom: 1.6rem; - } -} - /** @define closing-account; weak */ .closing-account { max-width: 67.2rem; diff --git a/packages/account/src/Types/common.type.ts b/packages/account/src/Types/common.type.ts index c956e71dfff2..eb813d8eb788 100644 --- a/packages/account/src/Types/common.type.ts +++ b/packages/account/src/Types/common.type.ts @@ -1,7 +1,6 @@ /** Add types that are shared between components */ import React from 'react'; -import { FormikHandlers, FormikProps, FormikValues } from 'formik'; -import { Authorize, IdentityVerificationAddDocumentResponse, ResidenceList } from '@deriv/api-types'; +import { Authorize, IdentityVerificationAddDocumentResponse } from '@deriv/api-types'; import { Redirect } from 'react-router-dom'; import { Platforms } from '@deriv/shared'; @@ -132,33 +131,15 @@ export type TPOIStatus = { }; export type TPersonalDetailsForm = { - warning_items?: Record; - is_virtual?: boolean; - is_mf?: boolean; - is_svg?: boolean; - is_qualified_for_idv?: boolean; - should_hide_helper_image: boolean; - is_appstore?: boolean; - editable_fields: Array; - has_real_account?: boolean; - residence_list?: ResidenceList; - is_fully_authenticated?: boolean; - account_opening_reason_list?: Record[]; - closeRealAccountSignup: () => void; - salutation_list?: Record[]; - is_rendered_for_onfido?: boolean; - should_close_tooltip?: boolean; - setShouldCloseTooltip?: (should_close_tooltip: boolean) => void; -} & FormikProps; + first_name: string; + last_name: string; + date_of_birth: string; +}; export type TInputFieldValues = Record; export type TIDVVerificationResponse = IdentityVerificationAddDocumentResponse & { error: { message: string } }; -export type TVerificationStatus = Readonly< - Record<'none' | 'pending' | 'rejected' | 'verified' | 'expired' | 'suspected', string> ->; - export type TDocument = { id: string; text: string; @@ -171,6 +152,10 @@ export type TDocument = { }; }; +export type TVerificationStatus = Readonly< + Record<'none' | 'pending' | 'rejected' | 'verified' | 'expired' | 'suspected', string> +>; + export type TIDVFormValues = { document_type: TDocument; document_number: string; @@ -178,14 +163,6 @@ export type TIDVFormValues = { error_message?: string; }; -export type TIDVForm = { - selected_country: ResidenceList[0]; - hide_hint?: boolean; - class_name?: string; - can_skip_document_verification: boolean; -} & Partial & - FormikProps; - export type TPlatforms = typeof Platforms[keyof typeof Platforms]; export type TServerError = { diff --git a/packages/appstore/src/components/cfds-listing/cfds-listing.scss b/packages/appstore/src/components/cfds-listing/cfds-listing.scss index 9e8c6e8b8f16..3f8915174632 100644 --- a/packages/appstore/src/components/cfds-listing/cfds-listing.scss +++ b/packages/appstore/src/components/cfds-listing/cfds-listing.scss @@ -879,6 +879,20 @@ position: relative; width: 100%; + .poi-form-on-signup__fields { + .dc-autocomplete { + margin-bottom: 0; + } + } + + .account-form__poi-confirm-example { + gap: 1.6rem; + + @include mobile { + gap: 0; + } + } + &__body { width: 100%; .poi-form-on-signup { @@ -935,6 +949,18 @@ padding-top: 1.6rem; } } + + .proof-of-identity__mismatch-container { + max-width: 70.2rem; + margin-inline: auto; + + .account__scrollbars_container { + padding-top: 2.4rem; + } + .dc-themed-scrollbars { + padding-inline: 1.6rem; + } + } } &__form { diff --git a/packages/components/src/components/hint-box/hint-box.scss b/packages/components/src/components/hint-box/hint-box.scss index d3a330bd37be..50d4813001e3 100644 --- a/packages/components/src/components/hint-box/hint-box.scss +++ b/packages/components/src/components/hint-box/hint-box.scss @@ -38,3 +38,9 @@ color: var(--text-prominent); } } + +.hint-box-layout { + display: grid; + grid-template-columns: auto 1fr; + align-items: flex-start; +} diff --git a/packages/core/src/sass/app.scss b/packages/core/src/sass/app.scss index 32a0db6d08b8..dc80c935a95c 100644 --- a/packages/core/src/sass/app.scss +++ b/packages/core/src/sass/app.scss @@ -47,7 +47,7 @@ @import 'app/_common/components/cookie-banner'; @import 'app/_common/components/notification-banner'; @import 'app/_common/components/notification-promo'; -@import 'app/_common/components/onfido-container'; +@import 'app/_common/components/account-common'; @import 'app/_common/components/cfd-poa'; // Modules @import 'app/modules/page-404'; diff --git a/packages/core/src/sass/app/_common/components/account-common.scss b/packages/core/src/sass/app/_common/components/account-common.scss new file mode 100644 index 000000000000..64b558c93565 --- /dev/null +++ b/packages/core/src/sass/app/_common/components/account-common.scss @@ -0,0 +1,873 @@ +.onfido-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.6rem; + + &-view_wrapper { + position: relative; + min-width: 32.8rem; + min-height: 50rem; + margin: auto; + } + + .account-form__poi-confirm-example_container { + margin-bottom: 0; + } + + .onfido-sdk-ui-Modal-inner { + border-radius: $BORDER_RADIUS * 2; + } + + @include mobile { + padding: 1.6rem; + + .onfido-sdk-ui-PageTitle-titleSpan { + font-size: 2rem; + } + } + + .onfido-sdk-ui-Camera-webcamContainer { + left: auto; + width: -webkit-fill-available; + } + + &__disabled { + opacity: 0.5; + pointer-events: none; + } + + &__info-message { + position: absolute; + top: 2.4rem; + left: 25%; + z-index: 1; + @include mobile { + top: 1.6rem; + left: 3.1rem; + } + } + + &__status-message { + background-color: var(--transparent-correct-message); + justify-content: flex-start; + transition: transform 0.35s ease, opacity 0.35s linear; + transform-origin: top; + opacity: 1; + width: 98%; + + &--exit { + transform: scale(1, 0); + opacity: 0; + } + + &_container { + position: absolute; + top: 0.4rem; + min-height: 3.4rem; + text-align: center; + z-index: 1; + width: 100%; + } + } +} + +.account { + &-form { + &__poi-confirm-example { + display: flex; + flex-direction: column; + gap: 0.8rem; + @include mobile { + height: fit-content; + } + + .account-form__fieldset { + max-width: unset; + } + + &--status-message { + margin-top: 1.6rem; + text-align: center; + width: 100%; + } + &_container { + border: 1px solid var(--general-active); + border-radius: 0.8rem; + + @include desktop { + padding: 1.6rem; + } + + @include mobile { + padding: 1.6rem 1.6rem 0; + } + } + + &_wrapper { + transition: transform 0.35s ease, opacity 0.35s linear; + transform-origin: top; + opacity: 1; + padding: 1.6rem; + max-width: 67rem; + + @include mobile { + padding-inline: 0; + } + + &--exit { + transform: scale(1, 0); + opacity: 0; + } + } + + .da-inline-note-with-icon { + margin-top: 0; + padding: 0.8rem; + display: flex; + background: var(--status-alert-background); + border-radius: $BORDER_RADIUS; + gap: 0.8rem; + line-height: 1.4rem; + } + + .account { + &__scrollbars_container--grid-layout { + grid-gap: 0; + } + &-form { + &__section-side-note { + @include mobile { + margin-top: 1.6rem; + justify-content: center; + display: flex; + } + } + &__section-content { + .account-form__fieldset { + margin-bottom: 0; + + .dc-input { + margin-bottom: 1rem; + } + + .dc-input--error, + .dc-input.dc-input--hint { + margin-bottom: 3.8rem; + } + } + } + } + } + + &--button { + padding: 0; + margin: 0; + background-color: unset; + border: unset; + + &__disabled > * { + cursor: not-allowed; + } + } + } + + &__poi-additional-information { + margin-top: 2rem; + } + + &__section { + display: grid; + + @include desktop { + grid-template-areas: 'section-side-note section-content'; + grid-template-columns: auto 1fr; + grid-gap: 1rem; + } + + align-items: center; + + @include mobile { + grid-template-areas: 'section-side-note' 'section-content'; + } + + &-side-note { + grid-area: section-side-note; + + @include mobile { + margin-top: unset; + width: 100%; + margin-bottom: 0.8rem; + } + &__example { + @include mobile { + flex-direction: column; + } + + &-image { + align-self: center; + } + } + } + + &-content { + grid-area: section-content; + + @include mobile { + width: 100%; + } + } + @include desktop { + &--reversed { + grid-template-areas: 'section-content section-side-note'; + grid-template-columns: 1fr auto; + } + } + } + + &__footer { + height: 8.0rem; + position: absolute; + bottom: 0; + left: 0; + display: flex; + width: 100%; + padding: 1.6rem 2.4rem; + align-items: center; + justify-content: flex-end; + z-index: 4; + border-radius: 0 $BORDER_RADIUS $BORDER_RADIUS 0; + border-top: 1px solid var(--general-section-1); + background-color: var(--general-main-1); + + &--reset { + padding: 2.4rem 0; + position: relative; + margin-top: 2rem; + } + + .back-btn { + margin-right: 8px; + + .back-btn-icon { + margin-right: 0.8rem; + } + + span { + display: inline-flex; + align-items: center; + } + } + + &-note { + color: var(--text-prominent); + font-size: var(--text-size-xxs); + line-height: 1.5; + text-align: right; + min-width: 27.6rem; + max-width: 36.6rem; + height: 3.6rem; + + @include mobile { + width: auto; + text-align: center; + align-self: center; + + &--dashboard { + margin: 0 1.6rem; + text-align: left; + } + } + } + + &-btn { + height: 4rem; + margin: 0 0 0 1.6rem; + + &-wrapper { + align-items: normal; + display: flex; + flex-direction: row; + + @include mobile { + flex-direction: column; + } + } + + &-dashboard { + .dc-btn__text { + color: var(--text-prominent); + } + } + + &-dashboard:hover { + background-color: unset !important; + } + + @include desktop { + margin-left: 1.6rem; + + &-wrapper--dashboard { + display: flex; + justify-content: space-between; + } + } + + @include mobile { + margin: 0 2.4rem; + + &--has-bottom-margin { + margin-bottom: 1.6rem; + } + } + } + + &--dashboard { + width: 68.5rem; + margin-left: 6rem; + + @include mobile { + width: 100vw; + margin-left: unset; + + & .account-form__footer-btn { + min-width: 35%; + + &-wrapper { + flex-direction: row; + } + } + + & > div { + display: flex; + justify-content: space-around; + } + } + } + + &-poa { + margin-left: unset; + } + + &-all-fields-required { + padding-bottom: 2.4rem; + padding-top: 0.8rem; + } + + @include mobile { + flex-direction: column; + height: auto; + align-items: initial; + border-top: 2px solid var(--general-section-1); + } + } + } + &__scrollbars_container { + height: 100%; + padding-top: 2.4rem; + padding-bottom: 6.4rem; + + &-wrapper { + overflow-x: hidden; + overflow-y: auto; + } + + &--grid-layout { + display: grid; + grid-gap: 4px; + + .dc-dropdown-container { + .dc-dropdown__display-text, + .dc-list__item-text { + text-transform: unset; + } + } + + @include mobile { + padding: 0 1.6rem; + overflow-x: hidden; + overflow-y: auto; + grid-template-rows: auto auto 1fr; + + & .account-form__header { + &:first-child { + padding-top: 2.4rem; + margin-bottom: 3.2rem; + } + } + } + } + + @include desktop { + padding-left: 16px; + padding-right: 16px; + } + + @include mobile { + height: unset; + padding-top: unset; + padding-bottom: unset; + } + } +} + +.idv-layout { + width: 100%; + .poi-form-on-signup__fields { + .proof-of-identity { + &__container { + width: 100%; + padding: 0; + } + + &__fieldset { + margin-bottom: 2rem; + &-input { + @include desktop { + margin-bottom: unset; + } + } + } + } + } + .proof-of-identity__inner-container { + @include desktop { + &--incl-image { + display: grid; + align-items: center; + column-gap: 1.5rem; + grid-template-columns: auto 0.5fr; + } + } + } + + .additional-field { + margin-top: 1.6rem; + margin-bottom: 2rem; + + @include mobile { + margin-top: 1rem; + } + } +} + +.account-form_poa { + .account { + &-form { + &__section { + align-items: unset; + + &-side-note { + width: 26rem; + } + + &-content { + width: 40rem; + + @include mobile { + width: 100%; + } + } + } + + &__fieldset { + max-width: unset; + margin-top: 1.6rem; + display: flex; + flex-direction: column; + gap: 3.2rem; + } + } + + &__scrollbars_container { + padding-top: 0; + padding-left: 0; + padding-bottom: 0; + } + } + + .files-description { + &__title { + margin-bottom: 1.6rem; + } + + li { + list-style-type: initial; + margin-left: 1.6rem; + } + } + + &-submit-error { + justify-content: left; + } +} + +.file-uploader { + &__container { + @include desktop { + margin: 1.6rem 0; + padding: 1.6rem 2.4rem; + border-radius: $BORDER_RADIUS * 2; + border: 1px solid var(--border-normal); + } + } + + &__file { + &-dropzone-wrapper { + flex: 1; + height: 13.2rem; + position: relative; + + .dc-file-dropzone { + border-radius: $BORDER_RADIUS * 2; + + &__message { + max-width: unset; + + &-subtitle { + font-size: 1.4rem; + font-weight: bold; + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-top: 1.6rem; + } + } + + @include mobile { + border: 1px dashed var(--icon-grey-background); + } + } + } + + &-title { + margin: 2.4rem 0 1.6rem; + } + + &-supported-formats { + display: flex; + justify-content: space-between; + margin: 1.6rem 0 2.4rem; + + @include mobile { + margin-bottom: 1.6rem; + } + + span { + @include mobile { + max-width: 14rem; + } + } + } + + @include mobile { + flex: unset; + margin-bottom: 2.4rem; + height: 15rem; + } + } + + &__remove-btn { + position: absolute; + width: 1.6rem; + height: 1.6rem; + top: 0.8rem; + right: 0.8rem; + cursor: pointer; + transition: transform 0.25s linear; + + &:hover { + transform: scale(1.25, 1.25); + } + + &--error { + circle { + fill: var(--status-danger); + } + } + + &-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } +} + +.proof-of-identity { + width: 100%; + height: 100%; + + &__main-container { + max-width: 68.2rem; + } + + .min-height { + min-height: 50vh; + + @include mobile { + min-height: unset; + } + } + + &__container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + + @include mobile { + min-height: unset; + overflow-y: scroll; + justify-content: center; + padding: 0 1.6rem; + } + + &--reset { + align-items: normal; + + @include mobile { + overflow-y: unset; + } + + .proof-of-identity__submit-button { + @include mobile { + margin: unset; + width: 100%; + } + } + + .form-body { + @include desktop { + z-index: 5; + } + } + + .account__scrollbars_container--grid-layout { + padding: 0; + } + } + + .icon { + width: 128px; + height: 128px; + margin-top: 5.2rem; + margin-bottom: 2.6rem; + + @include mobile { + width: 72px; + height: 72px; + margin-top: 2.6rem; + } + } + + .external-dropdown { + .dc-dropdown-list { + z-index: 5; + } + } + } + + .continue-trade { + margin-top: unset !important; + } + + &__redirection { + .dc-btn { + margin-top: 3.2rem; + height: 4rem; + + @include mobile { + margin: 1.6rem 0; + padding: 1.6rem; + width: 100%; + } + } + } + + &__country-text { + margin-bottom: 1.6rem; + } + + &__text { + @include mobile { + width: 94%; + text-align: center; + } + } + + &__dropdown-container { + margin-top: 1.6rem; + } + + &__inner-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + + @include mobile { + flex-direction: column; + } + } + + &__header { + margin: 5.4rem 0 1.6rem; + + @include mobile { + margin: 2.4rem 0 0.8rem; + } + } + + &__footer.external-footer { + padding: unset; + position: unset; + top: unset; + bottom: unset; + border: none; + height: fit-content; + z-index: 4; + } + + &__footer { + @include mobile { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + position: fixed; + bottom: 0; + } + } + + &__footer-alert { + margin-right: auto; + } + + &__submit-button { + @include mobile { + margin-right: 1.2rem; + } + } + + &__fieldset { + width: 39.5rem; + + @include mobile { + width: 94%; + margin: 0.4rem 0 3.2rem; + } + + .country-dropdown { + min-height: 35.2rem; + } + } + + &__fieldset-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + @include mobile { + width: 100%; + } + } + + &__fieldset-input { + width: 39.5rem; + + @include mobile { + width: 94%; + margin-top: 0.4rem; + } + } + + &__sample-container { + margin-left: 1.6rem; + + @include mobile { + margin-left: unset; + width: 94%; + } + } + + &__sample-container-external { + margin-top: 2.8rem; + } + + &__image-container { + width: fit-content; + height: fit-content; + padding: 8px; + border-radius: 4px; + background-color: $color-grey-2; + } + + &__image { + max-width: 24.5rem; + border-radius: 4px; + object-fit: contain; + + @include mobile { + max-width: calc(95vw - 16px); + } + } + + &__redirect { + width: auto !important; + } + + .text { + display: block; + } + + .btm-spacer { + margin-bottom: 1.6rem; + } + + &__failed { + &-message { + margin: 1.6rem auto 3.2rem; + border-radius: $BORDER_RADIUS * 2; + } + } + + &__mismatch-container { + .idv-form { + margin-bottom: 1.6rem; + } + + .proof-of-identity__submit-button { + @include desktop { + margin-top: 3.2rem; + margin-left: auto; + } + + @include mobile { + margin-inline: 2.4rem; + } + } + + .account__scrollbars_container { + @include desktop { + padding-top: 0; + } + } + + .account__scrollbars_container--grid-layout { + @include mobile { + margin-top: 2.4rem; + } + } + } +} + +@include mobile { + .formik__confirmation-checkbox { + margin-bottom: 1.6rem; + } +} diff --git a/packages/core/src/sass/app/_common/components/onfido-container.scss b/packages/core/src/sass/app/_common/components/onfido-container.scss deleted file mode 100644 index 7c549ba2daf7..000000000000 --- a/packages/core/src/sass/app/_common/components/onfido-container.scss +++ /dev/null @@ -1,441 +0,0 @@ -.onfido-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 1.6rem; - - &-view_wrapper { - position: relative; - min-width: 32.8rem; - min-height: 50rem; - margin: auto; - } - - .account-form__poi-confirm-example_container { - margin-bottom: 0; - } - - .onfido-sdk-ui-Modal-inner { - border-radius: 0.8rem; - } - - @include mobile() { - padding: 1.6rem; - - .onfido-sdk-ui-PageTitle-titleSpan { - font-size: 2rem; - } - } - - .onfido-sdk-ui-Camera-webcamContainer { - left: auto; - width: -webkit-fill-available; - } - - &__disabled { - opacity: 0.5; - pointer-events: none; - } - - &__info-message { - position: absolute; - top: 2.4rem; - left: 25%; - z-index: 1; - @include mobile { - top: 1.6rem; - left: 3.1rem; - } - } - - &__status-message { - background-color: var(--transparent-correct-message); - justify-content: flex-start; - transition: transform 0.35s ease, opacity 0.35s linear; - transform-origin: top; - opacity: 1; - width: 98%; - - &--exit { - transform: scale(1, 0); - opacity: 0; - } - - &_container { - position: absolute; - top: 0.4rem; - min-height: 3.4rem; - text-align: center; - z-index: 1; - width: 100%; - } - } -} - -.account-form { - &__poi-confirm-example { - display: flex; - flex-direction: column; - gap: 0.8rem; - - .account-form__fieldset { - max-width: unset; - } - - &--status-message { - margin-top: 1.6rem; - text-align: center; - width: 100%; - } - &_container { - border: 1px solid var(--general-active); - border-radius: 0.8rem; - - @include desktop() { - padding: 1.6rem; - } - - @include mobile() { - margin-bottom: 7rem; - padding: 1.6rem 1.6rem 0; - } - - .account__scrollbars_container { - padding: 0; - - &--grid-layout { - height: auto !important; - - @include mobile() { - padding: 0 1.6rem; - } - } - - .account-form__section { - align-items: center; - justify-content: space-between; - margin-top: 0.8rem; - - @include mobile() { - margin-bottom: 0; - } - } - } - } - - &_wrapper { - transition: transform 0.35s ease, opacity 0.35s linear; - transform-origin: top; - opacity: 1; - padding: 1.6rem; - max-width: 67rem; - - @include mobile() { - padding-inline: 0; - } - - &--exit { - transform: scale(1, 0); - opacity: 0; - } - } - - .da-inline-note-with-icon { - margin-top: 0; - padding: 0.8rem; - display: flex; - background: var(--status-alert-background); - border-radius: $BORDER_RADIUS; - gap: 0.8rem; - line-height: 1.4rem; - } - - .account__scrollbars_container--grid-layout { - grid-gap: 0; - } - - .account-form__section-side-note { - @include mobile { - margin-top: 1.6rem; - justify-content: center; - display: flex; - } - } - - .account-form__section-content { - .account-form__fieldset { - margin-bottom: 0; - - .dc-input { - margin-bottom: 1rem; - } - - .dc-input--error, - .dc-input.dc-input--hint { - margin-bottom: 3.8rem; - } - } - } - - &--button { - padding: 0; - margin: 0; - background-color: unset; - border: unset; - - &__disabled > * { - cursor: not-allowed; - } - } - } - - &__poi-additional-information { - margin-top: 2rem; - } - - &__section { - display: grid; - - @include desktop() { - grid-template-areas: 'section-side-note section-content'; - grid-template-columns: auto 1fr; - grid-gap: 1rem; - } - align-items: center; - @include mobile { - grid-template-areas: 'section-side-note' 'section-content'; - } - - &-side-note { - grid-area: section-side-note; - - @include mobile { - margin-top: unset; - width: 100%; - margin-bottom: 0.8rem; - } - - &__example { - @include mobile { - flex-direction: column; - } - - &-image { - align-self: center; - } - } - } - - &-content { - grid-area: section-content; - - @include mobile { - width: 100%; - } - } - - @include desktop { - &--reversed { - grid-template-areas: 'section-content section-side-note'; - grid-template-columns: 1fr auto; - } - } - } -} - -.idv-layout { - width: 100%; - .poi-form-on-signup__fields { - .proof-of-identity__container { - width: 100%; - } - .proof-of-identity__fieldset { - @include desktop { - margin-bottom: unset; - } - @include mobile { - margin-bottom: 2rem; - } - &-input { - @include desktop { - margin-bottom: unset; - } - } - } - } - .proof-of-identity__inner-container { - @include desktop { - &--incl-image { - display: grid; - align-items: center; - column-gap: 1.5rem; - grid-template-columns: auto 0.5fr; - } - } - } - - .additional-field { - margin-top: 1.6rem; - - @include mobile { - margin-top: 1rem; - } - } -} - -.account-form_poa { - .account { - &-form { - &__section { - align-items: unset; - - &-side-note { - width: 26rem; - } - - &-content { - width: 40rem; - - @include mobile { - width: 100%; - } - } - } - - &__fieldset { - max-width: unset; - margin-top: 1.6rem; - display: flex; - flex-direction: column; - gap: 3.2rem; - } - } - - &__scrollbars_container { - padding-top: 0; - padding-left: 0; - padding-bottom: 0; - - &--grid-layout { - @include mobile { - padding-top: 2.4rem; - } - } - } - } - - .files-description { - &__title { - margin-bottom: 1.6rem; - } - - li { - list-style-type: initial; - margin-left: 1.6rem; - } - } - - &-submit-error { - justify-content: left; - } -} - -.file-uploader { - &__container { - @include desktop { - margin: 1.6rem 0; - padding: 1.6rem 2.4rem; - border-radius: $BORDER_RADIUS * 2; - border: 1px solid var(--border-normal); - } - } - - &__file { - &-dropzone-wrapper { - flex: 1; - height: 13.2rem; - position: relative; - - .dc-file-dropzone { - border-radius: $BORDER_RADIUS * 2; - - &__message { - max-width: unset; - - &-subtitle { - font-size: 1.4rem; - font-weight: bold; - display: flex; - flex-direction: column; - gap: 0.8rem; - margin-top: 1.6rem; - } - } - - @include mobile { - border: 1px dashed var(--icon-grey-background); - } - } - } - - &-title { - margin: 2.4rem 0 1.6rem; - } - - &-supported-formats { - display: flex; - justify-content: space-between; - margin: 1.6rem 0 2.4rem; - - @include mobile { - margin-bottom: 1.6rem; - } - - span { - @include mobile { - max-width: 14rem; - } - } - } - - @include mobile { - flex: unset; - margin-bottom: 2.4rem; - height: 15rem; - } - } - - &__remove-btn { - position: absolute; - width: 1.6rem; - height: 1.6rem; - top: 0.8rem; - right: 0.8rem; - cursor: pointer; - transition: transform 0.25s linear; - - &:hover { - transform: scale(1.25, 1.25); - } - - &--error { - circle { - fill: var(--status-danger); - } - } - - &-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - } -} - -@include mobile { - .formik__confirmation-checkbox { - margin-bottom: 1.6rem; - } -} diff --git a/packages/core/src/sass/real-account-signup.scss b/packages/core/src/sass/real-account-signup.scss index 7f2d972bf8ff..4c75c668ee88 100644 --- a/packages/core/src/sass/real-account-signup.scss +++ b/packages/core/src/sass/real-account-signup.scss @@ -198,6 +198,7 @@ } .proof-of-identity { + &__footer { padding: 1.6rem 2.4rem; justify-content: end; @@ -205,6 +206,7 @@ border-top: 1px solid var(--general-section-1); @include mobile { width: 100%; + position:relative; } } @@ -216,9 +218,8 @@ } } .account-form__footer { - @include desktop { position: relative; - } + } } @@ -227,7 +228,8 @@ height: 100%; @include mobile { width: 100%; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: 1fr auto; + padding: unset; } } diff --git a/packages/shared/src/utils/constants/error.ts b/packages/shared/src/utils/constants/error.ts index 53fd468f5770..7058da23d4e0 100644 --- a/packages/shared/src/utils/constants/error.ts +++ b/packages/shared/src/utils/constants/error.ts @@ -5,3 +5,12 @@ export const getDefaultError = () => ({ description: localize('Our servers hit a bump. Let’s refresh to move on.'), cta_label: localize('Refresh'), }); + +export const STATUS_CODES = Object.freeze({ + NONE: 'none', + PENDING: 'pending', + REJECTED: 'rejected', + VERIFIED: 'verified', + EXPIRED: 'expired', + SUSPECTED: 'suspected', +}); diff --git a/packages/shared/src/utils/constants/idv-failure-codes.ts b/packages/shared/src/utils/constants/idv-failure-codes.ts new file mode 100644 index 000000000000..5df21e24c9d5 --- /dev/null +++ b/packages/shared/src/utils/constants/idv-failure-codes.ts @@ -0,0 +1,7 @@ +export const idv_error_statuses = Object.freeze({ + poi_name_dob_mismatch: 'POI_NAME_DOB_MISMATCH', + poi_dob_mismatch: 'POI_DOB_MISMATCH', + poi_name_mismatch: 'POI_NAME_MISMATCH', + poi_expired: 'POI_EXPIRED', + poi_failed: 'POI_FAILED', +}); diff --git a/packages/shared/src/utils/constants/index.ts b/packages/shared/src/utils/constants/index.ts index d9817697f166..101af38867a5 100644 --- a/packages/shared/src/utils/constants/index.ts +++ b/packages/shared/src/utils/constants/index.ts @@ -5,3 +5,4 @@ export * from './default-options'; export * from './jurisdictions-config'; export * from './signup_fields'; export * from './error'; +export * from './idv-failure-codes'; diff --git a/packages/shared/src/utils/helpers/format-response.ts b/packages/shared/src/utils/helpers/format-response.ts index 9f46174f556e..7149b8d706f5 100644 --- a/packages/shared/src/utils/helpers/format-response.ts +++ b/packages/shared/src/utils/helpers/format-response.ts @@ -1,8 +1,9 @@ import { GetSettings, ResidenceList } from '@deriv/api-types'; -import { getUnsupportedContracts } from '../constants'; +import { STATUS_CODES, getUnsupportedContracts } from '../constants'; import { getSymbolDisplayName, TActiveSymbols } from './active-symbols'; import { getMarketInformation } from './market-underlying'; import { TContractInfo } from '../contract'; +import { idv_error_statuses } from '../constants/idv-failure-codes'; type TIsUnSupportedContract = { contract_type?: string; @@ -42,6 +43,37 @@ export const formatPortfolioPosition = ( }; }; +export type TIDVErrorStatus = typeof idv_error_statuses[keyof typeof idv_error_statuses]; + +//formatIDVError is parsing errors messages from BE (strings) and returns error codes for using it on FE +export const formatIDVError = (errors: string[], status_code: string) => { + /** + * Check required incase of DIEL client + */ + if (errors.length === 0 && (status_code === STATUS_CODES.NONE || status_code === STATUS_CODES.VERIFIED)) + return null; + const error_keys: Record = { + name: 'POI_NAME_MISMATCH', + birth: 'POI_DOB_MISMATCH', + rejected: 'POI_FAILED', + }; + if (status_code === STATUS_CODES.EXPIRED) { + return 'POI_EXPIRED'; + } + const status: TIDVErrorStatus[] = []; + errors.forEach(error => { + const error_regex = RegExp(/(name|birth|rejected)/i).exec(error); + if (error_regex) { + status.push(error_keys[error_regex[0].toLowerCase()]); + } + }); + return status.includes(error_keys.name) && + status.includes(error_keys.birth) && + !status.includes(error_keys.rejected) + ? 'POI_NAME_DOB_MISMATCH' + : status[0] ?? 'POI_FAILED'; +}; + export const isVerificationServiceSupported = ( residence_list: ResidenceList, account_settings: GetSettings,