From 8d9005f0b4ff58b3c19abe151855b96522779d09 Mon Sep 17 00:00:00 2001 From: yauheni-deriv <103182683+yauheni-deriv@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:50:04 +0300 Subject: [PATCH] Kyc/wall 1322/revamp poa section (#9623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: :bento: added visual assets * Kyc/wall 1325/revamp poa section (#6) * feat: :sparkles: added new component * feat: :white_check_mark: added testcases * feat: account page revamp, cfd poa remove, account poa reuse * refactor: simplified && condition * chore: modal alignment styles * fix: failing tests * refactor: some ts issues resolve * refactor: poa container TS migration * chore: styles clarifications * refactor: poa form ts migration, code refactor * fix: ts ignore for cfd build * fix: commented code * fix: failing test * chore: code refactoring * chore: recreating PR for kyc-WALL-1322-revamp-poa-section * chore: incorporated review comments * chore: review comments * chore: review comments2 * chore: remove unused variable * chore: cfd-poa testcases * refactor: review comments * chore: review comments * ref: Incorporated review somments * chore: review coment * chore: review comments * chore: review comments addressing * fix: failing test * chore: styles code refactor * chore: spaces fix * refactor: file descriptions map array * chore: review comments * chore: review comments * chore: review comments * chore: compare account flow fix * chore: review comments * chore: review comments * chore: review comments * chore: review comments incorporating * fix: styling discrepancies * fix: back button condition * chore: remove back button * fix: failing test cfd-poa * refactor: localize change to component * refactor: :art: incorporated Localize component * chore: remove unused css and types * chore: wrong description on examples * fix: styles for resubmit message * fix: submitting error poa page * chore: mobing submit error to the top * fix: wrong message after submitting documents * refactor: boolean instead !! * chore: added gif as supported file * fix: failing test * fix: resolve path --------- Co-authored-by: Likhith Kolayari Co-authored-by: Likhith Kolayari <98398322+likhith-deriv@users.noreply.github.com> Co-authored-by: “yauheni-kryzhyk-deriv” <“yauheni@deriv.me”> --- packages/account/build/webpack.config.js | 2 +- packages/account/src/App.tsx | 2 +- .../account/src/Assets/ic-blurry-document.svg | 155 ++++++ .../src/Assets/ic-cropped-document.svg | 78 +++ .../Assets/ic-document-address-mismatch.svg | 58 +++ .../src/Assets/ic-document-name-mismatch.svg | 50 ++ packages/account/src/Assets/ic-envelop.svg | 110 ++++ .../account/src/Assets/ic-error-badge.svg | 4 + .../src/Assets/ic-old-issued-document.svg | 57 +++ .../__tests__/address-details.spec.tsx | 23 +- .../address-details/address-details.tsx | 23 +- .../Components/demo-message/demo-message.tsx | 2 +- .../file-uploader-container.spec.tsx | 126 ++--- .../__tests__/file-uploader.spec.tsx | 30 +- .../file-uploader-container.tsx | 142 ++--- .../file-uploader-container/file-uploader.tsx | 28 +- .../form-body-section/form-body-section.tsx | 10 +- .../src/Components/form-body/form-body.tsx | 2 +- .../Components/form-footer/form-footer.tsx | 20 +- .../form-sub-header/form-sub-header.tsx | 12 +- .../src/Components/forms/form-fields.jsx | 3 +- .../forms/personal-details-form.jsx | 169 +++++- .../icon-with-message/icon-with-message.tsx | 8 +- .../inline-note-with-icon.scss | 4 + .../__tests__/leave-confirm.spec.tsx | 2 +- .../leave-confirm/leave-confirm.tsx | 22 +- .../load-error-message/load-error-message.tsx | 2 +- .../__tests__/personal-details.spec.js | 88 +--- .../personal-details/personal-details.jsx | 12 +- .../common-mistake-examples.spec.tsx | 12 + .../common-mistake-examples.scss | 38 ++ .../common-mistake-examples.tsx | 57 +++ .../Components/poa/status/expired/expired.tsx | 2 +- .../poa/status/needs-review/needs-review.tsx | 13 +- .../poa/status/not-required/not-required.tsx | 2 +- .../poa/status/status-codes/status-codes.ts | 2 +- .../poa/status/submitted/submitted.tsx | 31 +- .../poa/status/unverified/unverified.tsx | 7 +- .../poa/status/verified/verified.tsx | 35 +- .../idv-document-submit.tsx | 3 - ...oi-confirm-with-example-form-container.tsx | 6 +- .../idv-doc-submit-on-signup.tsx | 5 +- .../Components/poi/status/expired/expired.tsx | 2 +- .../poi/status/verified/verified.tsx | 29 +- .../poa-common-mistake-examples-config.tsx | 50 ++ packages/account/src/Helpers/utils.ts | 7 +- .../__tests__/closing-account-reason.spec.js | 2 - .../Verification/Helpers/verification.js | 17 +- .../Verification/ProofOfAddress/index.js | 1 - .../Verification/ProofOfAddress/index.ts | 3 + .../proof-of-address-container.jsx | 155 ------ .../proof-of-address-container.tsx | 166 ++++++ .../ProofOfAddress/proof-of-address-form.jsx | 464 ----------------- .../ProofOfAddress/proof-of-address-form.tsx | 391 ++++++++++++++ .../ProofOfAddress/proof-of-address.jsx | 26 - .../ProofOfAddress/proof-of-address.tsx | 15 + .../ProofOfOwnership/file-uploader.jsx | 4 +- packages/account/src/Styles/account.scss | 215 +------- .../{common-prop.type.ts => common.type.ts} | 24 +- packages/account/src/Types/context.type.ts | 2 +- packages/account/src/Types/index.ts | 2 +- .../components/cfds-listing/cfds-listing.scss | 158 +----- .../src/Components/__tests__/cfd-poa.spec.js | 261 ---------- .../src/Components/__tests__/cfd-poa.spec.tsx | 106 ++++ packages/cfd/src/Components/cfd-poa.tsx | 483 +----------------- packages/cfd/src/Components/cfd-poi.tsx | 15 +- ...-financial-stp-real-account-signup.spec.js | 1 + .../cfd-financial-stp-real-account-signup.tsx | 25 +- .../cfd/src/Stores/Modules/CFD/cfd-store.js | 36 -- packages/cfd/src/sass/cfd-dashboard.scss | 154 ------ packages/cfd/src/types/cfd-store.types.ts | 8 - .../components/autocomplete/autocomplete.tsx | 2 +- .../file-dropzone/file-dropzone.scss | 1 + .../icon/common/ic-poa-verified-dashboard.svg | 1 - .../common/ic-unsaved-changes-dashboard.svg | 1 - .../src/components/icon/common/ic-upload.svg | 1 + .../components/src/components/icon/icons.js | 3 +- packages/components/stories/icon/icons.js | 3 +- packages/core/src/sass/app.scss | 1 + .../sass/app/_common/components/cfd-poa.scss | 24 + .../_common/components/onfido-container.scss | 153 +++++- .../shared/src/utils/location/location.ts | 20 +- packages/stores/src/mockStore.ts | 1 + packages/stores/types.ts | 5 +- .../SmartChart/Helpers/__tests__/barriers.ts | 6 +- .../sass/app/modules/mt5/cfd-dashboard.scss | 150 ------ types/global.d.ts | 1 + types/utils.d.ts | 4 + 88 files changed, 1963 insertions(+), 2693 deletions(-) create mode 100644 packages/account/src/Assets/ic-blurry-document.svg create mode 100644 packages/account/src/Assets/ic-cropped-document.svg create mode 100644 packages/account/src/Assets/ic-document-address-mismatch.svg create mode 100644 packages/account/src/Assets/ic-document-name-mismatch.svg create mode 100644 packages/account/src/Assets/ic-envelop.svg create mode 100644 packages/account/src/Assets/ic-error-badge.svg create mode 100644 packages/account/src/Assets/ic-old-issued-document.svg create mode 100644 packages/account/src/Components/poa/common-mistakes/__tests__/common-mistake-examples.spec.tsx create mode 100644 packages/account/src/Components/poa/common-mistakes/common-mistake-examples.scss create mode 100644 packages/account/src/Components/poa/common-mistakes/common-mistake-examples.tsx create mode 100644 packages/account/src/Configs/poa-common-mistake-examples-config.tsx delete mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/index.js create mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/index.ts delete mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-container.jsx create mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-container.tsx delete mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx create mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.tsx delete mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address.jsx create mode 100644 packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address.tsx rename packages/account/src/Types/{common-prop.type.ts => common.type.ts} (94%) delete mode 100644 packages/cfd/src/Components/__tests__/cfd-poa.spec.js create mode 100644 packages/cfd/src/Components/__tests__/cfd-poa.spec.tsx delete mode 100644 packages/components/src/components/icon/common/ic-poa-verified-dashboard.svg delete mode 100644 packages/components/src/components/icon/common/ic-unsaved-changes-dashboard.svg create mode 100644 packages/components/src/components/icon/common/ic-upload.svg create mode 100644 packages/core/src/sass/app/_common/components/cfd-poa.scss diff --git a/packages/account/build/webpack.config.js b/packages/account/build/webpack.config.js index 4f3fa3a531b5..5b7c955eda22 100644 --- a/packages/account/build/webpack.config.js +++ b/packages/account/build/webpack.config.js @@ -44,7 +44,7 @@ module.exports = function (env) { 'poi-unsupported': 'Components/poi/status/unsupported', 'poi-upload-complete': 'Components/poi/status/upload-complete', 'poi-verified': 'Components/poi/status/verified', - 'proof-of-address-container': 'Sections/Verification/ProofOfAddress/proof-of-address-container.jsx', + 'proof-of-address-container': 'Sections/Verification/ProofOfAddress/proof-of-address-container', 'proof-of-identity': 'Sections/Verification/ProofOfIdentity/proof-of-identity.jsx', 'proof-of-identity-container': 'Sections/Verification/ProofOfIdentity/proof-of-identity-container.jsx', 'proof-of-identity-config': 'Configs/proof-of-identity-config', diff --git a/packages/account/src/App.tsx b/packages/account/src/App.tsx index 23cbccb72a8e..9c71d674d019 100644 --- a/packages/account/src/App.tsx +++ b/packages/account/src/App.tsx @@ -3,7 +3,7 @@ import Routes from './Containers/routes'; import ResetTradingPassword from './Containers/reset-trading-password'; import { setWebsocket } from '@deriv/shared'; import { StoreProvider } from '@deriv/stores'; -import { TCoreStores } from '@deriv/stores/types'; +import type { TCoreStores } from '@deriv/stores/types'; // TODO: add correct types for WS after implementing them type TAppProps = { diff --git a/packages/account/src/Assets/ic-blurry-document.svg b/packages/account/src/Assets/ic-blurry-document.svg new file mode 100644 index 000000000000..d0cdf47114e8 --- /dev/null +++ b/packages/account/src/Assets/ic-blurry-document.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/account/src/Assets/ic-cropped-document.svg b/packages/account/src/Assets/ic-cropped-document.svg new file mode 100644 index 000000000000..d9914cf734d9 --- /dev/null +++ b/packages/account/src/Assets/ic-cropped-document.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/account/src/Assets/ic-document-address-mismatch.svg b/packages/account/src/Assets/ic-document-address-mismatch.svg new file mode 100644 index 000000000000..d53d8f41fe78 --- /dev/null +++ b/packages/account/src/Assets/ic-document-address-mismatch.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/account/src/Assets/ic-document-name-mismatch.svg b/packages/account/src/Assets/ic-document-name-mismatch.svg new file mode 100644 index 000000000000..427dc50f014d --- /dev/null +++ b/packages/account/src/Assets/ic-document-name-mismatch.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/account/src/Assets/ic-envelop.svg b/packages/account/src/Assets/ic-envelop.svg new file mode 100644 index 000000000000..92d0b680b584 --- /dev/null +++ b/packages/account/src/Assets/ic-envelop.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/account/src/Assets/ic-error-badge.svg b/packages/account/src/Assets/ic-error-badge.svg new file mode 100644 index 000000000000..c06b329f6f62 --- /dev/null +++ b/packages/account/src/Assets/ic-error-badge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/account/src/Assets/ic-old-issued-document.svg b/packages/account/src/Assets/ic-old-issued-document.svg new file mode 100644 index 000000000000..7bd48aaf3630 --- /dev/null +++ b/packages/account/src/Assets/ic-old-issued-document.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx b/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx index 1deb87a55a92..f460ad5fab61 100644 --- a/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx +++ b/packages/account/src/Components/address-details/__tests__/address-details.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import AddressDetails from '../address-details'; -import { isDesktop, isMobile, PlatformContext, TLocationList } from '@deriv/shared'; +import { isDesktop, isMobile, PlatformContext } from '@deriv/shared'; import { FormikProps, FormikValues } from 'formik'; jest.mock('@deriv/shared', () => ({ @@ -40,6 +40,7 @@ describe('', () => { getCurrentStep: jest.fn(), goToNextStep: jest.fn(), goToPreviousStep: jest.fn(), + has_real_account: false, is_gb_residence: '', is_svg: true, onCancel: jest.fn(), @@ -102,9 +103,9 @@ describe('', () => { expect(screen.queryByText(verification_info)).not.toBeInTheDocument(); const inputs: HTMLTextAreaElement[] = screen.getAllByRole('textbox'); - expect(inputs.length).toBe(5); + expect(inputs).toHaveLength(5); const required_fields = inputs.filter(input => input.required === true); - expect(required_fields.length).toBe(2); + expect(required_fields).toHaveLength(2); }); it('should render AddressDetails component and trigger buttons', async () => { @@ -116,10 +117,10 @@ describe('', () => { expect(screen.queryByText(verification_info)).not.toBeInTheDocument(); const inputs: HTMLTextAreaElement[] = screen.getAllByRole('textbox'); - expect(inputs.length).toBe(5); + expect(inputs).toHaveLength(5); const required_fields = inputs.filter(input => input.required === true); - expect(required_fields.length).toBe(2); + expect(required_fields).toHaveLength(2); const previous_btn = screen.getByRole('button', { name: /previous/i }); fireEvent.click(previous_btn); @@ -175,9 +176,9 @@ describe('', () => { expect(mock_props.onSubmitEnabledChange).toHaveBeenCalledTimes(1); const inputs: HTMLTextAreaElement[] = screen.getAllByRole('textbox'); - expect(inputs.length).toBe(5); + expect(inputs).toHaveLength(5); const required_fields = inputs.filter(input => input.required === true); - expect(required_fields.length).toBe(0); + expect(required_fields).toHaveLength(0); await waitFor(() => { expect(screen.getByLabelText(address_line_1)).toBeInTheDocument(); @@ -209,10 +210,10 @@ describe('', () => { expect(screen.queryByText(use_address_info)).not.toBeInTheDocument(); const inputs: HTMLTextAreaElement[] = screen.getAllByRole('textbox'); - expect(inputs.length).toBe(5); + expect(inputs).toHaveLength(5); const required_fields = inputs.filter(input => input.required === true); - expect(required_fields.length).toBe(4); + expect(required_fields).toHaveLength(4); expect(screen.getByLabelText(address_line_1_marked)).toBeInTheDocument(); expect(screen.getByLabelText(address_line_2_marked)).toBeInTheDocument(); @@ -235,7 +236,7 @@ describe('', () => { mock_props.states_list = [ { text: 'State 1', value: 'State 1' }, { text: 'State 2', value: 'State 2' }, - ] as TLocationList[]; + ]; render(); @@ -253,7 +254,7 @@ describe('', () => { mock_props.states_list = [ { text: 'State 1', value: 'State 1' }, { text: 'State 2', value: 'State 2' }, - ] as TLocationList[]; + ]; render(); diff --git a/packages/account/src/Components/address-details/address-details.tsx b/packages/account/src/Components/address-details/address-details.tsx index 708781a99b07..8b7320bb3a73 100644 --- a/packages/account/src/Components/address-details/address-details.tsx +++ b/packages/account/src/Components/address-details/address-details.tsx @@ -1,5 +1,6 @@ import { Formik, Field, FormikProps, FormikValues } from 'formik'; import React from 'react'; +import { StatesList } from '@deriv/api-types'; import { Modal, Autocomplete, @@ -15,19 +16,13 @@ import { Text, } from '@deriv/components'; import { localize, Localize } from '@deriv/translations'; -import { - isDesktop, - isMobile, - getLocation, - makeCancellablePromise, - PlatformContext, - TLocationList, -} from '@deriv/shared'; +import { isDesktop, isMobile, getLocation, makeCancellablePromise, PlatformContext } from '@deriv/shared'; import { splitValidationResultTypes } from '../real-account-signup/helpers/utils'; import classNames from 'classnames'; type TAddressDetails = { - states_list: TLocationList[]; + disabled_items: string[]; + states_list: StatesList; getCurrentStep?: () => number; onSave: (current_step: number, values: FormikValues) => void; onCancel: (current_step: number, goToPreviousStep: () => void) => void; @@ -43,17 +38,13 @@ type TAddressDetails = { is_svg: boolean; is_mf?: boolean; is_gb_residence: boolean | string; + has_real_account: boolean; onSubmitEnabledChange: (is_submit_disabled: boolean) => void; selected_step_ref?: React.RefObject>; fetchStatesList: () => Promise; value: FormikValues; }; -type TFormValidation = { - warnings: { [key: string]: string }; - errors: { [key: string]: string }; -}; - type TInputField = { name: string; required?: boolean | string; @@ -131,7 +122,7 @@ const AddressDetails = ({ return selected_step_ref?.current?.isSubmitting || (errors && Object.keys(errors).length > 0); }; - const checkSubmitStatus = (errors?: { [key: string]: string }) => { + const checkSubmitStatus = (errors?: { [key: string]: string } | FormikValues) => { const is_submit_disabled = isSubmitDisabled(errors); if (is_submit_disabled_ref.current !== is_submit_disabled) { @@ -147,7 +138,7 @@ const AddressDetails = ({ }; const handleValidate = (values: FormikValues) => { - const { errors }: Partial = splitValidationResultTypes(validate(values)); + const { errors } = splitValidationResultTypes(validate(values)); checkSubmitStatus(errors); return errors; }; diff --git a/packages/account/src/Components/demo-message/demo-message.tsx b/packages/account/src/Components/demo-message/demo-message.tsx index f2f379ebc433..796e2d757bb8 100644 --- a/packages/account/src/Components/demo-message/demo-message.tsx +++ b/packages/account/src/Components/demo-message/demo-message.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { localize } from '@deriv/translations'; -import IconWithMessage from 'Components/icon-with-message'; +import IconWithMessage from '../icon-with-message'; type TDemoMessage = { has_demo_icon?: boolean; diff --git a/packages/account/src/Components/file-uploader-container/__tests__/file-uploader-container.spec.tsx b/packages/account/src/Components/file-uploader-container/__tests__/file-uploader-container.spec.tsx index f130d2324d37..36e69d3709a1 100644 --- a/packages/account/src/Components/file-uploader-container/__tests__/file-uploader-container.spec.tsx +++ b/packages/account/src/Components/file-uploader-container/__tests__/file-uploader-container.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { isDesktop, isMobile, PlatformContext } from '@deriv/shared'; -import FileUploaderContainer, { TFileUploaderContainer } from '../file-uploader-container'; +import { isDesktop, isMobile } from '@deriv/shared'; +import FileUploaderContainer from '../file-uploader-container'; jest.mock('@deriv/components', () => { const original_module = jest.requireActual('@deriv/components'); @@ -20,107 +20,75 @@ jest.mock('@deriv/shared', () => ({ })); describe('', () => { + let mock_props: React.ComponentProps; + beforeEach(() => { - isDesktop.mockReturnValue(true); - isMobile.mockReturnValue(false); + mock_props = { + examples: '', + files_description: '', + getSocket: jest.fn(), + onFileDrop: jest.fn(), + onRef: jest.fn(), + settings: {}, + }; + + (isDesktop as jest.Mock).mockReturnValue(true); + (isMobile as jest.Mock).mockReturnValue(false); jest.clearAllMocks(); }); - const props: TFileUploaderContainer = { - getSocket: jest.fn(), - onFileDrop: jest.fn(), - onRef: jest.fn(), - settings: {}, - }; - - const file_size_msg = /less than 8mb/i; - const file_type_msg = /jpeg jpg png pdf gif/i; - const file_time_msg = /1 \- 6 months old/i; - const file_clear_msg = /a clear colour photo or scanned image/i; - const file_address = /issued under your name with your current address/i; + const file_size_msg = /maximum size: 8MB/i; + const file_type_msg = /supported formats: JPEG, JPG, PNG, PDF and GIF only/i; + const file_warning_msg = /remember, selfies, pictures of houses, or non-related images will be rejected./i; + const hint_msg_desktop = /drag and drop a file or click to browse your files/i; + const hint_msg_mobile = /click here to upload/i; const runCommonTests = () => { - expect(screen.getAllByText('mockedIcon')).toHaveLength(6); + expect(screen.getByTestId('dt_file_uploader_container')).toBeInTheDocument(); + expect(screen.getByText('mockedIcon')).toBeInTheDocument(); expect(screen.getByText(file_size_msg)).toBeInTheDocument(); expect(screen.getByText(file_type_msg)).toBeInTheDocument(); - expect(screen.getByText(file_time_msg)).toBeInTheDocument(); - expect(screen.getByText(file_clear_msg)).toBeInTheDocument(); - expect(screen.getByText(file_address)).toBeInTheDocument(); + expect(screen.getByText(file_warning_msg)).toBeInTheDocument(); }; - it('should render FileUploaderContainer component', () => { - render(); - expect(screen.getByTestId('dt_file_uploader_container')).toBeInTheDocument(); - }); - - it('should render FileUploaderContainer component if getSocket is not passed as prop', () => { - render(); - expect(screen.getByTestId('dt_file_uploader_container')).toBeInTheDocument(); - }); - - it('should not render FileUploaderContainer when is_appstore is true in desktop', () => { - render( - - - - ); - expect(screen.queryByTestId('dt_file_uploader_container')).not.toBeInTheDocument(); - }); - it('should show icons and description when is_appstore is true in desktop', () => { - render( - - - - ); + it('should render FileUploaderContainer component and show descriptions', () => { + render(); runCommonTests(); }); - it('should show description when is_appstore false in desktop', () => { - render( - - - - ); + it('should render FileUploaderContainer component if getSocket is not passed as prop', () => { + delete mock_props.getSocket; + render(); runCommonTests(); }); - it('should show description when is_appstore true in mobile', () => { - isMobile.mockReturnValue(true); - isDesktop.mockReturnValue(false); + it('files description and examples should be shown when passed', () => { + mock_props.files_description =
Files description
; + mock_props.examples =
Files failure examples
; - render( - - - - ); - runCommonTests(); + render(); + expect(screen.getByText('Files description')).toBeInTheDocument(); + expect(screen.getByText('Files failure examples')).toBeInTheDocument(); }); - it('should not show description if is_description_enabled is false)', () => { - isMobile.mockReturnValue(true); - isDesktop.mockReturnValue(false); + it('should show hint message for desktop', () => { + render(); + expect(screen.getByText(hint_msg_desktop)).toBeInTheDocument(); + expect(screen.queryByText(hint_msg_mobile)).not.toBeInTheDocument(); + }); - render( - - - - ); + it('should show hint message for mobile', () => { + (isMobile as jest.Mock).mockReturnValue(true); + (isDesktop as jest.Mock).mockReturnValue(false); - expect(screen.getByText('mockedIcon')).toBeInTheDocument(); - expect(screen.queryByText(file_size_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(file_type_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(file_time_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(file_clear_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(file_address)).not.toBeInTheDocument(); + render(); + expect(screen.getByText(hint_msg_mobile)).toBeInTheDocument(); + expect(screen.queryByText(hint_msg_desktop)).not.toBeInTheDocument(); }); it('should call ref function on rendering the component', () => { - render( - - - - ); + render(); - expect(props.onRef).toHaveBeenCalled(); + expect(mock_props.onRef).toHaveBeenCalled(); }); }); diff --git a/packages/account/src/Components/file-uploader-container/__tests__/file-uploader.spec.tsx b/packages/account/src/Components/file-uploader-container/__tests__/file-uploader.spec.tsx index ea1245d225b4..67fd1a236600 100644 --- a/packages/account/src/Components/file-uploader-container/__tests__/file-uploader.spec.tsx +++ b/packages/account/src/Components/file-uploader-container/__tests__/file-uploader.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { compressImageFiles, isMobile, isDesktop, readFiles, TSettings } from '@deriv/shared'; +import { DocumentUploadResponse } from '@deriv/api-types'; +import { compressImageFiles, isMobile, isDesktop, readFiles } from '@deriv/shared'; import FileUploader from '../file-uploader'; jest.mock('@deriv/shared', () => ({ @@ -15,17 +16,12 @@ jest.mock('@binary-com/binary-document-uploader'); describe('', () => { beforeEach(() => { - isDesktop.mockReturnValue(true); - isMobile.mockReturnValue(false); + (isDesktop as jest.Mock).mockReturnValue(true); + (isMobile as jest.Mock).mockReturnValue(false); jest.clearAllMocks(); }); - const props: { - onFileDrop: (file: File | undefined) => void; - getSocket: () => WebSocket; - ref: React.RefObject; - settings: TSettings; - } = { + const props: React.ComponentProps = { onFileDrop: jest.fn(), getSocket: jest.fn(), ref: React.createRef(), @@ -34,7 +30,7 @@ describe('', () => { const large_file_error_msg = /file size should be 8mb or less/i; const file_not_supported_msg = /file uploaded is not supported/i; - const drop_click_msg = /drop file or click here to upload/i; + const drop_click_msg = /drag and drop a file or click to browse your files/i; const click_msg = /click here to upload/i; it('should render FileUploader component in desktop mode', () => { @@ -43,8 +39,8 @@ describe('', () => { }); it('should render FileUploader component in mobile mode', () => { - isMobile.mockReturnValue(true); - isDesktop.mockReturnValue(false); + (isMobile as jest.Mock).mockReturnValue(true); + (isDesktop as jest.Mock).mockReturnValue(false); render(); expect(screen.getByText(click_msg)).toBeInTheDocument(); }); @@ -128,12 +124,14 @@ describe('', () => { it('upload function should return 0 if document is not selected', () => { render(); - const uploadFn = props.ref.current.upload(); + const uploadFn = ( + props?.ref as React.RefObject Promise }> + ).current?.upload(); expect(uploadFn).toBe(0); }); it('upload methods should reject if readFile returns empty array ', async () => { - readFiles.mockResolvedValue([]); + (readFiles as jest.Mock).mockResolvedValue([]); render(); const blob = new Blob(['sample_data']); @@ -145,7 +143,9 @@ describe('', () => { expect(screen.getByText(/hello\.pdf/i)).toBeInTheDocument(); expect(input?.files?.[0]).toBe(file); }); - props.ref.current.upload(); + ( + props?.ref as React.RefObject Promise }> + ).current?.upload(); expect(compressImageFiles).toBeCalled(); expect(props.onFileDrop).toBeCalled(); }); diff --git a/packages/account/src/Components/file-uploader-container/file-uploader-container.tsx b/packages/account/src/Components/file-uploader-container/file-uploader-container.tsx index de6375f07109..ddc023442493 100644 --- a/packages/account/src/Components/file-uploader-container/file-uploader-container.tsx +++ b/packages/account/src/Components/file-uploader-container/file-uploader-container.tsx @@ -1,59 +1,29 @@ import React from 'react'; -import classNames from 'classnames'; -import { Icon, Text } from '@deriv/components'; -import { PlatformContext, isDesktop, WS, TSettings } from '@deriv/shared'; -import { Localize, localize } from '@deriv/translations'; +import { Text } from '@deriv/components'; +import { isMobile, WS } from '@deriv/shared'; +import type { TSettings } from '@deriv/shared/src/utils/files/file-uploader-utils'; +import { Localize } from '@deriv/translations'; import FileUploader from './file-uploader'; -import { TFile, TPlatformContext } from 'Types'; +import { TFile } from '../../Types'; -export type TFileUploaderContainer = { - is_description_enabled?: boolean; - getSocket: () => WebSocket; +type TFileUploaderContainer = { + getSocket?: () => WebSocket; onFileDrop: (file: TFile | undefined) => void; - onRef: (ref: React.RefObject | undefined) => void; - settings: TSettings; -}; - -const FileProperties = () => { - const properties = [ - { name: 'size', icon: 'IcPoaFileEightMb', text: localize('Less than 8MB') }, - { name: 'format', icon: 'IcPoaFileFormat', text: localize('JPEG JPG PNG PDF GIF') }, - { name: 'time', icon: 'IcPoaFileTime', text: localize('1 - 6 months old') }, - { name: 'clear', icon: 'IcPoaFileClear', text: localize('A clear colour photo or scanned image') }, - { - name: 'with-address', - icon: 'IcPoaFileWithAddress', - text: localize('Issued under your name with your current address'), - }, - ]; - return ( -
- {properties.map(item => ( -
-
- - - {item.text} - -
-
- ))} -
- ); + onRef: (ref: React.RefObject void }> | undefined) => void; + settings?: Partial; + files_description: React.ReactNode; + examples: React.ReactNode; }; const FileUploaderContainer = ({ - is_description_enabled = true, + examples, + files_description, getSocket, onFileDrop, onRef, settings, }: TFileUploaderContainer) => { - const { is_appstore } = React.useContext>(PlatformContext); - const ref = React.useRef(); + const ref = React.useRef(null); const getSocketFunc = getSocket ?? WS.getSocket; @@ -63,77 +33,25 @@ const FileUploaderContainer = ({ } return () => onRef(undefined); }, [onRef, ref]); - if (is_appstore && isDesktop()) { - return ( -
-
- -
- -
-
-
- ); - } + return ( -
- {is_description_enabled && ( -
    -
  • - {is_appstore ? ( - - ) : ( - - )} -
    - -
    -
  • -
  • - -
    - -
    -
  • -
  • - {is_appstore ? ( - - ) : ( - - )} -
    - -
    -
  • -
  • - {is_appstore ? ( - - ) : ( - - )} -
    - -
    -
  • -
  • - {is_appstore ? ( - - ) : ( - - )} -
    - -
    -
  • -
- )} -
+
+ {files_description} + + + +
+
+ + + + + + +
+ {examples}
); }; diff --git a/packages/account/src/Components/file-uploader-container/file-uploader.tsx b/packages/account/src/Components/file-uploader-container/file-uploader.tsx index 4efefe6d25b1..db425b312c67 100644 --- a/packages/account/src/Components/file-uploader-container/file-uploader.tsx +++ b/packages/account/src/Components/file-uploader-container/file-uploader.tsx @@ -1,8 +1,11 @@ +//TODO all file upload process has to be checked and refactored with TS. skipping checks for passing CFD build +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck import React from 'react'; import classNames from 'classnames'; import DocumentUploader from '@binary-com/binary-document-uploader'; -import { FileDropzone, Icon, useStateCallback } from '@deriv/components'; -import { localize } from '@deriv/translations'; +import { FileDropzone, Icon, Text, useStateCallback } from '@deriv/components'; +import { Localize, localize } from '@deriv/translations'; import { isMobile, compressImageFiles, @@ -21,9 +24,18 @@ type TFileObject = { const UploadMessage = () => { return ( - +
- {isMobile() ? localize('Click here to upload') : localize('Drop file or click here to upload')} + + {isMobile() ? ( + + ) : ( + + )} + + + +
); @@ -35,7 +47,7 @@ const fileReadErrorMessage = (filename: string) => { const FileUploader = React.forwardRef< HTMLElement, - { onFileDrop: (file: TFile | undefined) => void; getSocket: () => WebSocket; settings: TSettings } + { onFileDrop: (file: TFile | undefined) => void; getSocket: () => WebSocket; settings?: Partial } >(({ onFileDrop, getSocket, settings = {} }, ref) => { const [document_file, setDocumentFile] = useStateCallback({ files: [], error_message: null }); @@ -121,11 +133,11 @@ const FileUploader = React.forwardRef< value={document_file.files} /> {(document_file.files.length > 0 || !!document_file.error_message) && ( -
+
) => { - const { is_appstore } = React.useContext(PlatformContext); - if (has_side_note) { return ( -
+
{typeof side_note === 'string' ? ( diff --git a/packages/account/src/Components/form-body/form-body.tsx b/packages/account/src/Components/form-body/form-body.tsx index a216f6f7ef3a..6806f15b33d8 100644 --- a/packages/account/src/Components/form-body/form-body.tsx +++ b/packages/account/src/Components/form-body/form-body.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ScrollbarsContainer } from 'Components/scrollbars-container/scrollbars-container'; +import { ScrollbarsContainer } from '../scrollbars-container/scrollbars-container'; import { Div100vhContainer, DesktopWrapper, MobileWrapper } from '@deriv/components'; type TFormBody = { diff --git a/packages/account/src/Components/form-footer/form-footer.tsx b/packages/account/src/Components/form-footer/form-footer.tsx index cd26de539eea..1e30d8fc801e 100644 --- a/packages/account/src/Components/form-footer/form-footer.tsx +++ b/packages/account/src/Components/form-footer/form-footer.tsx @@ -1,24 +1,14 @@ import React from 'react'; -import { PlatformContext } from '@deriv/shared'; import classNames from 'classnames'; export type TFormFooter = { className?: string; }; -const FormFooter = ({ children, className }: React.PropsWithChildren) => { - const { is_appstore } = React.useContext(PlatformContext); - - return ( -
- {children} -
- ); -}; +const FormFooter = ({ children, className }: React.PropsWithChildren) => ( +
+ {children} +
+); export default FormFooter; diff --git a/packages/account/src/Components/form-sub-header/form-sub-header.tsx b/packages/account/src/Components/form-sub-header/form-sub-header.tsx index 747613640a3a..935e799b1746 100644 --- a/packages/account/src/Components/form-sub-header/form-sub-header.tsx +++ b/packages/account/src/Components/form-sub-header/form-sub-header.tsx @@ -6,10 +6,12 @@ export type TFormSubHeader = { description?: string; subtitle?: string; title: string; + title_text_size?: string; }; -export const FormSubHeader = ({ description, subtitle, title }: TFormSubHeader) => { +export const FormSubHeader = ({ description, subtitle, title, title_text_size = 'xs' }: TFormSubHeader) => { const title_as_class = title.replace(/\s+/g, '-').toLowerCase(); + return (
- + {title} {subtitle && ( diff --git a/packages/account/src/Components/forms/form-fields.jsx b/packages/account/src/Components/forms/form-fields.jsx index 00f0f728df19..d7b7fe337747 100644 --- a/packages/account/src/Components/forms/form-fields.jsx +++ b/packages/account/src/Components/forms/form-fields.jsx @@ -21,12 +21,11 @@ export const DateOfBirthField = ({ name, portal_id, ...rest }) => ( ); -export const FormInputField = ({ name, optional = false, warn, ...rest }) => ( +export const FormInputField = ({ name, warn, ...rest }) => ( {({ field, form: { errors, touched } }) => ( { const { @@ -30,7 +30,6 @@ const PersonalDetailsForm = props => { is_svg, is_qualified_for_idv, should_hide_helper_image, - is_appstore, editable_fields = [], has_real_account, residence_list, @@ -39,9 +38,11 @@ const PersonalDetailsForm = props => { closeRealAccountSignup, salutation_list, is_rendered_for_onfido, + is_qualified_for_poa, should_close_tooltip, setShouldCloseTooltip, class_name, + states_list, } = props; const autocomplete_value = 'none'; const PoiNameDobExampleIcon = PoiNameDobExample; @@ -60,10 +61,9 @@ const PersonalDetailsForm = props => { const getNameAndDobLabels = () => { const is_asterisk_needed = is_svg || is_mf || is_rendered_for_onfido || is_qualified_for_idv; - const first_name_label = is_appstore || is_asterisk_needed ? localize('First name*') : localize('First name'); - const last_name_text = is_asterisk_needed ? localize('Last name*') : localize('Last name'); - const last_name_label = is_appstore ? localize('Family name*') : last_name_text; - const dob_label = is_appstore || is_asterisk_needed ? localize('Date of birth*') : localize('Date of birth'); + 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'); return { first_name_label, @@ -101,6 +101,10 @@ const PersonalDetailsForm = props => { /> ); + const poa_clarification_message = ( + + ); + // need to put this check related to DIEL clients const is_svg_only = is_svg && !is_mf; @@ -117,6 +121,13 @@ const PersonalDetailsForm = props => { font_size={isMobile() ? 'xxxs' : 'xs'} /> )} + {is_qualified_for_poa && ( + + )} } @@ -145,7 +156,7 @@ const PersonalDetailsForm = props => {
)} - {!is_qualified_for_idv && !is_appstore && !is_rendered_for_onfido && ( + {!is_qualified_for_idv && !is_rendered_for_onfido && !is_qualified_for_poa && ( @@ -176,7 +187,7 @@ const PersonalDetailsForm = props => { {'first_name' in values && ( { {'last_name' in values && ( { data-testid='last_name' /> )} - {!is_appstore && !is_qualified_for_idv && !is_rendered_for_onfido && ( + {!is_qualified_for_idv && !is_rendered_for_onfido && !is_qualified_for_poa && ( )} {'date_of_birth' in values && ( { (values?.date_of_birth && has_real_account) } placeholder={localize('01-07-1999')} - portal_id={is_appstore ? '' : 'modal_root'} + portal_id='modal_root' data_testid='date_of_birth' /> )} + {'address_line_1' in values && ( + + )} + {'address_line_2' in values && ( + + )} + {'address_city' in values && ( + + )} + {'address_state' in values && + (states_list?.length ? ( + + + + {({ field }) => ( + + setFieldValue('address_state', value ? text : '', true) + } + /> + )} + + + + setFieldValue('address_state', e.target.value, true)} + /> + + + ) : ( + + ))} + {'address_postcode' in values && ( + + )} {!is_svg_only && 'place_of_birth' in values && ( { label={is_mf ? localize('Citizenship*') : localize('Citizenship')} list_items={residence_list} value={values.citizen} - use_text={true} + use_text error={touched.citizen && errors.citizen} onChange={e => { handleChange(e); @@ -513,7 +636,7 @@ const PlaceOfBirthField = ({ handleChange, setFieldValue, disabled, residence_li label={required ? localize('Place of birth*') : localize('Place of birth')} list_items={residence_list} value={field.value} - use_text={true} + use_text error={meta.touched && meta.error} onChange={e => { handleChange(e); @@ -565,7 +688,7 @@ const TaxResidenceField = ({ label={required ? localize('Tax residence*') : localize('Tax residence')} list_items={residence_list} value={field.value} - use_text={true} + use_text error={meta.touched && meta.error} onChange={e => { field.onChange(e); diff --git a/packages/account/src/Components/icon-with-message/icon-with-message.tsx b/packages/account/src/Components/icon-with-message/icon-with-message.tsx index ce3ca078034d..e9d8a3c04eec 100644 --- a/packages/account/src/Components/icon-with-message/icon-with-message.tsx +++ b/packages/account/src/Components/icon-with-message/icon-with-message.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import classNames from 'classnames'; import { Icon, Text, Button } from '@deriv/components'; -import { isMobile, PlatformContext } from '@deriv/shared'; -import { localize } from '@deriv/translations'; +import { isMobile } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; +import { localize } from '@deriv/translations'; type TIconWithMessage = { icon: string; @@ -15,10 +14,9 @@ const IconWithMessage = observer(({ has_button, icon, message }: TIconWithMessag const { client, ui } = useStore(); const { has_any_real_account: has_real_account } = client; const { toggleAccountsDialog, toggleShouldShowRealAccountsList } = ui; - const { is_appstore } = React.useContext(PlatformContext); return ( -
+
{ it('should show proper icon', () => { get_leave_confirm_states(); render(); - expect(screen.getByTestId('unsaved_changes_icon')).toBeInTheDocument(); + expect(screen.getByTestId('dt_unsaved_changes_icon')).toBeInTheDocument(); expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); expect( screen.getByText('You have unsaved changes. Are you sure you want to discard changes and leave this page?') diff --git a/packages/account/src/Components/leave-confirm/leave-confirm.tsx b/packages/account/src/Components/leave-confirm/leave-confirm.tsx index fb60e32afad5..03ef8b01da24 100644 --- a/packages/account/src/Components/leave-confirm/leave-confirm.tsx +++ b/packages/account/src/Components/leave-confirm/leave-confirm.tsx @@ -1,36 +1,28 @@ import React from 'react'; -import { useHistory, withRouter } from 'react-router-dom'; +import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom'; import { FormikConsumer } from 'formik'; import { Button, Icon, Modal } from '@deriv/components'; -import { isMobile, PlatformContext } from '@deriv/shared'; +import { isMobile } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import IconMessageContent from 'Components/icon-message-content'; +import IconMessageContent from '../icon-message-content'; type TLeaveConfirmMessage = { back: () => void; leave: () => void; }; -type TTransitionBlocker = { +type TTransitionBlocker = RouteComponentProps & { dirty: boolean; - onDirty: (prop: boolean) => void; + onDirty?: (prop: boolean) => void; }; const LeaveConfirmMessage = ({ back, leave }: TLeaveConfirmMessage) => { - const { is_appstore } = React.useContext(PlatformContext); - return ( - } + icon={} >
- ) : null; - - if (is_loading) return ; - if ( - !allow_document_upload || - (!is_age_verified && !allow_poa_resubmission && document_status === 'none' && is_mx_mlt) - ) - return ; - if (has_submitted_poa && !poa_address_mismatch) - return ; - if ( - resubmit_poa || - allow_poa_resubmission || - (has_restricted_mt5_account && ['expired', 'rejected', 'suspected'].includes(document_status)) || - poa_address_mismatch - ) { - return onSubmit({ needs_poi })} />; - } - - switch (document_status) { - case PoaStatusCodes.none: - return onSubmit({ needs_poi })} />; - case PoaStatusCodes.pending: - return ; - case PoaStatusCodes.verified: - return ; - case PoaStatusCodes.expired: - return ; - case PoaStatusCodes.rejected: - case PoaStatusCodes.suspected: - return ; - default: - return null; - } -}; - -ProofOfAddressContainer.propTypes = { - is_mx_mlt: PropTypes.bool, - has_restricted_mt5_account: PropTypes.bool, - is_switching: PropTypes.bool, - refreshNotifications: PropTypes.func, -}; - -export default ProofOfAddressContainer; diff --git a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-container.tsx b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-container.tsx new file mode 100644 index 000000000000..be2b2ed2f113 --- /dev/null +++ b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-container.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { AccountStatusResponse, GetAccountStatus } from '@deriv/api-types'; +import { Button, Loading } from '@deriv/components'; +import { WS, getPlatformRedirect, platforms } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { Localize } from '@deriv/translations'; +import Expired from '../../../Components/poa/status/expired'; +import NeedsReview from '../../../Components/poa/status/needs-review'; +import NotRequired from '../../../Components/poa/status/not-required'; +import PoaStatusCodes from '../../../Components/poa/status/status-codes'; +import ProofOfAddressForm from './proof-of-address-form'; +import Submitted from '../../../Components/poa/status/submitted'; +import Unverified from '../../../Components/poa/status/unverified'; +import Verified from '../../../Components/poa/status/verified'; +import { populateVerificationStatus } from '../Helpers/verification.js'; + +type TAuthenticationStatus = Record< + | 'allow_document_upload' + | 'allow_poi_resubmission' + | 'allow_poa_resubmission' + | 'is_age_verified' + | 'has_poi' + | 'has_submitted_poa' + | 'needs_poa' + | 'needs_poi' + | 'poa_address_mismatch' + | 'resubmit_poa', + boolean +> & { document_status?: DeepRequired['authentication']['document']['status'] }; + +const ProofOfAddressContainer = observer(() => { + const [is_loading, setIsLoading] = React.useState(true); + const [authentication_status, setAuthenticationStatus] = React.useState({ + allow_document_upload: false, + allow_poi_resubmission: false, + allow_poa_resubmission: false, + needs_poi: false, + needs_poa: false, + has_poi: false, + resubmit_poa: false, + has_submitted_poa: false, + document_status: undefined, + is_age_verified: false, + poa_address_mismatch: false, + }); + + const { client, notifications, common } = useStore(); + const { app_routing_history } = common; + const { landing_company_shortcode, has_restricted_mt5_account, is_switching } = client; + const { refreshNotifications } = notifications; + + const is_mx_mlt = landing_company_shortcode === 'iom' || landing_company_shortcode === 'malta'; + + React.useEffect(() => { + if (!is_switching) { + WS.authorized.getAccountStatus().then((response: AccountStatusResponse) => { + const { get_account_status } = response; + if (get_account_status) { + const { + allow_document_upload, + allow_poa_resubmission, + document_status, + has_submitted_poa, + is_age_verified, + needs_poa, + needs_poi, + poa_address_mismatch, + } = populateVerificationStatus(get_account_status); + + setAuthenticationStatus(authentication_status => ({ + ...authentication_status, + allow_document_upload, + allow_poa_resubmission, + document_status, + has_submitted_poa, + is_age_verified, + needs_poa, + needs_poi, + poa_address_mismatch, + })); + setIsLoading(false); + refreshNotifications(); + } + }); + } + }, [is_switching, refreshNotifications]); + + const handleResubmit = () => { + setAuthenticationStatus(authentication_status => ({ ...authentication_status, ...{ resubmit_poa: true } })); + }; + + const onSubmit = (needs_poi: boolean) => { + setAuthenticationStatus(authentication_status => ({ + ...authentication_status, + ...{ has_submitted_poa: true, needs_poi }, + })); + }; + + const { + allow_document_upload, + allow_poa_resubmission, + document_status, + needs_poi, + resubmit_poa, + has_submitted_poa, + is_age_verified, + poa_address_mismatch, + } = authentication_status; + + const from_platform = getPlatformRedirect(app_routing_history); + + const should_show_redirect_btn = Object.keys(platforms).includes(from_platform?.ref ?? ''); + + const redirect_button = should_show_redirect_btn && ( + + ); + + if (is_loading) return ; + if ( + !allow_document_upload || + (!is_age_verified && !allow_poa_resubmission && document_status === 'none' && is_mx_mlt) + ) + return ; + if (has_submitted_poa && !poa_address_mismatch) + return ; + if ( + resubmit_poa || + allow_poa_resubmission || + (has_restricted_mt5_account && + document_status && + ['expired', 'rejected', 'suspected'].includes(document_status)) || + poa_address_mismatch + ) { + return ; + } + + switch (document_status) { + case PoaStatusCodes.none: + return ; + case PoaStatusCodes.pending: + return ; + case PoaStatusCodes.verified: + return ; + case PoaStatusCodes.expired: + return ; + case PoaStatusCodes.rejected: + case PoaStatusCodes.suspected: + return ; + default: + return null; + } +}); + +export default ProofOfAddressContainer; diff --git a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx deleted file mode 100644 index b47e1d6190e6..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx +++ /dev/null @@ -1,464 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Autocomplete, - Loading, - Button, - Input, - DesktopWrapper, - MobileWrapper, - SelectNative, - FormSubmitErrorMessage, - Text, - useStateCallback, -} from '@deriv/components'; -import { Formik, Field } from 'formik'; -import { localize, Localize } from '@deriv/translations'; -import { - isMobile, - removeEmptyPropertiesFromObject, - validAddress, - validPostCode, - validLetterSymbol, - validLength, - getLocation, - WS, -} from '@deriv/shared'; -import FormFooter from 'Components/form-footer'; -import FormBody from 'Components/form-body'; -import FormBodySection from 'Components/form-body-section'; -import FormSubHeader from 'Components/form-sub-header'; -import LoadErrorMessage from 'Components/load-error-message'; -import LeaveConfirm from 'Components/leave-confirm'; -import FileUploaderContainer from 'Components/file-uploader-container'; -import { observer, useStore } from '@deriv/stores'; - -const validate = (errors, values) => (fn, arr, err_msg) => { - arr.forEach(field => { - const value = values[field]; - if (!fn(value) && !errors[field] && err_msg !== true) errors[field] = err_msg; - }); -}; - -let file_uploader_ref = null; - -const UploaderSideNote = () => ( -
- - - - - - -
-); - -const ProofOfAddressForm = observer(({ is_resubmit, onSubmit }) => { - const { client, notifications } = useStore(); - const { account_settings, fetchResidenceList, fetchStatesList, is_eu, states_list } = client; - const { - addNotificationMessageByKey: addNotificationByKey, - removeNotificationMessage, - removeNotificationByKey, - } = notifications; - const [document_file, setDocumentFile] = React.useState({ files: [], error_message: null }); - const [is_loading, setIsLoading] = React.useState(true); - const [form_values, setFormValues] = useStateCallback({}); - const [api_initial_load_error, setAPIInitialLoadError] = React.useState(null); - const [form_state, setFormState] = useStateCallback({ should_show_form: true }); - - React.useEffect(() => { - fetchResidenceList().then(() => { - Promise.all([fetchStatesList(), WS.wait('get_settings')]).then(() => { - const { citizen, tax_identification_number, tax_residence } = account_settings; - setFormValues( - { - ...account_settings, - ...(is_eu ? { citizen, tax_identification_number, tax_residence } : {}), - }, - () => setIsLoading(false) - ); - }); - }); - }, [account_settings, fetchResidenceList, fetchStatesList, is_eu, setFormValues]); - - const validateFields = values => { - Object.entries(values).forEach(([key, value]) => (values[key] = value.trim())); - - setFormState({ ...form_state, ...{ should_allow_submit: false } }); - const errors = {}; - const validateValues = validate(errors, values); - - const required_fields = ['address_line_1', 'address_city']; - validateValues(val => val, required_fields, localize('This field is required')); - - const address_line_1_validation_result = validAddress(values.address_line_1, { is_required: true }); - if (!address_line_1_validation_result.is_ok) { - errors.address_line_1 = address_line_1_validation_result.message; - } - const address_line_2_validation_result = validAddress(values.address_line_2); - if (!address_line_2_validation_result.is_ok) { - errors.address_line_2 = address_line_2_validation_result.message; - } - - const validation_letter_symbol_message = localize( - 'Only letters, space, hyphen, period, and apostrophe are allowed.' - ); - - if (values.address_city && !validLetterSymbol(values.address_city)) { - errors.address_city = validation_letter_symbol_message; - } - - // only add state/province validation for countries that don't have states list fetched from API - if (values.address_state && !validLetterSymbol(values.address_state) && states_list?.length < 1) { - errors.address_state = validation_letter_symbol_message; - } - - if (values.address_postcode) { - if (!validLength(values.address_postcode, { min: 0, max: 20 })) { - errors.address_postcode = localize('Please enter a {{field_name}} under {{max_number}} characters.', { - field_name: localize('Postal/ZIP code'), - max_number: 20, - interpolation: { escapeValue: false }, - }); - } else if (!validPostCode(values.address_postcode)) { - errors.address_postcode = localize('Only letters, numbers, space, and hyphen are allowed.'); - } - } - - return errors; - }; - - const showForm = bool => { - setFormState({ ...form_state, ...{ should_show_form: bool } }); - }; - - // Settings update is handled here - const onSubmitValues = (values, { setStatus, setSubmitting }) => { - setStatus({ msg: '' }); - setFormState({ ...form_state, ...{ is_btn_loading: true } }); - let settings_values = { ...values }; - - if (values.address_state && states_list.length) { - settings_values.address_state = getLocation(states_list, values.address_state, 'value') || ''; - } - - if (is_eu) { - const { citizen, tax_residence, tax_identification_number } = form_values; - settings_values = removeEmptyPropertiesFromObject({ - ...settings_values, - citizen, - tax_identification_number, - tax_residence, - }); - } - - WS.setSettings(settings_values).then(data => { - if (data.error) { - setStatus({ msg: data.error.message }); - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - setSubmitting(false); - } else { - // force request to update settings cache since settings have been updated - WS.authorized.storage - .getSettings() - .then(({ error, get_settings }) => { - if (error) { - setAPIInitialLoadError(error.message); - setSubmitting(false); - return; - } - const { address_line_1, address_line_2, address_city, address_state, address_postcode } = - get_settings; - - setFormValues( - { - address_line_1, - address_line_2, - address_city, - address_state, - address_postcode, - }, - () => setIsLoading(false) - ); - }) - .then(() => { - // upload files - file_uploader_ref?.current - .upload() - .then(api_response => { - if (api_response.warning) { - setStatus({ msg: api_response.message }); - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - } else { - WS.authorized.storage.getAccountStatus().then(({ error, get_account_status }) => { - if (error) { - setAPIInitialLoadError(error.message); - setSubmitting(false); - return; - } - setFormState( - { ...form_state, ...{ is_submit_success: true, is_btn_loading: false } }, - () => { - const { identity, needs_verification } = - get_account_status.authentication; - const has_poi = !(identity && identity.status === 'none'); - // TODO: clean all of this up by simplifying the manually toggled notifications functions - const needs_poi = - needs_verification.length && - needs_verification.includes('identity'); - onSubmit({ has_poi }); - removeNotificationMessage({ key: 'authenticate' }); - removeNotificationByKey({ key: 'authenticate' }); - removeNotificationMessage({ key: 'needs_poa' }); - removeNotificationByKey({ key: 'needs_poa' }); - removeNotificationMessage({ key: 'poa_expired' }); - removeNotificationByKey({ key: 'poa_expired' }); - if (needs_poi) { - addNotificationByKey('needs_poi'); - } - } - ); - }); - } - }) - .catch(error => { - setStatus({ msg: error.message }); - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - }) - .then(() => { - setSubmitting(false); - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - }); - }); - } - }); - }; - - const { address_line_1, address_line_2, address_city, address_state, address_postcode } = form_values; - - const form_initial_values = { - address_line_1, - address_line_2, - address_city, - address_state, - address_postcode, - }; - - if (api_initial_load_error) { - return ; - } - if (is_loading) return ; - const mobile_scroll_offset = status && status.msg ? '200px' : '154px'; - - if (form_initial_values.address_state) { - form_initial_values.address_state = states_list.length - ? getLocation(states_list, form_initial_values.address_state, 'text') - : form_initial_values.address_state; - } else { - form_initial_values.address_state = ''; - } - - return ( - - {({ - values, - errors, - status, - touched, - handleChange, - handleBlur, - handleSubmit, - isSubmitting, - setFieldValue, - }) => ( - <> - - {form_state.should_show_form && ( - - - {is_resubmit && ( - - {localize( - 'We were unable to verify your address with the details you provided. Please check and resubmit or choose a different document type.' - )} - - )} - - -
-
-
- -
-
- -
-
- -
-
- {states_list.length ? ( - - - - {({ field }) => ( - - setFieldValue( - 'address_state', - value ? text : '', - true - ) - } - /> - )} - - - - - setFieldValue('address_state', e.target.value, true) - } - /> - - - ) : ( - - )} -
-
- -
-
-
-
- - - }> - (file_uploader_ref = ref)} - onFileDrop={df => - setDocumentFile({ files: df.files, error_message: df.error_message }) - } - getSocket={WS.getSocket} - /> - -
- - {status && status.msg && } -