diff --git a/packages/account/src/Components/forms/idv-form.tsx b/packages/account/src/Components/forms/idv-form.tsx index 25a2d3e2fae0..7386c4c85670 100644 --- a/packages/account/src/Components/forms/idv-form.tsx +++ b/packages/account/src/Components/forms/idv-form.tsx @@ -2,7 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import { Field, FormikValues, FormikProps, FormikHandlers } from 'formik'; import { ResidenceList } from '@deriv/api-types'; -import { localize, Localize } from '@deriv/translations'; +import { localize } from '@deriv/translations'; import { formatInput, IDV_NOT_APPLICABLE_OPTION } from '@deriv/shared'; import { Autocomplete, DesktopWrapper, Input, MobileWrapper, SelectNative, Text } from '@deriv/components'; import { getDocumentData, preventEmptyClipboardPaste, generatePlaceholderText } from 'Helpers/utils'; @@ -24,7 +24,6 @@ type TFormProps = { type TIDVForm = { selected_country: ResidenceList[0]; - is_from_external: boolean; hide_hint?: boolean; class_name?: string; } & FormikHandlers & @@ -44,11 +43,9 @@ const IDVForm = ({ const [document_image, setDocumentImage] = React.useState(null); const [selected_doc, setSelectedDoc] = React.useState(''); - const { selected_country, is_from_external } = props; - // const citizen = account_settings?.citizen || residence; + const { selected_country } = props; const { documents_supported: document_data, has_visual_sample } = selected_country?.identity?.services?.idv || {}; - // residence_list.find(residence_data => residence_data.value === citizen)?.identity.services?.idv || {}; React.useEffect(() => { const document_types = Object.keys(document_data); @@ -129,11 +126,7 @@ const IDVForm = ({ 'proof-of-identity__container--idv': props.hide_hint, })} > -
+
- {selected_doc && !props.hide_hint && ( - - - - )} ); }; diff --git a/packages/account/src/Components/forms/personal-details-form.jsx b/packages/account/src/Components/forms/personal-details-form.jsx index b96c714a9b85..ca39aa67a315 100644 --- a/packages/account/src/Components/forms/personal-details-form.jsx +++ b/packages/account/src/Components/forms/personal-details-form.jsx @@ -22,6 +22,7 @@ import InlineNoteWithIcon from 'Components/inline-note-with-icon'; import FormBodySection from 'Components/form-body-section'; import { Link } from 'react-router-dom'; import { getEmploymentStatusList } from 'Sections/Assessment/FinancialAssessment/financial-information-list'; +import { isFieldImmutable } from 'Helpers/utils'; const DateOfBirthField = props => ( @@ -85,7 +86,7 @@ const PersonalDetailsForm = ({ is_qualified_for_idv, should_hide_helper_image, is_appstore, - disabled_items, + editable_fields = [], has_real_account, residence_list, is_fully_authenticated, @@ -107,7 +108,7 @@ const PersonalDetailsForm = ({ handleToolTipStatus(); setShouldCloseTooltip(false); } - }, [should_close_tooltip]); + }, [should_close_tooltip, handleToolTipStatus, setShouldCloseTooltip]); const getLastNameLabel = () => { if (is_appstore) return localize('Family name*'); @@ -127,14 +128,14 @@ const PersonalDetailsForm = ({ ); }; - const handleToolTipStatus = () => { + const handleToolTipStatus = React.useCallback(() => { if (is_tax_residence_popover_open) { setIsTaxResidencePopoverOpen(false); } if (is_tin_popover_open) { setIsTinPopoverOpen(false); } - }; + }, [is_tax_residence_popover_open, is_tin_popover_open]); const name_dob_clarification_message = ( ))} @@ -206,7 +207,10 @@ const PersonalDetailsForm = ({ required={is_svg || is_appstore} label={is_svg || is_appstore || is_mf ? localize('First name*') : localize('First name')} hint={getFieldHint(localize('first name'))} - disabled={disabled_items.includes('first_name') || (values?.first_name && has_real_account)} + disabled={ + isFieldImmutable('first_name', editable_fields) || + (values?.first_name && has_real_account) + } placeholder={localize('John')} data-testid='first_name' /> @@ -217,7 +221,10 @@ const PersonalDetailsForm = ({ required={is_svg || is_appstore} label={getLastNameLabel()} hint={getFieldHint(localize('last name'))} - disabled={disabled_items.includes('last_name') || (values?.last_name && has_real_account)} + disabled={ + isFieldImmutable('last_name', editable_fields) || + (values?.last_name && has_real_account) + } placeholder={localize('Doe')} data-testid='last_name' /> @@ -234,7 +241,8 @@ const PersonalDetailsForm = ({ } hint={getFieldHint(localize('date of birth'))} disabled={ - disabled_items.includes('date_of_birth') || (values?.date_of_birth && has_real_account) + isFieldImmutable('date_of_birth', editable_fields) || + (values?.date_of_birth && has_real_account) } placeholder={localize('01-07-1999')} portal_id={is_appstore ? '' : 'modal_root'} @@ -249,7 +257,7 @@ const PersonalDetailsForm = ({ @@ -412,7 +420,7 @@ const PersonalDetailsForm = ({ {...field} required data_testid='tax_residence_mobile' - disabled={disabled_items.includes('tax_residence')} + disabled={isFieldImmutable('tax_residence', editable_fields)} />
@@ -506,7 +514,7 @@ const PersonalDetailsForm = ({ onChange={handleChange} handleBlur={handleBlur} error={touched.employment_status && errors.employment_status} - disabled={disabled_items.includes('employment_status')} + disabled={isFieldImmutable('employment_status', editable_fields)} /> @@ -523,7 +531,7 @@ const PersonalDetailsForm = ({ setFieldTouched('employment_status', true); handleChange(e); }} - disabled={disabled_items.includes('employment_status')} + disabled={isFieldImmutable('employment_status', editable_fields)} /> @@ -572,7 +580,7 @@ const PersonalDetailsForm = ({ : localize('Account opening reason') } name={field.name} - disabled={disabled_items.includes('account_opening_reason')} + disabled={isFieldImmutable('account_opening_reason', editable_fields)} is_align_text_left list={account_opening_reason_list} value={values.account_opening_reason} @@ -603,7 +611,7 @@ const PersonalDetailsForm = ({ {...field} required data_testid='account_opening_reason_mobile' - disabled={disabled_items.includes('account_opening_reason')} + disabled={isFieldImmutable('account_opening_reason', editable_fields)} /> diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index 87087d105d78..6a7256c5cf43 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -43,7 +43,6 @@ const PersonalDetails = ({ const { is_appstore } = React.useContext(PlatformContext); const [should_close_tooltip, setShouldCloseTooltip] = React.useState(false); const [warning_items, setWarningItems] = React.useState({}); - const [should_hide_helper_image, setShouldHideHelperImage] = React.useState(false); const is_submit_disabled_ref = React.useRef(true); const isSubmitDisabled = errors => { @@ -73,6 +72,8 @@ const PersonalDetails = ({ real_account_signup_target, }); + const shouldHideHelperImage = document_id => document_id === IDV_NOT_APPLICABLE_OPTION.id; + const validateIDV = values => { const errors = {}; const { document_type, document_number, document_additional } = values; @@ -82,23 +83,25 @@ const PersonalDetails = ({ if (!document_type || !document_type.text) { errors.document_type = localize('Please select a document type.'); } - if (needs_additional_document) { - const error_message = documentAdditionalError(document_additional, document_type.additional?.format); - if (error_message) - errors.document_additional = - localize(error_message) + getExampleFormat(document_type.additional?.example_format); - } + if (!shouldHideHelperImage(document_type?.id)) { + if (needs_additional_document) { + const error_message = documentAdditionalError(document_additional, document_type.additional?.format); + if (error_message) + errors.document_additional = + localize(error_message) + getExampleFormat(document_type.additional?.example_format); + } - if (!document_number) { - errors.document_number = - localize('Please enter your document number. ') + getExampleFormat(document_type.example_format); - } else if (is_document_number_invalid) { - errors.document_number = localize('Please enter a valid ID number.'); - } else { - const format_regex = getRegex(document_type.value); - if (!format_regex.test(document_number)) { + if (!document_number) { errors.document_number = - localize('Please enter the correct format. ') + getExampleFormat(document_type.example_format); + localize('Please enter your document number. ') + getExampleFormat(document_type.example_format); + } else if (is_document_number_invalid) { + errors.document_number = localize('Please enter a valid ID number.'); + } else { + const format_regex = getRegex(document_type.value); + if (!format_regex.test(document_number)) { + errors.document_number = + localize('Please enter the correct format. ') + getExampleFormat(document_type.example_format); + } } } return errors; @@ -131,6 +134,8 @@ const PersonalDetails = ({ const citizen = account_settings?.citizen || residence; const selected_country = residence_list.find(residence_data => residence_data.value === citizen) || {}; + const editable_fields = Object.keys(props.value).filter(field => !disabled_items.includes(field)) || []; + return ( - {({ field, form }) => { - setShouldHideHelperImage( - form.values?.document_type?.id === - IDV_NOT_APPLICABLE_OPTION.id - ); - return ( - - ); - }} + {({ field }) => ( + + )} @@ -213,7 +212,8 @@ const PersonalDetails = ({
diff --git a/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.jsx b/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.jsx index a047e57f0cbf..75506eed7989 100644 --- a/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.jsx +++ b/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.jsx @@ -1,60 +1,143 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Text } from '@deriv/components'; +import classNames from 'classnames'; +import { Button } from '@deriv/components'; import { Formik, Field } from 'formik'; import { localize } from '@deriv/translations'; -import { WS } from '@deriv/shared'; -import { documentAdditionalError, getRegex } from 'Helpers/utils'; +import { + WS, + IDV_NOT_APPLICABLE_OPTION, + toMoment, + validLength, + validName, + filterObjProperties, + isDesktop, +} from '@deriv/shared'; +import { documentAdditionalError, getRegex, validate } from 'Helpers/utils'; import FormFooter from 'Components/form-footer'; import BackButtonIcon from 'Assets/ic-poi-back-btn.svg'; -import DocumentSubmitLogo from 'Assets/ic-document-submit-icon.svg'; import IDVForm from 'Components/forms/idv-form'; +import PersonalDetailsForm from 'Components/forms/personal-details-form'; +import FormSubHeader from 'Components/form-sub-header'; -const IdvDocumentSubmit = ({ handleBack, handleViewComplete, selected_country, is_from_external }) => { - const initial_form_values = { - document_type: '', +const IdvDocumentSubmit = ({ + handleBack, + handleViewComplete, + selected_country, + is_from_external, + account_settings, + getChangeableFields, +}) => { + const visible_settings = ['first_name', 'last_name', 'date_of_birth']; + const form_initial_values = filterObjProperties(account_settings, visible_settings) || {}; + + if (form_initial_values.date_of_birth) { + form_initial_values.date_of_birth = toMoment(form_initial_values.date_of_birth).format('YYYY-MM-DD'); + } + + const changeable_fields = [...getChangeableFields()]; + + const initial_values = { + document_type: { + id: '', + text: '', + value: '', + example_format: '', + sample_image: '', + }, document_number: '', + ...form_initial_values, }; const getExampleFormat = example_format => { return example_format ? localize('Example: ') + example_format : ''; }; + const shouldHideHelperImage = document_id => document_id === IDV_NOT_APPLICABLE_OPTION.id; + const validateFields = values => { const errors = {}; const { document_type, document_number, document_additional } = values; const needs_additional_document = !!document_type.additional; const is_document_number_invalid = document_number === document_type.example_format; - if (!document_type || !document_type.text || !document_type.value) { + if (!document_type || !document_type.text) { errors.document_type = localize('Please select a document type.'); } - - if (needs_additional_document) { - const error_message = documentAdditionalError(document_additional, document_type.additional?.format); - if (error_message) - errors.document_additional = - localize(error_message) + getExampleFormat(document_type.additional?.example_format); - } - - if (!document_number) { - errors.document_number = - localize('Please enter your document number. ') + getExampleFormat(document_type.example_format); - } else if (is_document_number_invalid) { - errors.document_number = localize('Please enter a valid ID number.'); - } else { - const format_regex = getRegex(document_type.value); - if (!format_regex.test(document_number)) { + if (!shouldHideHelperImage(document_type?.id)) { + if (needs_additional_document) { + const error_message = documentAdditionalError(document_additional, document_type.additional?.format); + if (error_message) + errors.document_additional = + localize(error_message) + getExampleFormat(document_type.additional?.example_format); + } + if (!document_number) { errors.document_number = - localize('Please enter the correct format. ') + getExampleFormat(document_type.example_format); + localize('Please enter your document number. ') + getExampleFormat(document_type.example_format); + } else if (is_document_number_invalid) { + errors.document_number = localize('Please enter a valid ID number.'); + } else { + const format_regex = getRegex(document_type.value); + if (!format_regex.test(document_number)) { + errors.document_number = + localize('Please enter the correct format. ') + getExampleFormat(document_type.example_format); + } } } + const required_fields = ['first_name', 'last_name', 'date_of_birth']; + const validateValues = validate(errors, values); + validateValues(val => val, required_fields, localize('This field is required')); + const min_name = 2; + const max_name = 50; + const validateName = (name, field) => { + if (name) { + if (!validLength(name.trim(), { min: min_name, max: max_name })) { + errors[field] = localize('You should enter 2-50 characters.'); + } else if (!validName(name)) { + errors[field] = localize('Letters, spaces, periods, hyphens, apostrophes only.'); + } + } + }; + validateName(values.first_name, 'first_name'); + validateName(values.last_name, 'last_name'); return errors; }; - const submitHandler = (values, { setSubmitting, setErrors }) => { + const makeSettingsRequest = settings => { + const request = filterObjProperties(settings, changeable_fields); + + if (request.first_name) { + request.first_name = request.first_name.trim(); + } + if (request.last_name) { + request.last_name = request.last_name.trim(); + } + if (request.date_of_birth) { + request.date_of_birth = toMoment(request.date_of_birth).format('YYYY-MM-DD'); + } + + return request; + }; + + const submitHandler = async (values, { setSubmitting, setErrors }) => { setSubmitting(true); + + const request = makeSettingsRequest(values); + + const data = await WS.setSettings(request); + + if (data.error) { + setErrors({ error_message: data.error.message }); + setSubmitting(false); + return; + } + const get_settings = WS.authorized.storage.getSettings(); + if (get_settings.error) { + setErrors({ error_message: data.error.message }); + setSubmitting(false); + return; + } const submit_data = { identity_verification_document_add: 1, document_number: values.document_number, @@ -63,6 +146,9 @@ const IdvDocumentSubmit = ({ handleBack, handleViewComplete, selected_country, i issuing_country: selected_country.value, }; + if (submit_data.document_type === IDV_NOT_APPLICABLE_OPTION.id) { + return; + } WS.send(submit_data).then(response => { setSubmitting(false); if (response.error) { @@ -74,7 +160,7 @@ const IdvDocumentSubmit = ({ handleBack, handleViewComplete, selected_country, i }; return ( - + {({ dirty, errors, @@ -84,39 +170,65 @@ const IdvDocumentSubmit = ({ handleBack, handleViewComplete, selected_country, i isSubmitting, isValid, setFieldValue, + setFieldTouched, touched, values, }) => ( -
- - - {localize('Verify your identity')} - - - {localize('Please select the document type and enter the ID number.')} - +
+ + + {({ field }) => { + return ( + + ); + }} + + {({ field }) => ( - +
+ +
)}
- + {isDesktop() && ( + + )}
)} ); diff --git a/packages/account/src/Helpers/__tests__/utils.spec.ts b/packages/account/src/Helpers/__tests__/utils.spec.ts index 324ea076a4d8..d6b6a936696b 100644 --- a/packages/account/src/Helpers/__tests__/utils.spec.ts +++ b/packages/account/src/Helpers/__tests__/utils.spec.ts @@ -5,6 +5,7 @@ import { getDocumentData, getRegex, preventEmptyClipboardPaste, + isFieldImmutable, } from '../utils'; describe('generatePlaceholderText', () => { @@ -150,3 +151,15 @@ describe('preventEmptyClipboardPaste', () => { expect(event.preventDefault).not.toHaveBeenCalled(); }); }); + +describe('isFieldImmutable', () => { + it('should return false if field is mutable', () => { + const immutable_field_set = ['test1', 'test2']; + expect(isFieldImmutable('test1', immutable_field_set)).toBeFalsy(); + }); + + it('should return true if field is immutable', () => { + const mutable_field_set = ['test1', 'test2']; + expect(isFieldImmutable('test3', mutable_field_set)).toBeTruthy(); + }); +}); diff --git a/packages/account/src/Helpers/utils.ts b/packages/account/src/Helpers/utils.ts index 7f76ab3711d4..9b59f3f834d5 100644 --- a/packages/account/src/Helpers/utils.ts +++ b/packages/account/src/Helpers/utils.ts @@ -215,3 +215,14 @@ export const generatePlaceholderText = (selected_doc: string) => { return localize('Enter your document number'); } }; + +export const validate = + (errors: Record, values: Record) => + (fn: (value: string) => string, arr: string[], err_msg: string) => { + arr.forEach(field => { + const value = values[field]; + if (/^\s+$/.test(value) || (!fn(value) && !errors[field] && !err_msg)) errors[field] = err_msg; + }); + }; + +export const isFieldImmutable = (field: string, mutable_fields: string[] = []) => !mutable_fields.includes(field); 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 a60bbf497fa9..e6e4a7bc9ba9 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,6 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React from 'react'; -import { WS } from '@deriv/shared'; +import { WS, toMoment, filterObjProperties, IDV_NOT_APPLICABLE_OPTION } from '@deriv/shared'; import Unsupported from 'Components/poi/status/unsupported'; import OnfidoUpload from './onfido-sdk-view'; import { identity_status_codes, submission_status_code, service_code } from './proof-of-identity-utils'; @@ -51,9 +51,43 @@ const POISubmissionForMT5 = ({ refreshNotifications(); }); }; - const handleIdvSubmit = (values, { setSubmitting, setErrors }) => { + + const makeSettingsRequest = settings => { + const request = filterObjProperties(settings, [...getChangeableFields()]); + + if (request.first_name) { + request.first_name = request.first_name.trim(); + } + if (request.last_name) { + request.last_name = request.last_name.trim(); + } + if (request.date_of_birth) { + request.date_of_birth = toMoment(request.date_of_birth).format('YYYY-MM-DD'); + } + + return request; + }; + + const handleIdvSubmit = async (values, { setSubmitting, setErrors }) => { setSubmitting(true); const { document_number, document_type } = values; + + const request = makeSettingsRequest(values); + + const data = await WS.setSettings(request); + + if (data.error) { + setErrors({ error_message: data.error.message }); + setSubmitting(false); + return; + } + const get_settings = WS.authorized.storage.getSettings(); + if (get_settings.error) { + setErrors({ error_message: data.error.message }); + setSubmitting(false); + return; + } + const submit_data = { identity_verification_document_add: 1, document_number, @@ -61,6 +95,11 @@ const POISubmissionForMT5 = ({ issuing_country: citizen_data.value, }; + if (submit_data.document_type === IDV_NOT_APPLICABLE_OPTION.id) { + handlePOIComplete(); + return; + } + WS.send(submit_data).then(response => { setSubmitting(false); if (response.error) { @@ -79,6 +118,8 @@ const POISubmissionForMT5 = ({ citizen_data={citizen_data} onNext={handleIdvSubmit} has_idv_error={has_idv_error} + getChangeableFields={getChangeableFields} + account_settings={account_settings} /> ); case service_code.onfido: { 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 67bacaef9431..a61291113d89 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 @@ -132,6 +132,8 @@ const POISubmission = ({ handleViewComplete={handleViewComplete} handleBack={handleBack} selected_country={selected_country} + account_settings={account_settings} + getChangeableFields={getChangeableFields} /> ); case service_code.onfido: { diff --git a/packages/account/src/Styles/account.scss b/packages/account/src/Styles/account.scss index e3735791fcd5..f8e68a47d3f0 100644 --- a/packages/account/src/Styles/account.scss +++ b/packages/account/src/Styles/account.scss @@ -563,7 +563,6 @@ $MIN_HEIGHT_FLOATING: calc( &__personal-details { &--dashboard { - // buto & .account-form__fieldset { margin-bottom: 3.2rem; @@ -2270,6 +2269,25 @@ $MIN_HEIGHT_FLOATING: calc( 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; + } + } + } + .icon { width: 128px; height: 128px; diff --git a/packages/components/src/components/input/input.scss b/packages/components/src/components/input/input.scss index 2ce40f427cea..349313179cb4 100644 --- a/packages/components/src/components/input/input.scss +++ b/packages/components/src/components/input/input.scss @@ -52,7 +52,7 @@ height: 4rem; &:hover:not(.dc-input--disabled) { - border-color: var(--border-hover); + border-color: var(--general-disabled); } &:focus-within { border-color: var(--brand-secondary); diff --git a/packages/core/src/sass/app/_common/components/onfido-container.scss b/packages/core/src/sass/app/_common/components/onfido-container.scss index 99260a846ca7..10aa7f1e8203 100644 --- a/packages/core/src/sass/app/_common/components/onfido-container.scss +++ b/packages/core/src/sass/app/_common/components/onfido-container.scss @@ -79,6 +79,10 @@ flex-direction: column; gap: 0.8rem; + .account-form__fieldset { + max-width: unset; + } + &_container { @include desktop() { border: 1px solid var(--general-active); @@ -228,13 +232,20 @@ } .idv-layout { - width: 60%; + width: 100%; .poi-form-on-signup__fields { .proof-of-identity__container { width: 100%; } .proof-of-identity__fieldset { - margin-bottom: unset; + @include desktop { + margin-bottom: unset; + } + &-input { + @include desktop { + margin-bottom: unset; + } + } } } } diff --git a/packages/core/src/sass/real-account-signup.scss b/packages/core/src/sass/real-account-signup.scss index 470ec58797b9..92416635f70a 100644 --- a/packages/core/src/sass/real-account-signup.scss +++ b/packages/core/src/sass/real-account-signup.scss @@ -181,3 +181,45 @@ } } } + +.mt5-layout { + &__container { + overflow: auto; + height: 100%; + @include desktop { + padding: 0.8rem 15.5rem; + } + @include mobile { + padding: 1.6rem; + } + } + + .proof-of-identity { + &__footer { + padding: 1.6rem 2.4rem; + justify-content: end; + display: flex; + border-top: 1px solid var(--general-section-1); + @include mobile { + width: 100%; + } + } + + &__submit-button { + @include mobile { + width: 100%; + margin: unset; + } + } + } +} + +.proof-of-identity__container.mt5-layout { + justify-content: space-between; + height: 100%; + @include mobile { + width: 100%; + display: flex; + flex-direction: column; + } +}