From 3b114c9d586bd726e6b56ae76eed60fd7838f6c3 Mon Sep 17 00:00:00 2001 From: yauheni-deriv <103182683+yauheni-deriv@users.noreply.github.com> Date: Thu, 26 Oct 2023 05:17:35 +0300 Subject: [PATCH] kyc / wall-1551: CTA 'Next' button enhancement (#10010) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: scroll to field with error component * chore: account signup next behaviour improve * chore: personal details on signup field order chnage * chore: improving behavior for address details page in account signup flow * chore: errors fields order * chore: should_scroll_to_error_field flag added * chore: custom hook for getting error field and scroll to it * chore: remove one field error and hool * chore: review comments * chore: remove shaking, added error message * chore: KYC-586/ client cannot add personal details after switching from I want to do it later * fix: checkbox handling field * chore: add chechbox to formik value * fix: checkbox field name * chore: eu account confirmation fix * chore: description for scrolling component * chore: mobile scroll to errors * chore: handling status for checkbox * chore: review comments * chore: confirmation refactoring * chore: error message, scroll for checkboxes * trigger build * chore: review comments * fix: failing tests * fix: unnecessary field removed from data to BE * fix: placeholders color with error * fix: red error placegolder for input * fix: failing tests --------- Co-authored-by: “yauheni-kryzhyk-deriv” <“yauheni@deriv.me”> Co-authored-by: Likhith Kolayari --- .../address-details/address-details.tsx | 51 ++--- .../currency-selector/currency-selector.tsx | 12 +- .../financial-details/financial-details.tsx | 16 +- .../__tests__/confirmation-checkbox.spec.tsx | 25 --- .../confirmation-checkbox.tsx | 60 ------ .../forms/confirmation-checkbox/index.ts | 3 - .../account/src/Components/forms/idv-form.tsx | 9 +- .../forms/personal-details-form.jsx | 30 +-- .../forms/scroll-to-field-with-error.tsx | 45 ++++ .../personal-details/personal-details.jsx | 35 +-- .../__tests__/idv-document-submit.spec.tsx | 8 +- .../idv-document-submit.tsx | 19 +- .../idv-doc-submit-on-signup.tsx | 25 +-- .../trading-assessment-dropdown.jsx | 31 +-- .../trading-assessment-form.jsx | 202 +++++++++++------- .../trading-assessment-radio-buttons.jsx | 1 + .../src/Configs/personal-details-config.ts | 5 + .../src/Constants/trading-assessment.ts | 1 + .../src/components/checkbox/checkbox.scss | 6 + .../src/components/checkbox/checkbox.tsx | 12 +- .../src/components/dropdown/dropdown.scss | 3 + .../src/components/input/input.scss | 5 + .../components/radio-group/radio-group.scss | 6 + .../components/radio-group/radio-group.tsx | 3 + .../RealAccountSignup/account-wizard.jsx | 1 + packages/core/src/sass/account-wizard.scss | 87 ++------ 26 files changed, 341 insertions(+), 360 deletions(-) delete mode 100644 packages/account/src/Components/forms/confirmation-checkbox/__tests__/confirmation-checkbox.spec.tsx delete mode 100644 packages/account/src/Components/forms/confirmation-checkbox/confirmation-checkbox.tsx delete mode 100644 packages/account/src/Components/forms/confirmation-checkbox/index.ts create mode 100644 packages/account/src/Components/forms/scroll-to-field-with-error.tsx create mode 100644 packages/account/src/Constants/trading-assessment.ts diff --git a/packages/account/src/Components/address-details/address-details.tsx b/packages/account/src/Components/address-details/address-details.tsx index 07a71265283a..de8aedd270d7 100644 --- a/packages/account/src/Components/address-details/address-details.tsx +++ b/packages/account/src/Components/address-details/address-details.tsx @@ -1,15 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import { - Formik, - Field, - FormikProps, - FormikValues, - FormikErrors, - FormikHelpers, - FormikHandlers, - FormikState, -} from 'formik'; +import { Formik, Field, FormikValues, FormikHelpers, FormikHandlers, FormikState } from 'formik'; import { StatesList } from '@deriv/api-types'; import { Autocomplete, @@ -29,6 +20,7 @@ import { getLocation } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; import { localize, Localize } from '@deriv/translations'; import { FormInputField } from '../forms/form-fields'; +import ScrollToFieldWithError from '../forms/scroll-to-field-with-error'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; export type TAddressDetailFormProps = { @@ -55,7 +47,6 @@ type TAddressDetails = { next_step: () => void ) => void; is_gb_residence: boolean | string; - selected_step_ref?: React.RefObject>; value: TAddressDetailFormProps; has_real_account: boolean; }; @@ -77,7 +68,6 @@ type TAutoComplete = { * @param validate - function to validate form values * @param onSubmit - function to submit form values * @param is_gb_residence - is residence Great Britan - * @param selected_step_ref - reference to selected step * @param value - form values * @param disabled_items - array of disabled fields * @param has_real_account - has real account @@ -93,7 +83,6 @@ const AddressDetails = observer( validate, onSubmit, is_gb_residence, - selected_step_ref, disabled_items, has_real_account, ...props @@ -108,11 +97,6 @@ const AddressDetails = observer( const { is_desktop, is_mobile } = ui; const { data: states_list, isFetched } = useStatesList(residence); - const isSubmitDisabled = (errors: FormikErrors = {}): boolean => { - const is_submitting = selected_step_ref?.current?.isSubmitting ?? false; - return is_submitting || Object.keys(errors).length > 0; - }; - const handleCancel = (values: TAddressDetailFormProps) => { const current_step = (getCurrentStep?.() || 1) - 1; onSave(current_step, values); @@ -134,16 +118,10 @@ const AddressDetails = observer( }; return ( - + {({ handleSubmit, - errors, + isSubmitting, values, setFieldValue, handleChange, @@ -157,12 +135,14 @@ const AddressDetails = observer( setRef: (instance: HTMLFormElement) => void; height: number | string; }) => ( -
+ //noValidate here is for skipping default browser validation + + {!isFetched && ( @@ -266,7 +246,8 @@ const AddressDetails = observer( }} disabled={ disabled_items.includes('address_state') || - (props.value?.address_state && has_real_account) + (!!props.value?.address_state && + has_real_account) } /> @@ -281,13 +262,13 @@ const AddressDetails = observer( placeholder={localize('State/Province')} disabled={ disabled_items.includes('address_state') || - (props.value?.address_state && has_real_account) + (!!props.value?.address_state && has_real_account) } /> )} { @@ -296,7 +277,7 @@ const AddressDetails = observer( }} disabled={ disabled_items.includes('address_postcode') || - (props.value?.address_postcode && has_real_account) + (!!props.value?.address_postcode && has_real_account) } /> @@ -304,7 +285,7 @@ const AddressDetails = observer( void, next_step: () => void ) => void; - selected_step_ref?: React.RefObject>; set_currency: boolean; validate: (values: TCurrencySelectorFormProps) => TCurrencySelectorFormProps; value: TCurrencySelectorFormProps; @@ -58,7 +57,6 @@ type TCurrencySelector = React.HTMLAttributes { @@ -114,10 +111,6 @@ const CurrencySelector = observer( item => item.landing_company_shortcode === real_account_signup_target ).length; - const isSubmitDisabled = (values: TCurrencySelectorFormProps) => { - return selected_step_ref?.current?.isSubmitting || !values.currency; - }; - const handleCancel = (values: TCurrencySelectorFormProps) => { const current_step = getCurrentStep() - 1; onSave(current_step, values); @@ -196,7 +189,6 @@ const CurrencySelector = observer( return ( { onSubmit(getCurrentStep ? getCurrentStep() - 1 : null, values, actions.setSubmitting, goToNextStep); @@ -288,7 +280,7 @@ const CurrencySelector = observer( ? 'currency-selector--set-currency' : 'currency-selector--deriv-account' } - is_disabled={isSubmitDisabled(values)} + is_disabled={!values.currency} is_center={false} is_absolute={set_currency} label={getSubmitLabel()} diff --git a/packages/account/src/Components/financial-details/financial-details.tsx b/packages/account/src/Components/financial-details/financial-details.tsx index 54a25059229a..d3bfe2efcc8c 100644 --- a/packages/account/src/Components/financial-details/financial-details.tsx +++ b/packages/account/src/Components/financial-details/financial-details.tsx @@ -13,6 +13,7 @@ import { isDesktop, isMobile } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; import FinancialInformation from './financial-details-partials'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; +import ScrollToFieldWithError from '../forms/scroll-to-field-with-error'; type TFinancialDetailsFormValues = { income_source: string; @@ -59,6 +60,11 @@ const FinancialDetails = (props: TFinancialDetails) => { return errors; }; + const fields_to_scroll_top = isMobile() + ? ['income_source', 'account_turnover', 'estimated_worth'] + : ['income_source']; + const fields_to_scroll_bottom = isMobile() ? [] : ['account_turnover', 'estimated_worth']; + return ( { }} validateOnMount > - {({ handleSubmit, isSubmitting, errors, values }) => { + {({ handleSubmit, isSubmitting, values }) => { return ( {({ @@ -78,7 +84,11 @@ const FinancialDetails = (props: TFinancialDetails) => { setRef: (instance: HTMLFormElement) => void; height?: number | string; }) => ( - + + { 0} + is_disabled={isSubmitting} is_absolute={isMobile()} label={localize('Next')} has_cancel diff --git a/packages/account/src/Components/forms/confirmation-checkbox/__tests__/confirmation-checkbox.spec.tsx b/packages/account/src/Components/forms/confirmation-checkbox/__tests__/confirmation-checkbox.spec.tsx deleted file mode 100644 index 4601c86759be..000000000000 --- a/packages/account/src/Components/forms/confirmation-checkbox/__tests__/confirmation-checkbox.spec.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Form, Formik } from 'formik'; - -import { render, screen } from '@testing-library/react'; - -import { ConfirmationCheckbox } from '../confirmation-checkbox'; - -describe('ConfirmationCheckbox', () => { - const props: React.ComponentProps = { - label: 'I confirm my details are correct.', - }; - - test('renders checkbox with label', () => { - render( - - - - - - ); - - const checkbox = screen.getByLabelText('I confirm my details are correct.'); - expect(checkbox).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Components/forms/confirmation-checkbox/confirmation-checkbox.tsx b/packages/account/src/Components/forms/confirmation-checkbox/confirmation-checkbox.tsx deleted file mode 100644 index c67256f7d1ea..000000000000 --- a/packages/account/src/Components/forms/confirmation-checkbox/confirmation-checkbox.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { useFormikContext } from 'formik'; - -import { Checkbox, Text } from '@deriv/components'; -import { isMobile } from '@deriv/shared'; - -/** - * Props for the confirmation checkbox component. - */ -type TConfirmationCheckboxProps = { - /** - * The label of the checkbox. - */ - label: React.ReactNode; - /** - * The size of the checkbox label. - */ - label_size?: 'xxxxs' | 'xxxs' | 'xxs' | 'xs' | 's' | 'sm' | 'm' | 'l' | 'xl' | 'xxl'; - disabled?: boolean; -}; - -/** - * A checkbox component for confirming an action with an optional description. - * - * This component renders a checkbox that can be used to confirm an action, such as agreeing to terms - * and conditions. It also allows displaying an optional description next to the checkbox. - * - * **Note**: This component is meant to be used with Formik forms. - * To use this component, you must set initialStatus in the Formik form to { is_confirmed: false }. - * - * @name ConfirmationCheckbox - * @returns {JSX.Element} React component that renders a checkbox with a label - */ -export const ConfirmationCheckbox = ({ - label, - label_size, - disabled = false, -}: TConfirmationCheckboxProps): JSX.Element => { - /** - * The formik context for the current form. - * - * This context provides information about the form's state and helps in managing form behavior. - */ - const { setStatus, status } = useFormikContext(); - - const handleChange = () => { - // check if status is an object to avoid overwriting the status if it is a string - if (typeof status === 'object') setStatus({ ...status, is_confirmed: !status?.is_confirmed }); - }; - - return ( - {label}} - disabled={disabled} - onChange={handleChange} - /> - ); -}; diff --git a/packages/account/src/Components/forms/confirmation-checkbox/index.ts b/packages/account/src/Components/forms/confirmation-checkbox/index.ts deleted file mode 100644 index 2bac013d8e46..000000000000 --- a/packages/account/src/Components/forms/confirmation-checkbox/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ConfirmationCheckbox } from './confirmation-checkbox'; - -export default ConfirmationCheckbox; diff --git a/packages/account/src/Components/forms/idv-form.tsx b/packages/account/src/Components/forms/idv-form.tsx index 9febec547d50..36dfdc4069e6 100644 --- a/packages/account/src/Components/forms/idv-form.tsx +++ b/packages/account/src/Components/forms/idv-form.tsx @@ -204,7 +204,9 @@ const IDVForm = ({ } disabled={!values.document_type.id} error={ - (touched.document_number && errors.document_number) || + (values.document_type.id && + touched.document_number && + errors.document_number) || errors.error_message } autoComplete='off' @@ -218,7 +220,10 @@ const IDVForm = ({ } className='additional-field' required - label={generatePlaceholderText(selected_doc)} + label={ + values.document_type.id && + generatePlaceholderText(selected_doc) + } /> {values.document_type.additional?.display_name && ( { const { @@ -55,8 +52,7 @@ const PersonalDetailsForm = props => { const [is_tax_residence_popover_open, setIsTaxResidencePopoverOpen] = React.useState(false); const [is_tin_popover_open, setIsTinPopoverOpen] = React.useState(false); - const { errors, touched, values, setFieldValue, handleChange, handleBlur, setFieldTouched, setStatus, status } = - useFormikContext(); + const { errors, touched, values, setFieldValue, handleChange, handleBlur, setFieldTouched } = useFormikContext(); React.useEffect(() => { if (should_close_tooltip) { @@ -65,12 +61,6 @@ const PersonalDetailsForm = props => { } }, [should_close_tooltip, handleToolTipStatus, setShouldCloseTooltip]); - React.useEffect(() => { - if (no_confirmation_needed && typeof status === 'object' && !status.is_confirmed) { - setStatus({ ...status, is_confirmed: true }); - } - }, [no_confirmation_needed, setStatus, status]); - const getNameAndDobLabels = () => { const is_asterisk_needed = is_svg || is_mf || is_rendered_for_onfido || is_qualified_for_idv; const first_name_label = is_asterisk_needed ? localize('First name*') : localize('First name'); @@ -206,6 +196,7 @@ const PersonalDetailsForm = props => { disabled={ !!values.salutation && isFieldImmutable('salutation', editable_fields) } + has_error={!!(touched.salutation && errors.salutation)} /> ))} @@ -529,6 +520,9 @@ const PersonalDetailsForm = props => { )} withTabIndex={0} data-testid='tax_identification_confirm' + has_error={ + !!(touched.tax_identification_confirm && errors.tax_identification_confirm) + } /> )} @@ -547,11 +541,17 @@ const PersonalDetailsForm = props => { {!no_confirmation_needed && is_qualified_for_idv && ( - } + label_font_size={isMobile() ? 'xxs' : 'xs'} + disabled={is_confirmation_checkbox_disabled} + onChange={handleChange} + has_error={!!(touched.confirmation_checkbox && errors.confirmation_checkbox)} /> )} diff --git a/packages/account/src/Components/forms/scroll-to-field-with-error.tsx b/packages/account/src/Components/forms/scroll-to-field-with-error.tsx new file mode 100644 index 000000000000..a982e4777841 --- /dev/null +++ b/packages/account/src/Components/forms/scroll-to-field-with-error.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; + +type TScrollToFieldWithError = { + fields_to_scroll_top?: string[]; + fields_to_scroll_bottom?: string[]; + should_recollect_inputs_names?: boolean; +}; + +const ScrollToFieldWithError = ({ + fields_to_scroll_top, + fields_to_scroll_bottom, + should_recollect_inputs_names = false, +}: TScrollToFieldWithError) => { + const [all_page_inputs_names, setAllPageInputsNames] = React.useState([]); + const { errors, isSubmitting } = useFormikContext(); + const scrollToElement = (element_name: string, block: ScrollLogicalPosition = 'center') => { + if (!element_name) return; + const el = document.querySelector(`[name="${element_name}"]`) as HTMLInputElement; + (el?.parentElement ?? el)?.scrollIntoView({ behavior: 'smooth', block }); + if (el?.type !== 'radio') el?.focus(); + }; + + React.useEffect(() => { + const inputs = [...document.querySelectorAll('input, select')] as HTMLInputElement[]; + setAllPageInputsNames(inputs.map(input => input.name)); + }, [should_recollect_inputs_names]); + React.useEffect(() => { + const current_error_field_name = + all_page_inputs_names.find(input_name => Object.hasOwn(errors, input_name)) || ''; + + if (fields_to_scroll_top?.includes(current_error_field_name)) { + scrollToElement(current_error_field_name, 'start'); + } else if (fields_to_scroll_bottom?.includes(current_error_field_name)) { + scrollToElement(current_error_field_name, 'end'); + } else { + scrollToElement(current_error_field_name); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSubmitting]); + + return null; +}; + +export default ScrollToFieldWithError; diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index 913c84d75b57..47e657d283fb 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -19,10 +19,11 @@ import { shouldHideHelperImage, shouldShowIdentityInformation, } from 'Helpers/utils'; -import FormSubHeader from '../form-sub-header'; import IDVForm from '../forms/idv-form'; import PersonalDetailsForm from '../forms/personal-details-form'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; +import FormSubHeader from '../form-sub-header'; +import ScrollToFieldWithError from '../forms/scroll-to-field-with-error'; const PersonalDetails = ({ getCurrentStep, @@ -40,17 +41,13 @@ const PersonalDetails = ({ is_virtual, is_fully_authenticated, account_opening_reason_list, - selected_step_ref, closeRealAccountSignup, has_real_account, ...props }) => { const { account_status, account_settings, residence, real_account_signup_target } = props; const [should_close_tooltip, setShouldCloseTooltip] = React.useState(false); - - const isSubmitDisabled = errors => { - return selected_step_ref?.current?.isSubmitting || Object.keys(errors).length > 0; - }; + const [no_confirmation_needed, setNoConfirmationNeeded] = React.useState(false); const handleCancel = values => { const current_step = getCurrentStep() - 1; @@ -81,10 +78,16 @@ const PersonalDetails = ({ } errors.document_number = isDocumentNumberValid(document_number, document_type); + + if (document_type.id !== IDV_NOT_APPLICABLE_OPTION.id && !values.confirmation_checkbox) { + errors.confirmation_checkbox = 'error'; + } return removeEmptyPropertiesFromObject(errors); }; const handleValidate = values => { + setNoConfirmationNeeded(values?.document_type?.id === IDV_NOT_APPLICABLE_OPTION.id); + let idv_error = {}; if (is_qualified_for_idv) { idv_error = validateIDV(values); @@ -118,26 +121,32 @@ const PersonalDetails = ({ return ( { onSubmit(getCurrentStep() - 1, values, actions.setSubmitting, goToNextStep); }} > - {({ handleSubmit, errors, setFieldValue, touched, values, handleChange, handleBlur, status }) => ( + {({ handleSubmit, errors, isSubmitting, setFieldValue, touched, values, handleChange, handleBlur }) => ( {({ setRef, height }) => (
+ {!is_qualified_for_idv && ( @@ -185,7 +194,7 @@ const PersonalDetails = ({ is_mf={is_mf} is_qualified_for_idv={is_qualified_for_idv} editable_fields={getEditableFields( - status?.is_confirmed, + values.confirmation_checkbox, values?.document_type?.id )} residence_list={residence_list} @@ -197,9 +206,7 @@ const PersonalDetails = ({ should_close_tooltip={should_close_tooltip} setShouldCloseTooltip={setShouldCloseTooltip} should_hide_helper_image={shouldHideHelperImage(values?.document_type?.id)} - no_confirmation_needed={ - values?.document_type?.id === IDV_NOT_APPLICABLE_OPTION.id - } + no_confirmation_needed={no_confirmation_needed} /> @@ -208,7 +215,7 @@ const PersonalDetails = ({ handleCancel(values)} 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 a4c01889c3fa..6837074c2440 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 @@ -104,7 +104,7 @@ describe('', () => { expect(mock_props.handleBack).toHaveBeenCalledTimes(1); const document_type_input = screen.getByLabelText('Choose the document type'); - const document_number_input = screen.getByLabelText('Enter your document number'); + const document_number_input = screen.getByPlaceholderText('Enter your document number'); expect(document_number_input).toBeDisabled(); expect(screen.queryByText('Test document 1 name')).not.toBeInTheDocument(); expect(screen.queryByText('Test document 2 name')).not.toBeInTheDocument(); @@ -139,7 +139,7 @@ describe('', () => { const document_type_input = screen.getByRole('combobox'); expect(document_type_input.name).toBe('document_type'); - const document_number_input = screen.getByLabelText('Enter your document number'); + const document_number_input = screen.getByPlaceholderText('Enter your document number'); expect(document_number_input.name).toBe('document_number'); expect(document_number_input).toBeDisabled(); @@ -162,7 +162,9 @@ describe('', () => { }); fireEvent.click(confirmation_checkbox); - expect(verifyBtn).toBeEnabled(); + await waitFor(() => { + expect(verifyBtn).toBeEnabled(); + }); fireEvent.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 026308a1b066..b1249555623c 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 @@ -58,6 +58,7 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c example_format: '', }, document_number: '', + confirmation_checkbox: false, ...form_initial_values, }; @@ -103,6 +104,10 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c errors.last_name = validateName(values.last_name); } + if (!values.confirmation_checkbox) { + errors.confirmation_checkbox = 'error'; + } + return removeEmptyPropertiesFromObject(errors); }; @@ -143,14 +148,7 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c }; return ( - + {({ dirty, errors, @@ -162,7 +160,6 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c setFieldValue, touched, values, - status, }) => (
@@ -188,7 +185,7 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c })} is_qualified_for_idv should_hide_helper_image={shouldHideHelperImage(values?.document_type?.id)} - editable_fields={status?.is_confirmed ? [] : changeable_fields} + editable_fields={values.confirmation_checkbox ? [] : changeable_fields} />
@@ -202,7 +199,7 @@ const IdvDocumentSubmit = observer(({ handleBack, handleViewComplete, selected_c type='submit' onClick={handleSubmit} has_effect - is_disabled={!dirty || isSubmitting || !isValid || !status?.is_confirmed} + is_disabled={!dirty || isSubmitting || !isValid} text={localize('Verify')} large primary diff --git a/packages/account/src/Components/poi/poi-form-on-signup/idv-doc-submit-on-signup/idv-doc-submit-on-signup.tsx b/packages/account/src/Components/poi/poi-form-on-signup/idv-doc-submit-on-signup/idv-doc-submit-on-signup.tsx index 4df0df924f3e..ea9f8a41a1f9 100644 --- a/packages/account/src/Components/poi/poi-form-on-signup/idv-doc-submit-on-signup/idv-doc-submit-on-signup.tsx +++ b/packages/account/src/Components/poi/poi-form-on-signup/idv-doc-submit-on-signup/idv-doc-submit-on-signup.tsx @@ -59,6 +59,10 @@ export const IdvDocSubmitOnSignup = ({ errors.last_name = validateName(values.last_name); } + if (!values.confirmation_checkbox) { + errors.confirmation_checkbox = 'error'; + } + return removeEmptyPropertiesFromObject(errors); }; @@ -78,6 +82,7 @@ export const IdvDocSubmitOnSignup = ({ value: '', example_format: '', }, + confirmation_checkbox: false, document_number: '', ...form_initial_values, }; @@ -92,22 +97,8 @@ export const IdvDocSubmitOnSignup = ({ validateOnMount validateOnChange validateOnBlur - initialStatus={{ - is_confirmed: false, - }} > - {({ - errors, - handleBlur, - handleChange, - isSubmitting, - isValid, - setFieldValue, - touched, - dirty, - values, - status, - }) => ( + {({ errors, handleBlur, handleChange, isSubmitting, isValid, setFieldValue, touched, dirty, values }) => (
@@ -132,7 +123,7 @@ export const IdvDocSubmitOnSignup = ({ is_qualified_for_idv is_appstore should_hide_helper_image={shouldHideHelperImage(values?.document_type?.id)} - editable_fields={status?.is_confirmed ? [] : changeable_fields} + editable_fields={values.confirmation_checkbox ? [] : changeable_fields} residence_list={residence_list} />
@@ -141,7 +132,7 @@ export const IdvDocSubmitOnSignup = ({ className='proof-of-identity__submit-button' type='submit' has_effect - is_disabled={!dirty || isSubmitting || !isValid || !status?.is_confirmed} + is_disabled={!dirty || isSubmitting || !isValid} text={localize('Next')} large primary diff --git a/packages/account/src/Components/trading-assessment/trading-assessment-dropdown.jsx b/packages/account/src/Components/trading-assessment/trading-assessment-dropdown.jsx index c7c26e01a5fd..b9fd57924af8 100644 --- a/packages/account/src/Components/trading-assessment/trading-assessment-dropdown.jsx +++ b/packages/account/src/Components/trading-assessment/trading-assessment-dropdown.jsx @@ -1,8 +1,9 @@ import React from 'react'; +import classNames from 'classnames'; import { Field } from 'formik'; import { DesktopWrapper, Dropdown, MobileWrapper, Text, SelectNative } from '@deriv/components'; -import { localize, getLanguage } from '@deriv/translations'; -import classNames from 'classnames'; +import { localize } from '@deriv/translations'; +import { MAX_QUESTION_TEXT_LENGTH } from '../../Constants/trading-assessment'; const TradingAssessmentDropdown = ({ disabled_items, @@ -31,27 +32,27 @@ const TradingAssessmentDropdown = ({
{item_list.map(question => ( - {() => { + {({ field, meta }) => { + const should_extend_trading_frequency_field = + question.form_control === 'trading_frequency_financial_instruments' && + question?.question_text.length > MAX_QUESTION_TEXT_LENGTH; + return ( onChange(e, question.form_control, setFieldValue)} value={values[question.form_control]} disabled={disabled_items.includes(question.form_control)} + error={meta.touched && meta.error} /> @@ -59,9 +60,10 @@ const TradingAssessmentDropdown = ({ {question?.question_text} { onChange(e, question.form_control, setFieldValue); @@ -69,6 +71,7 @@ const TradingAssessmentDropdown = ({ value={values[question.form_control]} hide_top_placeholder disabled={disabled_items.includes(question.form_control)} + error={meta.touched && meta.error} /> diff --git a/packages/account/src/Components/trading-assessment/trading-assessment-form.jsx b/packages/account/src/Components/trading-assessment/trading-assessment-form.jsx index 55fe6d1d54a8..3507a155ef79 100644 --- a/packages/account/src/Components/trading-assessment/trading-assessment-form.jsx +++ b/packages/account/src/Components/trading-assessment/trading-assessment-form.jsx @@ -2,8 +2,10 @@ import classNames from 'classnames'; import React from 'react'; import { Formik, Form } from 'formik'; import { Button, Modal, Text } from '@deriv/components'; -import { isMobile } from '@deriv/shared'; -import { localize, Localize, getLanguage } from '@deriv/translations'; +import { isEmptyObject, isMobile } from '@deriv/shared'; +import { localize, Localize } from '@deriv/translations'; +import { MAX_QUESTION_TEXT_LENGTH } from '../../Constants/trading-assessment'; +import ScrollToFieldWithError from '../forms/scroll-to-field-with-error'; import TradingAssessmentRadioButton from './trading-assessment-radio-buttons.jsx'; import TradingAssessmentDropdown from './trading-assessment-dropdown.jsx'; @@ -19,7 +21,6 @@ const TradingAssessmentForm = ({ is_independent_section, }) => { const [is_section_filled, setIsSectionFilled] = React.useState(false); - const [should_inform_user, shouldInformUser] = React.useState(false); const [current_question_details, setCurrentQuestionDetails] = React.useState({ current_question_index: 0, current_question: {}, @@ -32,13 +33,6 @@ const TradingAssessmentForm = ({ ? current_question_details.current_question_index !== 0 : true; - const verifyIfAllFieldsFilled = () => { - shouldInformUser(!is_section_filled); - setTimeout(() => { - shouldInformUser(false); - }, 500); - }; - React.useEffect(() => { setCurrentQuestionDetails(prevState => { return { @@ -111,87 +105,145 @@ const TradingAssessmentForm = ({ const isAssessmentCompleted = answers => Object.values(answers).every(answer => Boolean(answer)); const nextButtonHandler = values => { - verifyIfAllFieldsFilled(); if (is_section_filled) { if (isAssessmentCompleted(values) && stored_items === last_question_index) onSubmit(values); else displayNextPage(); } }; + const handleValidate = values => { + const errors = {}; + + if (!values.risk_tolerance && current_question_details.current_question.section === 'risk_tolerance') { + errors.risk_tolerance = 'error'; + } + if ( + !values.source_of_experience && + current_question_details.current_question.section === 'source_of_experience' + ) { + errors.source_of_experience = 'error'; + } + if (current_question_details.current_question.section === 'trading_experience') { + const trading_experience_required_fields = [ + 'cfd_experience', + 'cfd_frequency', + 'trading_experience_financial_instruments', + 'trading_frequency_financial_instruments', + ]; + trading_experience_required_fields.forEach(field => { + if (!values[field]) { + errors[field] = localize('Please select an option'); + } + }); + } + if (current_question_details.current_question.section === 'trading_knowledge') { + const trading_knowledge_required_fields = [ + 'cfd_trading_definition', + 'leverage_impact_trading', + 'leverage_trading_high_risk_stop_loss', + 'required_initial_margin', + ]; + trading_knowledge_required_fields.forEach(field => { + if (!values[field] && current_question_details.current_question.form_control === field) { + errors[field] = 'error'; + } + }); + } + + return errors; + }; + return (
-
- - {({ setFieldValue, values }) => { - const { question_text, form_control, answer_options, questions } = - current_question_details.current_question; - - return ( - -
- {questions?.length ? ( - - ) : ( - handleValueSelection(e, form_control, setFieldValue, values)} - values={values} - form_control={form_control} - setEnableNextSection={setIsSectionFilled} - disabled_items={disabled_items ?? []} - /> - )} -
- - - {should_display_previous_button && ( + + {({ errors, setFieldValue, values }) => { + const { question_text, form_control, answer_options, questions } = + current_question_details.current_question; + const has_long_question = questions?.some( + question => question.question_text.length > MAX_QUESTION_TEXT_LENGTH + ); + + return ( + + + + + * {!isEmptyObject(errors) && } + + +
+ + +
+ {questions?.length ? ( + + ) : ( + { + handleValueSelection(e, form_control, setFieldValue, values); + }} + values={values} + form_control={form_control} + setEnableNextSection={setIsSectionFilled} + disabled_items={disabled_items ?? []} + /> + )} +
+ + + {should_display_previous_button && ( +
+
+
+ +
+ + ); + }} +
); }; diff --git a/packages/account/src/Components/trading-assessment/trading-assessment-radio-buttons.jsx b/packages/account/src/Components/trading-assessment/trading-assessment-radio-buttons.jsx index 8d95b0829338..5616508246cd 100644 --- a/packages/account/src/Components/trading-assessment/trading-assessment-radio-buttons.jsx +++ b/packages/account/src/Components/trading-assessment/trading-assessment-radio-buttons.jsx @@ -25,6 +25,7 @@ const TradingAssessmentRadioButton = ({ , 'value' | 'label'> greyDisabled?: boolean; id?: string; label: string | React.ReactElement; + label_font_size?: string; onChange?: (e: React.ChangeEvent | React.KeyboardEvent) => void; value?: boolean; withTabIndex?: number; + has_error?: boolean; }; const Checkbox = React.forwardRef( @@ -24,11 +26,13 @@ const Checkbox = React.forwardRef( disabled = false, id, label, + label_font_size = 'xs', defaultChecked, onChange, // This needs to be here so it's not included in `otherProps` value = false, withTabIndex = 0, greyDisabled = false, + has_error = false, ...otherProps }, ref @@ -84,7 +88,13 @@ const Checkbox = React.forwardRef( {!!checked && } - + {label} diff --git a/packages/components/src/components/dropdown/dropdown.scss b/packages/components/src/components/dropdown/dropdown.scss index e7db408e4003..c4c6724357e4 100644 --- a/packages/components/src/components/dropdown/dropdown.scss +++ b/packages/components/src/components/dropdown/dropdown.scss @@ -179,6 +179,9 @@ & .dc-dropdown__label { color: var(--brand-red-coral) !important; } + & .dc-dropdown__display-placeholder-text { + color: var(--brand-red-coral) !important; + } } &__list { left: 0; diff --git a/packages/components/src/components/input/input.scss b/packages/components/src/components/input/input.scss index fe19c90bb935..bb5b2c0fcead 100644 --- a/packages/components/src/components/input/input.scss +++ b/packages/components/src/components/input/input.scss @@ -42,6 +42,11 @@ label { color: var(--brand-red-coral) !important; } + + & ::placeholder { + color: var(--text-loss-danger) !important; + opacity: 1 !important; + } } &__container { display: flex; diff --git a/packages/components/src/components/radio-group/radio-group.scss b/packages/components/src/components/radio-group/radio-group.scss index f7f5b3628342..54ce0a77c187 100644 --- a/packages/components/src/components/radio-group/radio-group.scss +++ b/packages/components/src/components/radio-group/radio-group.scss @@ -33,10 +33,16 @@ border-width: 4px; border-color: var(--brand-red-coral); } + &--error { + border-color: var(--text-less-prominent); + } } &__label { &--disabled { color: var(--text-disabled); } + &--error { + color: var(--text-loss-danger); + } } } diff --git a/packages/components/src/components/radio-group/radio-group.tsx b/packages/components/src/components/radio-group/radio-group.tsx index 73b376417bcb..deba85081a1e 100644 --- a/packages/components/src/components/radio-group/radio-group.tsx +++ b/packages/components/src/components/radio-group/radio-group.tsx @@ -8,6 +8,7 @@ type TItem = React.HTMLAttributes & { label: string; disabled?: boolean; hidden?: boolean; + has_error?: boolean; }; type TItemWrapper = { should_wrap_items?: boolean; @@ -75,12 +76,14 @@ const RadioGroup = ({ className={classNames('dc-radio-group__circle', { 'dc-radio-group__circle--selected': selected_option === item.props.value, 'dc-radio-group__circle--disabled': item.props.disabled, + 'dc-radio-group__circle--error': item.props.has_error, })} /> {item.props.label} diff --git a/packages/core/src/App/Containers/RealAccountSignup/account-wizard.jsx b/packages/core/src/App/Containers/RealAccountSignup/account-wizard.jsx index a7512e6ab496..0aae229ec628 100644 --- a/packages/core/src/App/Containers/RealAccountSignup/account-wizard.jsx +++ b/packages/core/src/App/Containers/RealAccountSignup/account-wizard.jsx @@ -219,6 +219,7 @@ const AccountWizard = props => { delete clone?.tax_identification_confirm; delete clone?.agreed_tnc; delete clone?.agreed_tos; + delete clone?.confirmation_checkbox; // BE does not accept empty strings for TIN // so we remove it from the payload if it is empty in case of optional TIN field diff --git a/packages/core/src/sass/account-wizard.scss b/packages/core/src/sass/account-wizard.scss index 0d8b8fc5d2c3..396adf611ac0 100644 --- a/packages/core/src/sass/account-wizard.scss +++ b/packages/core/src/sass/account-wizard.scss @@ -568,8 +568,17 @@ } } + &__question-counter { + margin: 2.4rem 0 0; + padding-left: 12rem; + + @include mobile { + padding-left: 1.6rem; + } + } + &__form { - margin-top: 2.2rem; + margin-top: 1.6rem; display: grid; grid-template-rows: 4.7fr 1fr; @@ -585,10 +594,6 @@ display: flex; padding: 0 10rem; - @include desktop() { - justify-content: center; - } - @include mobile() { padding: 0 1.6rem; } @@ -609,6 +614,11 @@ } &__wrapper__question { + .dc-field--error { + padding-left: 0; + margin: 1.6rem 0; + } + @include desktop() { padding: 0 2rem; } @@ -707,73 +717,6 @@ } } -.highlight { - animation: shake 150ms 2 linear; - -moz-animation: shake 150ms 2 linear; - -webkit-animation: shake 150ms 2 linear; - -o-animation: shake 150ms 2 linear; -} - -@keyframes shake { - 0% { - transform: translate(3px, 0); - } - 50% { - transform: translate(-3px, 0); - } - 100% { - transform: translate(0, 0); - } -} - -@-moz-keyframes shake { - 0% { - -moz-transform: translate(3px, 0); - } - 50% { - -moz-transform: translate(-3px, 0); - } - 100% { - -moz-transform: translate(0, 0); - } -} - -@-webkit-keyframes shake { - 0% { - -webkit-transform: translate(3px, 0); - } - 50% { - -webkit-transform: translate(-3px, 0); - } - 100% { - -webkit-transform: translate(0, 0); - } -} - -@-ms-keyframes shake { - 0% { - -ms-transform: translate(3px, 0); - } - 50% { - -ms-transform: translate(-3px, 0); - } - 100% { - -ms-transform: translate(0, 0); - } -} - -@-o-keyframes shake { - 0% { - -o-transform: translate(3px, 0); - } - 50% { - -o-transform: translate(-3px, 0); - } - 100% { - -o-transform: translate(0, 0); - } -} - .field-layout { .trading-assessment__wrapper__dropdown { div:last-child {