diff --git a/packages/account-v2/src/App.tsx b/packages/account-v2/src/App.tsx index 750ad5055014..3c98ee5e0d2c 100644 --- a/packages/account-v2/src/App.tsx +++ b/packages/account-v2/src/App.tsx @@ -2,34 +2,22 @@ /* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-empty-function */ import React from 'react'; -import { Formik } from 'formik'; import { APIProvider } from '@deriv/api'; import { BreakpointProvider } from '@deriv/quill-design'; -import { SelfieDocumentUpload } from './containers/SelfieDocumentUpload'; -import { DOCUMENT_LIST, INITIAL_VALUES, SELECTED_COUNTRY } from './mocks/idv-form.mock'; -import { IDVForm } from './modules/IDVForm'; -import { getIDVFormValidationSchema } from './modules/IDVForm/utils'; +import { ManualUploadContainer } from './pages/ManualFormContainer/manual-form-container'; import RouteLinks from './router/components/route-links/route-links'; import './index.scss'; const App: React.FC = () => { - // TODO - Remove this once the IDV form is moved out - const getValidationSchema = getIDVFormValidationSchema(DOCUMENT_LIST); - return (
Account V2
- {/* TODO - Remove this ManualForm later, only rendered here for testing purposes. */} - {/* {}} selectedDocument='driving_licence' /> */} - {/* [TODO]:Mock - Remove Mock values */} - {}} validationSchema={getValidationSchema}> - {/* */} - - + console.log('Called with', val)} + /> - {/* [TODO]:Mock - Remove Mock values */} - {/* console.log(val)} /> */}
); diff --git a/packages/account-v2/src/components/Dropzone/Dropzone.scss b/packages/account-v2/src/components/Dropzone/Dropzone.scss index 8f3092a9da83..5c5e87a7e7b0 100644 --- a/packages/account-v2/src/components/Dropzone/Dropzone.scss +++ b/packages/account-v2/src/components/Dropzone/Dropzone.scss @@ -23,16 +23,6 @@ border: 0.1rem solid var(--status-success, #4bb4b3); } - &__content { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 2.4rem; - } - &__thumb { position: relative; width: 100%; @@ -56,33 +46,33 @@ } } - &__placeholder { - display: flex; - flex-direction: column; - align-items: center; - gap: 1.4rem; - } + // &__placeholder { + // display: flex; + // flex-direction: column; + // align-items: center; + // gap: 1.4rem; + // } - &__placeholder-icon { - flex-shrink: 0; - } + // &__placeholder-icon { + // flex-shrink: 0; + // } - &__placeholder-text { - display: flex; - flex-direction: column; - align-items: center; - gap: 3.2rem; - } + // &__placeholder-text { + // display: flex; + // flex-direction: column; + // align-items: center; + // gap: 3.2rem; + // } - &__remove-file { - position: absolute; - top: 0.8rem; - right: 0.8rem; - border-radius: 50%; - background-color: var(--system-light-5-active-background, #d6dadb); + // &__remove-file { + // position: absolute; + // top: 0.8rem; + // right: 0.8rem; + // border-radius: 50%; + // background-color: var(--system-light-5-active-background, #d6dadb); - &:hover { - background-color: var(--system-light-5-hover-background, #e9ecef); - } - } + // &:hover { + // background-color: var(--system-light-5-hover-background, #e9ecef); + // } + // } } diff --git a/packages/account-v2/src/components/Dropzone/Dropzone.tsx b/packages/account-v2/src/components/Dropzone/Dropzone.tsx index 9213ab6b81f5..a5246ee77f94 100644 --- a/packages/account-v2/src/components/Dropzone/Dropzone.tsx +++ b/packages/account-v2/src/components/Dropzone/Dropzone.tsx @@ -112,11 +112,11 @@ const Dropzone: React.FC = ({ { 'wallets-dropzone--active': file } )} > -
+
{showHoverMessage && {hoverMessage}} {!file && ( -
-
{icon}
+
+
{icon}
{title && ( {title} @@ -126,7 +126,7 @@ const Dropzone: React.FC = ({ {description} {buttonText && ( -
+
@@ -150,7 +150,7 @@ const Dropzone: React.FC = ({ > {hasFrame && } } diff --git a/packages/account-v2/src/components/IconButton/icon-button.scss b/packages/account-v2/src/components/IconButton/icon-button.scss deleted file mode 100644 index 8a4a361cc35d..000000000000 --- a/packages/account-v2/src/components/IconButton/icon-button.scss +++ /dev/null @@ -1,94 +0,0 @@ -/* stylelint-disable color-no-hex */ - -$color-map: ( - 'primary': ( - backgroundcolor: #ff444f, - hover: #eb3e48, - ), - 'white': ( - backgroundcolor: #fff, - hover: #d6dadb, - ), - 'black': ( - backgroundcolor: #0e0e0e, - hover: #323738, - ), - 'transparent': ( - backgroundcolor: transparent, - hover: #d6dadb, - ), -); - -$size-map: ( - sm: ( - padding: 0.4rem, - height: 2.4rem, - ), - md: ( - padding: 0.8rem, - height: 3.2rem, - ), - lg: ( - padding: 1.2rem, - height: 4rem, - ), -); - -$border-radius-map: ( - sm: 0.4rem, - md: 6.4rem, - lg: 50%, -); - -.icon-button { - // display: grid; - // place-content: center; - // border: none; - - // &:hover { - // cursor: pointer; - // } - - // &:disabled { - // display: none; - // } - - &__border-radius { - &--round { - border-radius: 50%; - } - - &--default { - border-radius: 0.4rem; - } - } - - @each $color, $values in $color-map { - &__color--#{$color} { - background-color: map-get($values, backgroundcolor); - &:hover { - background-color: map-get($values, hover); - } - } - } - - @each $size, $values in $size-map { - &__size--#{$size} { - padding: map-get($values, padding); - height: map-get($values, height); - } - } - - @each $size, $values in $border-radius-map { - &__border-radius--#{$size} { - border-radius: ($values); - } - } - - // &__icon { - // display: grid; - // place-content: center; - // width: 1.6rem; - // height: 1.6rem; - // } -} diff --git a/packages/account-v2/src/components/IconButton/icon-button.tsx b/packages/account-v2/src/components/IconButton/icon-button.tsx index 6be1fc1c13a4..94fc57bf4e48 100644 --- a/packages/account-v2/src/components/IconButton/icon-button.tsx +++ b/packages/account-v2/src/components/IconButton/icon-button.tsx @@ -2,7 +2,6 @@ import React, { ButtonHTMLAttributes, ComponentProps, forwardRef, Ref } from 're import clsx from 'clsx'; import { TGenericSizes } from '../types'; import { iconButtonVariants } from './icon-button.classnames'; -import './icon-button.scss'; interface IconButtonProps extends ButtonHTMLAttributes { color?: 'black' | 'primary' | 'transparent' | 'white'; diff --git a/packages/account-v2/src/components/base/WalletDatePicker/WalletDatePicker.tsx b/packages/account-v2/src/components/base/WalletDatePicker/WalletDatePicker.tsx index 10375f788456..1ea359cc0e50 100644 --- a/packages/account-v2/src/components/base/WalletDatePicker/WalletDatePicker.tsx +++ b/packages/account-v2/src/components/base/WalletDatePicker/WalletDatePicker.tsx @@ -60,6 +60,7 @@ const WalletDatePicker = ({
, - fileUploadText: 'Upload the front of your driving licence.', + error: 'Front side of driving licence is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.FRONT, + text: 'Upload the front of your driving licence.', }, { - fileUploadIcon: , - fileUploadText: 'Upload the back of your driving licence.', + error: 'Back side of driving licence is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.BACK, + text: 'Upload the back of your driving licence.', }, ], uploadSectionHeader: 'Next, upload the front and back of your driving licence.', @@ -56,14 +67,18 @@ export const MANUAL_DOCUMENT_TYPES_DATA = Object.freeze({ }, }, inputSectionHeader: 'First, enter your Identity card number and the expiry date.', - upload: [ + uploads: [ { - fileUploadIcon: , - fileUploadText: 'Upload the front of your identity card.', + error: 'Front side of identity card is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.FRONT, + text: 'Upload the front of your identity card.', }, { - fileUploadIcon: , - fileUploadText: 'Upload the back of your identity card.', + error: 'Back side of identity card is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.BACK, + text: 'Upload the back of your identity card.', }, ], uploadSectionHeader: 'Next, upload the front and back of your identity card.', @@ -77,14 +92,18 @@ export const MANUAL_DOCUMENT_TYPES_DATA = Object.freeze({ }, }, inputSectionHeader: 'First, enter your NIMC slip number and the expiry date.', - upload: [ + uploads: [ { - fileUploadIcon: , - fileUploadText: 'Upload your NIMC slip.', + error: 'Front side of NIMC slip is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.FRONT, + text: 'Upload your NIMC slip.', }, { - fileUploadIcon: , - fileUploadText: 'Upload your proof of age: birth certificate or age declaration document.', + error: 'Back side of NIMC slip is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.PHOTO, + text: 'Upload your proof of age: birth certificate or age declaration document.', }, ], uploadSectionHeader: 'Next, upload the page of your NIMC slip that contains your photo.', @@ -98,10 +117,12 @@ export const MANUAL_DOCUMENT_TYPES_DATA = Object.freeze({ }, }, inputSectionHeader: 'First, enter your Passport number and the expiry date.', - upload: [ + uploads: [ { - fileUploadIcon: , - fileUploadText: 'Upload the page of your passport that contains your photo.', + error: 'Front side of passport is required.', + icon: , + pageType: MANUAL_FORM_PAGE_TYPES.FRONT, + text: 'Upload the page of your passport that contains your photo.', }, ], uploadSectionHeader: 'Next, upload the page of your passport that contains your photo.', diff --git a/packages/account-v2/src/containers/ManualForm/__tests__/manualForm.spec.tsx b/packages/account-v2/src/containers/ManualForm/__tests__/manualForm.spec.tsx index 438e16dad4dc..04c233574998 100644 --- a/packages/account-v2/src/containers/ManualForm/__tests__/manualForm.spec.tsx +++ b/packages/account-v2/src/containers/ManualForm/__tests__/manualForm.spec.tsx @@ -13,6 +13,7 @@ describe('ManualForm', () => { selectedDocument?: React.ComponentProps['selectedDocument']; }) => { const mockProps: React.ComponentProps = { + onCancel: jest.fn(), onSubmit: jest.fn(), selectedDocument: selectedDocument ?? 'driving_licence', }; diff --git a/packages/account-v2/src/containers/ManualForm/manualForm.tsx b/packages/account-v2/src/containers/ManualForm/manualForm.tsx index 735bc33dd5a3..61cfc868c965 100644 --- a/packages/account-v2/src/containers/ManualForm/manualForm.tsx +++ b/packages/account-v2/src/containers/ManualForm/manualForm.tsx @@ -2,39 +2,52 @@ import React from 'react'; import { Form, Formik } from 'formik'; import { Button } from '@deriv-com/ui'; import { MANUAL_FORM_INITIAL_VALUES, TManualDocumentTypes } from '../../constants/manualFormConstants'; +import { useManualForm } from '../../hooks'; import { getManualFormValidationSchema } from '../../utils/manualFormUtils'; import { ManualFormDocumentUpload } from './manualFormDocumentUpload'; import { ManualFormFooter } from './manualFormFooter'; import { ManualFormInputs } from './manualFormInputs'; type TManualFormProps = { + onCancel: () => void; onSubmit: (values: typeof MANUAL_FORM_INITIAL_VALUES) => void; selectedDocument: TManualDocumentTypes; - onCancel: () => void; }; -export const ManualForm = ({ onSubmit, selectedDocument, onCancel }: TManualFormProps) => ( -
+export const ManualForm = ({ onCancel, onSubmit, selectedDocument }: TManualFormProps) => { + const { isExpiryDateRequired } = useManualForm(); + + return ( getManualFormValidationSchema(selectedDocument)} + validationSchema={() => getManualFormValidationSchema(selectedDocument, isExpiryDateRequired)} > - {({ isValid }) => ( + {({ isSubmitting, isValid }) => (
-
- - - -
- - +
)}
-
-); + ); +}; diff --git a/packages/account-v2/src/containers/ManualForm/manualFormDocumentUpload.tsx b/packages/account-v2/src/containers/ManualForm/manualFormDocumentUpload.tsx index 60ec886fc0ab..08de109b2c0e 100644 --- a/packages/account-v2/src/containers/ManualForm/manualFormDocumentUpload.tsx +++ b/packages/account-v2/src/containers/ManualForm/manualFormDocumentUpload.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { WalletText } from '../../components/base/WalletText'; -import { Dropzone } from '../../components/Dropzone'; +import { Text } from '@deriv-com/ui'; +import FormDocumentUploadField from '../../components/FormFields/FormDocumentUploadField'; import { TManualDocumentTypes } from '../../constants/manualFormConstants'; import { getTitleForDocumentUpload, getUploadConfig } from '../../utils/manualFormUtils'; @@ -11,17 +11,18 @@ export const ManualFormDocumentUpload = ({ selectedDocument }: TManualFormDocume return (
- {getTitleForDocumentUpload(selectedDocument)} + {getTitleForDocumentUpload(selectedDocument)}
{uploadConfig.map(upload => ( -
- + console.log(file.name)} + name={upload.pageType} />
))} diff --git a/packages/account-v2/src/containers/ManualForm/manualFormFooter.tsx b/packages/account-v2/src/containers/ManualForm/manualFormFooter.tsx index 48879404c1c3..487b7a16d30a 100644 --- a/packages/account-v2/src/containers/ManualForm/manualFormFooter.tsx +++ b/packages/account-v2/src/containers/ManualForm/manualFormFooter.tsx @@ -1,9 +1,9 @@ import React from 'react'; +import { Text } from '@deriv-com/ui'; import IcPoiClearPhoto from '../../assets/manual-upload/ic-poi-clear-photo.svg'; import IcPoiDocExpiry from '../../assets/manual-upload/ic-poi-doc-expiry.svg'; import IcPoiFileFormat from '../../assets/manual-upload/ic-poi-file-format.svg'; import IcPoiFileSize from '../../assets/manual-upload/ic-poi-file-size.svg'; -import { WalletText } from '../../components/base/WalletText'; const FOOTER_ITEMS = [ { icon: , text: 'A clear colour photo or scanned image' }, @@ -13,13 +13,13 @@ const FOOTER_ITEMS = [ ]; export const ManualFormFooter = () => ( -
+
{FOOTER_ITEMS.map(footer => (
{footer.icon}
- + {footer.text} - +
))}
diff --git a/packages/account-v2/src/containers/ManualForm/manualFormInputs.tsx b/packages/account-v2/src/containers/ManualForm/manualFormInputs.tsx index 5a7073e7fe7a..5ec13f858725 100644 --- a/packages/account-v2/src/containers/ManualForm/manualFormInputs.tsx +++ b/packages/account-v2/src/containers/ManualForm/manualFormInputs.tsx @@ -1,8 +1,7 @@ import React, { Fragment } from 'react'; import { Field, FieldProps } from 'formik'; -import { Input } from '@deriv-com/ui'; +import { Input, Text } from '@deriv-com/ui'; import { WalletDatePicker } from '../../components/base/WalletDatePicker'; -import { WalletText } from '../../components/base/WalletText'; import { TManualDocumentTypes } from '../../constants/manualFormConstants'; import { useManualForm } from '../../hooks'; import { getFieldsConfig, getTitleForFormInputs } from '../../utils/manualFormUtils'; @@ -15,7 +14,7 @@ export const ManualFormInputs = ({ selectedDocument }: TManualFormInputsProps) = return ( - {getTitleForFormInputs(selectedDocument)} + {getTitleForFormInputs(selectedDocument)}
{({ field, meta }: FieldProps) => { @@ -25,6 +24,7 @@ export const ManualFormInputs = ({ selectedDocument }: TManualFormInputsProps) = { descriptionSize='2xs' fileFormats={['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'application/pdf']} hoverMessage='Upload your file here' - icon={} + icon={} maxSize={8388608} name='document' title='Drag and drop a file or click to browse your files.' diff --git a/packages/account-v2/src/containers/SelfieDocumentUpload/__tests__/selfie-document-upload.spec.tsx b/packages/account-v2/src/containers/SelfieDocumentUpload/__tests__/selfie-document-upload.spec.tsx new file mode 100644 index 000000000000..8f7f000ff7d8 --- /dev/null +++ b/packages/account-v2/src/containers/SelfieDocumentUpload/__tests__/selfie-document-upload.spec.tsx @@ -0,0 +1,69 @@ +import React, { ComponentProps } from 'react'; +import { useDevice } from '@deriv-com/ui'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { SelfieDocumentUpload } from '../selfie-document-upload'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: jest.fn().mockReturnValue({ isMobile: false }), +})); + +describe('SelfieDocumentUpload', () => { + const mockHandleCancel = jest.fn(); + const mockHandleSubmit = jest.fn(); + + const mockProps: ComponentProps = { + formData: {}, + handleCancel: mockHandleCancel, + handleSubmit: mockHandleSubmit, + }; + + it('should render component', () => { + render(); + + expect(screen.getAllByText(/Upload your selfie/i)).toHaveLength(2); + expect(screen.getAllByRole('button')).toHaveLength(3); + expect(screen.getByRole('button', { name: /Drop file or click here to upload/i })).toBeInTheDocument(); + }); + + it('should render component in mobile mode', () => { + (useDevice as jest.Mock).mockReturnValue({ isMobile: true }); + + render(); + expect(screen.getAllByText(/Upload your selfie/i)).toHaveLength(2); + expect(screen.getAllByRole('button')).toHaveLength(3); + expect(screen.getByText(/Tap here to upload/i)).toBeInTheDocument(); + }); + + it('should disable Confirm and upload button when no file is uploaded but enable Back button', () => { + render(); + expect(screen.getByRole('button', { name: /Confirm and upload/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /Back/i })).toBeEnabled(); + }); + + it('should enable Confirm and upload button when file is uploaded via drag and drop', async () => { + (useDevice as jest.Mock).mockReturnValue({ isMobile: true }); + const file = new File(['file'], 'ping.jpg', { + type: 'image/jpg', + }); + window.URL.createObjectURL = jest.fn().mockImplementation(() => 'url'); + render(); + const dropZoneEl = screen.getByTestId('dt_dropzone-input'); + userEvent.upload(dropZoneEl, file); + + expect(await screen.findByRole('button', { name: /Confirm and upload/i })).toBeEnabled(); + }); + + it('should enable Confirm and upload button when file is uploaded via file input', async () => { + (useDevice as jest.Mock).mockReturnValue({ isMobile: false }); + render(); + const file = new File(['file'], 'ping.jpg', { + type: 'image/jpg', + }); + const dropZoneEl = screen.getByTestId('dt_dropzone-input'); + fireEvent.change(dropZoneEl, { target: { files: [file] } }); + + expect(await screen.findByRole('button', { name: /Confirm and upload/i })).toBeEnabled(); + }); +}); diff --git a/packages/account-v2/src/containers/SelfieDocumentUpload/selfie-document-upload.tsx b/packages/account-v2/src/containers/SelfieDocumentUpload/selfie-document-upload.tsx index c8d4ff1016e4..8bd880bf6296 100644 --- a/packages/account-v2/src/containers/SelfieDocumentUpload/selfie-document-upload.tsx +++ b/packages/account-v2/src/containers/SelfieDocumentUpload/selfie-document-upload.tsx @@ -1,28 +1,61 @@ import React from 'react'; -import { useFormikContext } from 'formik'; -import { Text, useDevice } from '@deriv-com/ui'; +import { Formik, FormikValues } from 'formik'; +import { InferType } from 'yup'; +import { Button, Text, useDevice } from '@deriv-com/ui'; import SelfieIcon from '../../assets/manual-upload/selfie-icon.svg'; import { Dropzone } from '../../components/Dropzone'; +import { MANUAL_DOCUMENT_SELFIE } from '../../constants/manualFormConstants'; +import { getSelfieValidationSchema } from '../../utils/manualFormUtils'; -export const SelfieDocumentUpload = () => { +type TSelfieFormValue = InferType>; + +type TSelfieDocumentUpload = { + formData: FormikValues; + handleCancel: () => void; + handleSubmit: (value: TSelfieFormValue) => void; +}; + +export const SelfieDocumentUpload = ({ formData, handleCancel, handleSubmit }: TSelfieDocumentUpload) => { const { isMobile } = useDevice(); - const { setFieldValue } = useFormikContext(); + + const validationSchema = getSelfieValidationSchema(); + + const initialVal = validationSchema.cast({ + [MANUAL_DOCUMENT_SELFIE]: formData[MANUAL_DOCUMENT_SELFIE] ?? validationSchema.getDefault().selfie_with_id, + }); return ( -
- Upload your selfie - } - onFileChange={(file: File) => setFieldValue('selfie', file)} - /> - - Face forward and remove your glasses if necessary. Make sure your eyes are clearly visible and your face - is within the frame. - -
+ + {({ errors, initialValues, isValid, setFieldValue, values }) => { + console.log('Values: ', { initialValues, errors, values, isValid }); + return ( +
+ Upload your selfie + } + onFileChange={(file: File) => setFieldValue(MANUAL_DOCUMENT_SELFIE, file)} + /> + + Face forward and remove your glasses if necessary. Make sure your eyes are clearly visible + and your face is within the frame. + +
+ + +
+
+ ); + }} +
); }; diff --git a/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts b/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts new file mode 100644 index 000000000000..69413e7ecc31 --- /dev/null +++ b/packages/account-v2/src/hooks/__tests__/useManualForm.spec.ts @@ -0,0 +1,46 @@ +import { useSettings } from '@deriv/api'; +import { renderHook } from '@testing-library/react-hooks'; +import useManualForm from '../useManualForm'; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useSettings: jest.fn(), +})); + +describe('useManualForm', () => { + it('should return false if citizenship is ng', () => { + (useSettings as jest.Mock).mockReturnValue({ + data: { + citizen: 'ng', + country_code: 'in', + }, + }); + const { result } = renderHook(() => useManualForm()); + const { isExpiryDateRequired } = result.current; + expect(isExpiryDateRequired).toBe(false); + }); + + it('should return true if citizenship and country code is not ng', () => { + (useSettings as jest.Mock).mockReturnValue({ + data: { + citizen: 'au', + country_code: 'au', + }, + }); + const { result } = renderHook(() => useManualForm()); + const { isExpiryDateRequired } = result.current; + expect(isExpiryDateRequired).toBe(true); + }); + + it('should return true if citizenship is not ng but country code is ng', () => { + (useSettings as jest.Mock).mockReturnValue({ + data: { + citizen: 'in', + country_code: 'ng', + }, + }); + const { result } = renderHook(() => useManualForm()); + const { isExpiryDateRequired } = result.current; + expect(isExpiryDateRequired).toBe(true); + }); +}); diff --git a/packages/account-v2/src/hooks/index.ts b/packages/account-v2/src/hooks/index.ts index c73eaaea09f1..6e31bb348d3a 100644 --- a/packages/account-v2/src/hooks/index.ts +++ b/packages/account-v2/src/hooks/index.ts @@ -1 +1 @@ -export { default as useManualForm } from './ManualForm/useManualForm'; +export { default as useManualForm } from './useManualForm'; diff --git a/packages/account-v2/src/hooks/ManualForm/useManualForm.ts b/packages/account-v2/src/hooks/useManualForm.ts similarity index 53% rename from packages/account-v2/src/hooks/ManualForm/useManualForm.ts rename to packages/account-v2/src/hooks/useManualForm.ts index 7e0ea1c81774..dcdba4bc310b 100644 --- a/packages/account-v2/src/hooks/ManualForm/useManualForm.ts +++ b/packages/account-v2/src/hooks/useManualForm.ts @@ -2,12 +2,12 @@ import { useSettings } from '@deriv/api'; /** A custom hook used for manual verification flow */ const useManualForm = () => { - const { data: settings, ...rest } = useSettings(); - const isExpiryDateRequired = settings?.country !== 'ng'; + const { data: settings } = useSettings(); + const countryCode = settings?.citizen ?? settings?.country_code; + const isExpiryDateRequired = !!countryCode && countryCode !== 'ng'; return { isExpiryDateRequired, - ...rest, }; }; diff --git a/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx b/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx new file mode 100644 index 000000000000..e3f027becfcc --- /dev/null +++ b/packages/account-v2/src/modules/IDVForm/__tests__/idv-form.spec.tsx @@ -0,0 +1,33 @@ +import React, { ComponentProps } from 'react'; +import { Formik } from 'formik'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { IDVForm } from '../idv-form'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: jest.fn().mockReturnValue({ isMobile: false }), +})); + +type TIDVFormProps = ComponentProps; + +describe('IDVForm', () => { + const renderComponent = (props: TIDVFormProps = { selectedCountry: {} }) => { + return render( + + + + ); + }; + + it('should render IDVForm', () => { + renderComponent(); + + expect(screen.getByText(/Choose the document type/i)).toBeInTheDocument(); + expect(screen.getByText(/Enter your document number/i)).toBeInTheDocument(); + }); + + it('should throw error when IDVform is not wrapped with Formik', () => { + expect(() => render()).toThrowError(); + }); +}); diff --git a/packages/account-v2/src/modules/IDVForm/index.tsx b/packages/account-v2/src/modules/IDVForm/idv-form.tsx similarity index 92% rename from packages/account-v2/src/modules/IDVForm/index.tsx rename to packages/account-v2/src/modules/IDVForm/idv-form.tsx index c108a7a3c253..ddf588ac0b83 100644 --- a/packages/account-v2/src/modules/IDVForm/index.tsx +++ b/packages/account-v2/src/modules/IDVForm/idv-form.tsx @@ -1,12 +1,12 @@ import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { Field, FieldProps, FormikProps, useFormikContext } from 'formik'; import { useResidenceList } from '@deriv/api'; -import { useBreakpoint } from '@deriv/quill-design'; +import { useDevice } from '@deriv-com/ui'; import { WalletDropdown } from '../../components/base/WalletDropdown'; import { WalletTextField } from '../../components/base/WalletTextField'; import { DOCUMENT_LIST } from '../../mocks/idv-form.mock'; import { getIDVNotApplicableOption } from '../../utils/default-options'; -import { getSelectedDocumentConfigData, TDocument } from './utils'; +import { getSelectedDocumentConfigData, TDocument } from '../../utils/idv-form-utils'; type TIDVFormProps = { allowIDVSkip?: boolean; @@ -28,12 +28,13 @@ type TDropDownList = { }; export const IDVForm = ({ allowIDVSkip, selectedCountry }: TIDVFormProps) => { - const { setFieldValue, values }: FormikProps = useFormikContext(); + const formik: FormikProps = useFormikContext(); + const [documentList, setDocumentList] = useState([]); const [selectedDocument, setSelectedDocument] = useState(); - const { isMobile } = useBreakpoint(); + const { isMobile } = useDevice(); const { documents_supported } = selectedCountry; @@ -46,6 +47,24 @@ export const IDVForm = ({ allowIDVSkip, selectedCountry }: TIDVFormProps) => { value: '', }; + useEffect(() => { + if (documents_supported && Object.keys(documents_supported)?.length) { + const docList = Object.keys(documents_supported).map((key: string) => { + return { + text: documents_supported[key].display_name, + value: key, + }; + }); + setDocumentList(docList as TDropDownList[]); + } + }, [documents_supported]); + + if (!formik) { + throw new Error('IDVForm must be used within a Formik component'); + } + + const { setFieldValue, values } = formik; + const bindDocumentData = (item: string) => { setFieldValue('document_type', item, true); setSelectedDocument(getSelectedDocumentConfigData(item, DOCUMENT_LIST)); @@ -63,18 +82,6 @@ export const IDVForm = ({ allowIDVSkip, selectedCountry }: TIDVFormProps) => { } }; - useEffect(() => { - if (documents_supported && Object.keys(documents_supported)?.length) { - const docList = Object.keys(documents_supported).map((key: string) => { - return { - text: documents_supported[key].display_name, - value: key, - }; - }); - setDocumentList(docList as TDropDownList[]); - } - }, [documents_supported]); - return (
diff --git a/packages/account-v2/src/modules/IDVForm/index.ts b/packages/account-v2/src/modules/IDVForm/index.ts new file mode 100644 index 000000000000..d3b20c71bcaa --- /dev/null +++ b/packages/account-v2/src/modules/IDVForm/index.ts @@ -0,0 +1 @@ +export { IDVForm } from './idv-form'; diff --git a/packages/account-v2/src/modules/ManualUpload/manual-upload.tsx b/packages/account-v2/src/modules/ManualUpload/manual-upload.tsx index bc7e16185700..1cfe1e69473a 100644 --- a/packages/account-v2/src/modules/ManualUpload/manual-upload.tsx +++ b/packages/account-v2/src/modules/ManualUpload/manual-upload.tsx @@ -1,8 +1,7 @@ /* eslint-disable no-console */ import React, { useState } from 'react'; -import { TManualDocumentTypes } from '../../constants/manualFormConstants'; import { DocumentSelection } from '../../containers/DocumentSelection'; -import { ManualForm } from '../../containers/ManualForm'; +import { ManualUploadContainer } from '../../pages/ManualFormContainer/manual-form-container'; type TManualUploadProps = { countryCode: string }; @@ -10,16 +9,7 @@ export const ManualUpload = ({ countryCode }: TManualUploadProps) => { const [selectedDocument, setSelectedDocument] = useState(null); if (selectedDocument) { - return ( - { - console.log('Called on Cancel'); - setSelectedDocument(null); - }} - onSubmit={() => console.log('Called submit')} - selectedDocument={selectedDocument as TManualDocumentTypes} - /> - ); + return ; } return ; }; diff --git a/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx b/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx new file mode 100644 index 000000000000..c2cd581fde32 --- /dev/null +++ b/packages/account-v2/src/pages/ManualFormContainer/manual-form-container.tsx @@ -0,0 +1,50 @@ +/* eslint-disable no-console */ +import React, { useState } from 'react'; +import { InferType } from 'yup'; +import { TManualDocumentTypes } from '../../constants/manualFormConstants'; +import { ManualForm } from '../../containers/ManualForm'; +import { SelfieDocumentUpload } from '../../containers/SelfieDocumentUpload'; +import { getManualFormValidationSchema, getSelfieValidationSchema } from '../../utils/manualFormUtils'; + +type TManualUploadContainerProps = { + selectedDocument: string | null; + setSelectedDocument: (value: string | null) => void; +}; + +export const ManualUploadContainer = ({ selectedDocument, setSelectedDocument }: TManualUploadContainerProps) => { + const manualUpload = getManualFormValidationSchema(selectedDocument as TManualDocumentTypes).concat( + getSelfieValidationSchema() + ); + + type TManualUploadFormData = InferType; + + const [formData, setFormData] = useState>({}); + const [shouldUploadSelfie, setShouldUploadSelfie] = useState(false); + + if (shouldUploadSelfie) { + return ( + setShouldUploadSelfie(false)} + handleSubmit={values => { + setFormData(prev => ({ ...prev, ...values })); + }} + /> + ); + } + return ( + { + console.log('Called on Cancel'); + setSelectedDocument(null); + }} + onSubmit={values => { + console.log('Called submit'); + setFormData(prev => ({ ...prev, ...values })); + setShouldUploadSelfie(true); + }} + selectedDocument={selectedDocument as TManualDocumentTypes} + /> + ); +}; diff --git a/packages/account-v2/src/router/components/route-links/route-links.tsx b/packages/account-v2/src/router/components/route-links/route-links.tsx index c78e8fb41665..4c0994ffc1c1 100644 --- a/packages/account-v2/src/router/components/route-links/route-links.tsx +++ b/packages/account-v2/src/router/components/route-links/route-links.tsx @@ -4,7 +4,7 @@ import { defaultRoute, routes } from '../../constants/routesConfig'; const RouteLinks = () => ( -
+
{routes.map(route => ( MANUAL_DOCUMENT_TYPES_DATA[selectedDocument].inputSectionHeader; @@ -11,12 +15,44 @@ export const getFieldsConfig = (selectedDocument: TManualDocumentTypes) => MANUAL_DOCUMENT_TYPES_DATA[selectedDocument].fields; export const getUploadConfig = (selectedDocument: TManualDocumentTypes) => - MANUAL_DOCUMENT_TYPES_DATA[selectedDocument].upload; + MANUAL_DOCUMENT_TYPES_DATA[selectedDocument].uploads; + +export const getManualFormValidationSchema = ( + selectedDocument: TManualDocumentTypes, + isExpiryDateRequired: boolean +) => { + const fieldsConfig = getFieldsConfig(selectedDocument); + const uploadConfig = getUploadConfig(selectedDocument); + + const documentExpiryValidation = { + document_expiry: isExpiryDateRequired + ? Yup.string().required(fieldsConfig.documentExpiry.errorMessage) + : Yup.string().notRequired(), + }; + + const documentUploadValidation = Object.fromEntries( + uploadConfig.map(item => [item.pageType, Yup.string().required(item.error)]) + ); -export const getManualFormValidationSchema = (selectedDocument: TManualDocumentTypes) => { - const fieldsData = MANUAL_DOCUMENT_TYPES_DATA[selectedDocument].fields; return Yup.object({ - document_expiry: Yup.string().required(fieldsData.documentExpiry.errorMessage), - document_number: Yup.string().required(fieldsData.documentNumber.errorMessage), + document_number: Yup.string().required(fieldsConfig.documentNumber.errorMessage), + ...documentExpiryValidation, + ...documentUploadValidation, }); }; + +type Demo = Yup.InferType>; + +export const getSelfieValidationSchema = () => { + return Yup.object({ + [MANUAL_DOCUMENT_SELFIE]: Yup.mixed() + .test({ + message: 'File is required', + name: 'file', + test: value => { + return !!value && value instanceof File; + }, + }) + .required(), + }).default(() => ({ [MANUAL_DOCUMENT_SELFIE]: null })); +}; diff --git a/packages/account-v2/tailwind.config.ts b/packages/account-v2/tailwind.config.ts index 6aa418433aa7..d1ea6adc86f3 100644 --- a/packages/account-v2/tailwind.config.ts +++ b/packages/account-v2/tailwind.config.ts @@ -13,6 +13,7 @@ export default { '2': '#f2f3f4', '5': '#d6dadb', '6': '#d6d6d6', + '14': '#e9ecef', }, black: { '0': '#0e0e0e', diff --git a/packages/account/src/Components/poi/status/unsupported/card-details/selfie-upload.tsx b/packages/account/src/Components/poi/status/unsupported/card-details/selfie-upload.tsx index 15726249d263..586e886a4cb0 100644 --- a/packages/account/src/Components/poi/status/unsupported/card-details/selfie-upload.tsx +++ b/packages/account/src/Components/poi/status/unsupported/card-details/selfie-upload.tsx @@ -29,7 +29,8 @@ const SelfieUpload = ({ initial_values, goBack, onConfirm, onFileDrop }: TSelfie validate={values => validateFields(values, undefined, [SELFIE_DOCUMENT])} onSubmit={onConfirm} > - {({ values, isValid, isSubmitting, touched }: Partial>) => { + {({ values, isValid, isSubmitting, touched, initialValues }: Partial>) => { + console.log('Values: ', values, initialValues); let is_form_touched, is_form_empty; if (touched) { is_form_touched = Object.keys(touched).length > 0; diff --git a/packages/api/src/hooks/useSettings.ts b/packages/api/src/hooks/useSettings.ts index 6b739edf7a8f..48dd5159b114 100644 --- a/packages/api/src/hooks/useSettings.ts +++ b/packages/api/src/hooks/useSettings.ts @@ -1,4 +1,5 @@ import { useCallback, useMemo } from 'react'; +import useAuthorize from './useAuthorize'; import useQuery from '../useQuery'; import useInvalidateQuery from '../useInvalidateQuery'; import useMutation from '../useMutation'; @@ -9,7 +10,8 @@ type TSetSettingsPayload = NonNullable< /** A custom hook to get and update the user settings. */ const useSettings = () => { - const { data, ...rest } = useQuery('get_settings'); + const { isSuccess } = useAuthorize(); + const { data, ...rest } = useQuery('get_settings', { options: { enabled: isSuccess } }); const { mutate, ...mutate_rest } = useMutation('set_settings', { onSuccess: () => invalidate('get_settings') }); const invalidate = useInvalidateQuery();