diff --git a/packages/account/build/webpack.config.js b/packages/account/build/webpack.config.js index 562631e03316..b401cbad0039 100644 --- a/packages/account/build/webpack.config.js +++ b/packages/account/build/webpack.config.js @@ -46,7 +46,6 @@ module.exports = function (env) { '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', 'proof-of-identity-container-for-mt5': 'Sections/Verification/ProofOfIdentity/proof-of-identity-container-for-mt5', 'poi-poa-docs-submitted': 'Components/poi-poa-docs-submitted/poi-poa-docs-submitted', diff --git a/packages/account/src/Components/sample-credit-card-modal/__tests__/sample-credit-card-modal.spec.tsx b/packages/account/src/Components/sample-credit-card-modal/__tests__/sample-credit-card-modal.spec.tsx new file mode 100644 index 000000000000..090aa5fb8a3d --- /dev/null +++ b/packages/account/src/Components/sample-credit-card-modal/__tests__/sample-credit-card-modal.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import { SampleCreditCardModal } from '../sample-credit-card-modal'; + +describe('SampleCreditCardModal', () => { + let modal_root_el: HTMLDivElement; + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + it('should render modal props', () => { + const props: React.ComponentProps = { + is_open: true, + onClose: jest.fn(), + }; + render(); + expect(screen.getByRole('heading')).toHaveTextContent('How to mask your card?'); + expect(screen.getByRole('img')).toHaveAttribute('alt', 'creditcardsample'); + }); + + it('should not render modal when is_open is false', () => { + const props: React.ComponentProps = { + is_open: false, + onClose: jest.fn(), + }; + render(); + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); + expect(screen.queryByRole('img')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/sample-credit-card-modal/index.js b/packages/account/src/Components/sample-credit-card-modal/index.ts similarity index 92% rename from packages/account/src/Components/sample-credit-card-modal/index.js rename to packages/account/src/Components/sample-credit-card-modal/index.ts index 2d8dc1ebc6a8..281e001a64c0 100644 --- a/packages/account/src/Components/sample-credit-card-modal/index.js +++ b/packages/account/src/Components/sample-credit-card-modal/index.ts @@ -1,3 +1,3 @@ -import { SampleCreditCardModal } from './sample-credit-card-modal.jsx'; +import { SampleCreditCardModal } from './sample-credit-card-modal'; export default SampleCreditCardModal; diff --git a/packages/account/src/Components/sample-credit-card-modal/sample-credit-card-modal.jsx b/packages/account/src/Components/sample-credit-card-modal/sample-credit-card-modal.tsx similarity index 54% rename from packages/account/src/Components/sample-credit-card-modal/sample-credit-card-modal.jsx rename to packages/account/src/Components/sample-credit-card-modal/sample-credit-card-modal.tsx index d63963fabe65..eee329d67c46 100644 --- a/packages/account/src/Components/sample-credit-card-modal/sample-credit-card-modal.jsx +++ b/packages/account/src/Components/sample-credit-card-modal/sample-credit-card-modal.tsx @@ -1,9 +1,21 @@ import React from 'react'; import { Modal, Text } from '@deriv/components'; -import { localize, Localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; import { getUrlBase } from '@deriv/shared'; -export const SampleCreditCardModal = ({ is_open, onClose }) => { +type TSampleCreditCardModalProps = { + is_open: boolean; + onClose: () => void; +}; + +/** + * Display a modal with a sample credit card image and instructions on how to mask the card. + * @name SampleCreditCardModal + * @param is_open - boolean to determine if the modal should be open or not + * @param onClose - function to close the modal + * @returns React component + */ +export const SampleCreditCardModal = ({ is_open, onClose }: TSampleCreditCardModalProps) => { return ( { > - {localize( - 'Black out digits 7 to 12 of the card number that’s shown on the front of your debit/credit card.⁤' - )} + ({ icon_light: 'IcAdvcashLight', icon_dark: 'IcAdvcashDark', instructions: [ - localize('Upload a screenshot of your name and email address from the personal information section.'), + , ], input_label: localize('Email address'), identifier_type: 'email_address', @@ -25,6 +28,7 @@ const getPaymentMethodsConfig = () => ({ target='_blank' rel='noreferrer' href='https://app.astropay.com/profile' + aria-label='Read more on AstroPay' />, ]} />, @@ -35,14 +39,21 @@ const getPaymentMethodsConfig = () => ({ beyonic: { icon_light: 'IcBeyonic', icon_dark: 'IcBeyonic', - instructions: [localize('Upload your mobile bill statement showing your name and phone number.')], + instructions: [ + , + ], input_label: localize('Mobile number'), identifier_type: 'mobile_number', }, 'boleto (d24 voucher)': { icon_light: 'IcBoletoD24VoucherLight', icon_dark: 'IcBoletoD24VoucherDark', - instructions: [localize('Upload your bank statement showing your name and account details.')], + instructions: [ + , + ], input_label: localize('Bank account number'), identifier_type: 'bank_account_number', }, @@ -50,9 +61,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcVisaLight', icon_dark: 'IcVisaDark', instructions: [ - localize( - 'Upload a photo showing your name and the first six and last four digits of your card number. If the card does not display your name, upload the bank statement showing your name and card number in the transaction history.' - ), + , ], input_label: localize('Card number'), identifier_type: 'card_number', @@ -61,9 +73,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcMasterCardLight', icon_dark: 'IcMasterCardDark', instructions: [ - localize( - 'Upload a photo showing your name and the first six and last four digits of your card number. If the card does not display your name, upload the bank statement showing your name and card number in the transaction history.' - ), + , ], input_label: localize('Card number'), identifier_type: 'card_number', @@ -72,10 +85,13 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcPixLight', icon_dark: 'IcPixDark', instructions: [ - localize('Upload a screenshot of either of the following to process the transaction:'), - localize('- your account profile section on the website'), - localize('- the Account Information page on the app'), - localize('- your account details of the bank linked to your account'), + , + , + , + , ], input_label: localize('User ID'), identifier_type: 'user_id', @@ -84,9 +100,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcSkrillLight', icon_dark: 'IcSkrillDark', instructions: [ - localize( - 'Upload a screenshot of your name, account number, and email address from the personal details section of the app or profile section of your account on the website.' - ), + , ], input_label: localize('Email address'), identifier_type: 'email_address', @@ -116,6 +133,7 @@ const getPaymentMethodsConfig = () => ({ target='_blank' rel='noreferrer' href='https://onlinenaira.com/members/index.htm' + aria-label='Read more on OnlineNaira' />, ]} />, @@ -129,6 +147,7 @@ const getPaymentMethodsConfig = () => ({ target='_blank' rel='noreferrer' href='https://onlinenaira.com/members/bank.htm' + aria-label='Read more on OnlineNaira' />, ]} />, @@ -140,9 +159,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcWebMoneyLight', icon_dark: 'IcWebMoneyDark', instructions: [ - localize( - 'Upload a screenshot of your account and personal details page with your name, account number, phone number, and email address.' - ), + , ], input_label: localize('Account number'), identifier_type: 'account_number', @@ -151,7 +171,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcZingpay', icon_dark: 'IcZingpay', instructions: [ - localize('Upload your bank statement showing your name, account number, and transaction history.'), + , ], input_label: localize('Bank account number'), identifier_type: 'bank_account_number', @@ -160,7 +183,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcSticpayLight', icon_dark: 'IcSticpayDark', instructions: [ - localize('Upload a screenshot of your name and email address from the personal details section.'), + , ], input_label: localize('Email address'), identifier_type: 'email_address', @@ -169,7 +195,10 @@ const getPaymentMethodsConfig = () => ({ icon_light: 'IcJetonLight', icon_dark: 'IcJetonDark', instructions: [ - localize('Upload a screenshot of your name and account number from the personal details section.'), + , ], input_label: localize('Account number'), identifier_type: 'account_number', @@ -177,7 +206,12 @@ const getPaymentMethodsConfig = () => ({ other: { icon_light: 'IcOtherPaymentMethod', icon_dark: 'IcOtherPaymentMethod', - instructions: [localize('Upload a document showing your name and bank account number or account details.')], + instructions: [ + , + ], input_label: null, identifier_type: 'none', }, diff --git a/packages/account/src/Configs/proof-of-identity-config.ts b/packages/account/src/Configs/proof-of-identity-config.ts deleted file mode 100644 index cf1b0260e3ec..000000000000 --- a/packages/account/src/Configs/proof-of-identity-config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { GetSettings } from '@deriv/api-types'; -import { localize } from '@deriv/translations'; - -const proofOfIdentityConfig = ( - { account_settings }: { account_settings: GetSettings }, - ProofOfIdentityForm: React.Component -) => { - return { - header: { - active_title: localize('Identity information'), - title: localize('Identity information'), - }, - body: ProofOfIdentityForm, - props: { citizen: account_settings.citizen || account_settings.country_code }, - passthrough: ['refreshNotifications', 'residence_list'], - }; -}; - -export default proofOfIdentityConfig; diff --git a/packages/account/src/Constants/poo-identifier.ts b/packages/account/src/Constants/poo-identifier.ts new file mode 100644 index 000000000000..b4ccae0685bf --- /dev/null +++ b/packages/account/src/Constants/poo-identifier.ts @@ -0,0 +1,16 @@ +export const IDENTIFIER_TYPES = Object.freeze({ + ACCOUNT_ID: 'account_id', + ACCOUNT_NUMBER: 'account_number', + BANK_ACCOUNT_NUMBER: 'bank_account_number', + CARD_NUMBER: 'card_number', + EMAIL_ADDRESS: 'email_address', + MOBILE_NUMBER: 'mobile_number', + USER_ID: 'user_id', +}); + +export const CARD_NUMBER = { + MAX_LENGTH: 19, + MIN_LENGTH: 16, +}; + +export const MAX_FILE_SIZE = 8000; // 8MB diff --git a/packages/account/src/Containers/Account/account.tsx b/packages/account/src/Containers/Account/account.tsx index a19787236ed3..2b5c78068864 100644 --- a/packages/account/src/Containers/Account/account.tsx +++ b/packages/account/src/Containers/Account/account.tsx @@ -26,7 +26,7 @@ const Account = observer(({ history, location, routes }: TAccountProps) => { is_virtual, is_logged_in, is_logging_in, - is_pending_proof_of_ownership, + is_proof_of_ownership_enabled, landing_company_shortcode, should_allow_authentication, should_allow_poinc_authentication, @@ -56,7 +56,7 @@ const Account = observer(({ history, location, routes }: TAccountProps) => { } if (route.path === shared_routes.proof_of_ownership) { - route.is_disabled = is_virtual || !is_pending_proof_of_ownership; + route.is_disabled = is_virtual || !is_proof_of_ownership_enabled; } if (route.path === shared_routes.proof_of_income) { diff --git a/packages/account/src/Containers/proof-of-ownership/__tests__/card.spec.tsx b/packages/account/src/Containers/proof-of-ownership/__tests__/card.spec.tsx new file mode 100644 index 000000000000..7f51064b45ad --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/__tests__/card.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Card from '../card'; + +jest.mock('../expanded-card', () => jest.fn(() =>
Expanded Card
)); + +describe('Card', () => { + const mock_props: React.ComponentProps = { + details: { + icon: 'IcVisaLight', + payment_method: 'visa', + items: [ + { + creation_time: '1699433416524', + id: 4, + payment_method: 'visa', + documents_required: 1, + }, + ], + instructions: ['mock instruction 1', 'mock instruction 2'], + input_label: 'Card number', + identifier_type: 'card_number', + is_generic_pm: false, + documents_required: 1, + }, + index: 0, + }; + + it('should render payment method card', () => { + render(); + expect(screen.getByText('visa')).toBeInTheDocument(); + }); + + it('should render expanded card when clicked', () => { + render(); + + userEvent.click(screen.getByRole('button')); + expect(screen.getByText('Expanded Card')).toBeInTheDocument(); + }); + + it('should close the rendered expanded card when clicked', () => { + render(); + + userEvent.click(screen.getByRole('button')); + + expect(screen.getByText('Expanded Card')).toBeInTheDocument(); + + userEvent.click(screen.getByRole('button')); + + expect(screen.queryByText('Expanded Card')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Containers/proof-of-ownership/__tests__/example-link.spec.tsx b/packages/account/src/Containers/proof-of-ownership/__tests__/example-link.spec.tsx new file mode 100644 index 000000000000..14e256a5205b --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/__tests__/example-link.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ExampleLink from '../example-link'; + +describe('ExampleLink', () => { + let modal_root_el: HTMLDivElement; + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + it('renders ExampleLink component', () => { + render(); + expect(screen.getByText('See example')).toBeInTheDocument(); + }); + + it('should render SampleCreditCardModal when clicked', () => { + render(); + + userEvent.click(screen.getByText('See example')); + expect(screen.getByText('How to mask your card?')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Containers/proof-of-ownership/__tests__/expanded-card.spec.tsx b/packages/account/src/Containers/proof-of-ownership/__tests__/expanded-card.spec.tsx new file mode 100644 index 000000000000..b67f2f8158fa --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/__tests__/expanded-card.spec.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Formik } from 'formik'; +import { TPaymentMethodInfo } from 'Types'; +import ExpandedCard from '../expanded-card'; + +const grouped_payment_method_data: Record = { + visa: { + icon: 'IcVisaLight', + payment_method: 'visa', + items: [ + { + id: 4, + payment_method: 'visa', + documents_required: 1, + }, + ], + instructions: ['mock instruction 1'], + input_label: 'Card number', + identifier_type: 'card_number', + is_generic_pm: false, + documents_required: 1, + }, + onlinenaira: { + icon: 'IcOnlineNaira', + payment_method: 'onlinenaira', + items: [ + { + id: 9, + payment_method: 'onlinenaira', + documents_required: 2, + }, + ], + instructions: [ + 'Upload a screenshot of your username on the General Information page at https://onlinenaira.com/members/index.htm', + 'Upload a screenshot of your account number and phone number on the Bank Account/Mobile wallet page at https://onlinenaira.com/members/bank.htm', + ], + input_label: 'Account ID', + identifier_type: 'account_id', + is_generic_pm: false, + documents_required: 0, + }, +}; + +describe('expanded-card.jsx', () => { + const mock_props: React.ComponentProps = { + card_details: grouped_payment_method_data.visa, + }; + + const renderComponent = ({ props = mock_props }) => + render( + + + + ); + + it('should display correct identifier', () => { + renderComponent({}); + + expect(screen.getByDisplayValue('1234 56XX XXXX 1121')).toBeInTheDocument(); + }); + + it('should show example link for credit/debit card and render the correct identifier label', () => { + renderComponent({}); + const el_example_link = screen.getByText('See example'); + expect(el_example_link).toBeInTheDocument(); + expect(screen.getByText('Card number')).toBeInTheDocument(); + }); + + it('should render payment method link in the description', () => { + const new_props: React.ComponentProps = { + ...mock_props, + card_details: grouped_payment_method_data.onlinenaira, + }; + renderComponent({ props: new_props }); + const element = screen.getByText( + 'Upload a screenshot of your username on the General Information page at https://onlinenaira.com/members/index.htm' + ); + expect(element).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Containers/proof-of-ownership/card.tsx b/packages/account/src/Containers/proof-of-ownership/card.tsx new file mode 100644 index 000000000000..5be4912d0cf5 --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/card.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Button, Icon, Text } from '@deriv/components'; +import ExpandedCard from './expanded-card'; +import { TPaymentMethodInfo } from '../../Types'; + +type TCardProps = { + details: TPaymentMethodInfo; +}; + +/** + * Renders Chevron Icon which is used to expand card + * @name ExpansionIcon + * @param is_open - status to check if card is open + * @returns React Component + */ +const ExpansionIcon = ({ is_open }: { is_open: boolean }) => ( + +); + +/** + * Renders payment method + * @name Card + * @param details - payment method details + * @returns React Component + */ +const Card = ({ details }: TCardProps) => { + const [is_open, setIsOpen] = React.useState(false); + + const onClickHandler = () => { + setIsOpen(!is_open); + }; + + return ( +
+
+ {details?.icon && ( + + )} + + {details?.payment_method} + +
+ {is_open && } +
+ ); +}; + +export default Card; diff --git a/packages/account/src/Containers/proof-of-ownership/example-link.tsx b/packages/account/src/Containers/proof-of-ownership/example-link.tsx new file mode 100644 index 000000000000..9b2d14c95cf5 --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/example-link.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Text } from '@deriv/components'; +import { Localize } from '@deriv/translations'; +import SampleCreditCardModal from '../../Components/sample-credit-card-modal'; + +/** + * Renders Text with link to Example Credit card modal + * @name ExampleLink + * @returns React Component + */ +const ExampleLink = () => { + const [is_sample_modal_open, setIsSampleModalOpen] = React.useState(false); + + return ( + + setIsSampleModalOpen(true)}> + + + { + setIsSampleModalOpen(false); + }} + /> + + ); +}; + +export default ExampleLink; diff --git a/packages/account/src/Containers/proof-of-ownership/expanded-card.tsx b/packages/account/src/Containers/proof-of-ownership/expanded-card.tsx new file mode 100644 index 000000000000..107be4662c7a --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/expanded-card.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useFormikContext } from 'formik'; +import { Input, Text } from '@deriv/components'; +import { hasInvalidCharacters } from '@deriv/shared'; +import { IDENTIFIER_TYPES } from '../../Constants/poo-identifier'; +import { isSpecialPaymentMethod } from '../../Helpers/utils'; +import FileUploader from './file-uploader'; +import { TPaymentMethod, TPaymentMethodInfo, TProofOfOwnershipData, TProofOfOwnershipFormValue } from '../../Types'; +import ExampleLink from './example-link'; + +type TExpandedCardProps = { + card_details: TPaymentMethodInfo; +}; + +/** + * + * @param card_details Details of payment method + * @param index Index of payment method + * @returns React Component + */ +const ExpandedCard = ({ card_details }: TExpandedCardProps) => { + const { values, setFieldValue, errors } = useFormikContext>(); + + const payment_method = card_details.payment_method.toLowerCase() as TPaymentMethod; + + const handleBlur = (payment_method_identifier: string, identifier_type: string, payment_id: number) => { + handleIdentifierChange(formatIdentifier(payment_method_identifier, identifier_type), payment_id); + }; + const handleIdentifierChange = (payment_method_identifier: string, payment_id: number) => { + const selected_payment_method = values[payment_method] as Record; + + selected_payment_method[payment_id] = { + ...selected_payment_method[payment_id], + payment_method_identifier, + }; + + setFieldValue(payment_method, { ...selected_payment_method }); + }; + + const formatIdentifier = (payment_method_identifier: string, identifier_type: string) => { + let formatted_id = payment_method_identifier?.replace(/\s/g, '') || ''; + if (identifier_type === IDENTIFIER_TYPES.CARD_NUMBER) { + if (formatted_id.length !== 16 || (formatted_id.length === 16 && hasInvalidCharacters(formatted_id))) { + return formatted_id; + } + formatted_id = `${formatted_id.substring(0, 6)}XXXXXX${formatted_id.substring(12)}`; + } else if ([IDENTIFIER_TYPES.EMAIL_ADDRESS, IDENTIFIER_TYPES.USER_ID].some(s => s === identifier_type)) { + return formatted_id; + } + return formatted_id.replace(/(\w{4})/g, '$1 ').trim(); + }; + + return ( +
+ {card_details?.instructions?.map(instruction => ( + + {instruction} {card_details?.identifier_type === IDENTIFIER_TYPES.CARD_NUMBER && } + + ))} +
+ {card_details?.items && + card_details?.items.map(item => { + const controls_to_show = [...Array(item?.documents_required).keys()]; + const payment_id = item.id; + return ( +
+ {card_details?.input_label && isSpecialPaymentMethod(card_details?.icon) && ( +
+ { + handleIdentifierChange(e.target.value, payment_id); + }} + value={values?.[payment_method]?.[payment_id]?.payment_method_identifier} + onBlur={e => { + handleBlur( + e.target.value.trim(), + card_details?.identifier_type, + payment_id + ); + }} + data-testid='dt_payment_method_identifier' + error={errors?.[payment_method]?.[payment_id]?.payment_method_identifier} + /> +
+ )} + {controls_to_show.map(i => ( + + {card_details?.input_label && !isSpecialPaymentMethod(card_details?.icon) && ( +
+ { + handleIdentifierChange(e.target.value.trim(), payment_id); + }} + value={ + values?.[payment_method]?.[payment_id] + ?.payment_method_identifier ?? '' + } + onBlur={e => { + handleBlur( + e.target.value.trim(), + card_details?.identifier_type, + payment_id + ); + }} + data-testid='dt_payment_method_identifier' + error={ + errors?.[payment_method]?.[payment_id] + ?.payment_method_identifier ?? '' + } + /> +
+ )} +
+ +
+
+ ))} +
+ ); + })} +
+
+ ); +}; + +export default ExpandedCard; diff --git a/packages/account/src/Containers/proof-of-ownership/file-uploader.tsx b/packages/account/src/Containers/proof-of-ownership/file-uploader.tsx new file mode 100644 index 000000000000..66347fc1b2c6 --- /dev/null +++ b/packages/account/src/Containers/proof-of-ownership/file-uploader.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useFormikContext } from 'formik'; +import { Button, Input, Icon } from '@deriv/components'; +import { compressImageFiles } from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import { TPaymentMethod, TProofOfOwnershipFormValue } from 'Types'; + +type TFileUploaderProps = { + class_name?: string; + name: TPaymentMethod; + sub_index: number | string; + payment_id: number | string; +}; + +/** + * Field to upload files for Payment methods in proof of ownership form + * @name FileUploader + * @param class_name - To add custom styles to class + * @param name - Payment method name + * @param sub_index - Index of the file + * @param payment_id - Index of the payment method + * @returns React Component + */ + +const FileUploader = ({ class_name, name, sub_index, payment_id }: TFileUploaderProps) => { + const { values, setFieldValue, errors, setFieldError } = useFormikContext>(); + + const [show_browse_button, setShowBrowseButton] = React.useState( + !values[name]?.[payment_id]?.files?.[sub_index]?.name + ); + // Create a reference to the hidden file input element + const hidden_file_input = React.useRef(null); + const handleClick = e => { + e.nativeEvent.preventDefault(); + e.nativeEvent.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + hidden_file_input?.current?.click(); + }; + + const handleChange = async (event: React.FormEvent) => { + event.nativeEvent.preventDefault(); + event.nativeEvent.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + const file_to_upload = await compressImageFiles([event.target.files[0]]); + const payment_file_data = [...(values[name]?.[payment_id]?.files ?? [])]; + payment_file_data[sub_index] = file_to_upload[0]; + const selected_payment_method = values?.[name]; + if (!selected_payment_method) { + return; + } + selected_payment_method[payment_id] = { + ...selected_payment_method[payment_id], + files: payment_file_data ?? [], + }; + await setFieldValue(name, { ...selected_payment_method }); + setShowBrowseButton(!file_to_upload[0]); + }; + + const updateError = () => { + const payment_method_error = errors?.[name] ?? {}; + const payment_method_file_error = payment_method_error?.[payment_id]?.files ?? {}; + delete payment_method_file_error?.[sub_index]; + // @ts-expect-error Error is an object + payment_method_error[payment_id] = { + // @ts-expect-error Error is an object + ...(payment_method_error[payment_id] ?? {}), + files: payment_method_file_error, + }; + if (Object.keys(payment_method_error[payment_id]?.files).length === 0) { + delete payment_method_error[payment_id]?.payment_method_identifier; + } + + // @ts-expect-error Error is an array + setFieldError(name, { ...payment_method_error }); + }; + + const handleIconClick = async e => { + e.nativeEvent.preventDefault(); + e.nativeEvent.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + if (hidden_file_input.current && 'value' in hidden_file_input.current) { + hidden_file_input.current.value = ''; + } + const payment_file_data = values[name]?.[payment_id]?.files ?? []; + const filtered_file_data = payment_file_data.filter((_, i) => i !== sub_index); + const selected_payment_method = values?.[name]; + if (!selected_payment_method) { + return; + } + selected_payment_method[payment_id] = { + ...selected_payment_method[payment_id], + files: filtered_file_data ?? [], + }; + await setFieldValue(name, { ...selected_payment_method }); + setShowBrowseButton(prevState => !prevState); + updateError(); + }; + return ( +
+ + + } + /> +
+ ); +}; + +export default FileUploader; diff --git a/packages/account/src/Helpers/__tests__/utils.spec.tsx b/packages/account/src/Helpers/__tests__/utils.spec.tsx index afb61cdd87cc..d755d4720ac5 100644 --- a/packages/account/src/Helpers/__tests__/utils.spec.tsx +++ b/packages/account/src/Helpers/__tests__/utils.spec.tsx @@ -8,6 +8,7 @@ import { getRegex, isDocumentNumberValid, isFieldImmutable, + isSpecialPaymentMethod, preventEmptyClipboardPaste, shouldShowIdentityInformation, getOnfidoSupportedLocaleCode, @@ -272,3 +273,21 @@ describe('verifyFields', () => { expect(verifyFields('Expired')).toEqual(['first_name', 'last_name', 'date_of_birth']); }); }); + +describe('isSpecialPaymentMethod', () => { + it('should return false if payment method icon is IcCreditCard', () => { + expect(isSpecialPaymentMethod('IcCreditCard')).toBeFalsy(); + }); + + it('should return true if payment method icon is IcOnlineNaira', () => { + expect(isSpecialPaymentMethod('IcOnlineNaira')).toBeTruthy(); + }); + + it('should return true if payment method icon is IcAstroPayLight', () => { + expect(isSpecialPaymentMethod('IcAstroPayLight')).toBeTruthy(); + }); + + it('should return true if payment method icon is IcAstroPayDark', () => { + expect(isSpecialPaymentMethod('IcAstroPayDark')).toBeTruthy(); + }); +}); diff --git a/packages/account/src/Helpers/utils.tsx b/packages/account/src/Helpers/utils.tsx index 184e5fd16d32..01e36b13dc9f 100644 --- a/packages/account/src/Helpers/utils.tsx +++ b/packages/account/src/Helpers/utils.tsx @@ -12,7 +12,7 @@ import { AUTH_STATUS_CODES, } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; -import { getIDVDocuments } from '../Constants/idv-document-config'; +import { getIDVDocuments } from '../Configs/idv-document-config'; import { TServerError } from '../Types'; import { LANGUAGE_CODES } from '../Constants/onfido'; @@ -276,3 +276,6 @@ export const verifyFields = (status: TIDVErrorStatus) => { return ['first_name', 'last_name', 'date_of_birth']; } }; + +export const isSpecialPaymentMethod = (payment_method_icon: string) => + ['IcOnlineNaira', 'IcAstroPayLight', 'IcAstroPayDark'].some(icon => icon === payment_method_icon); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/card.spec.js b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/card.spec.js deleted file mode 100644 index db2e962d7d3a..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/card.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react'; -import Card from '../card.jsx'; -import React from 'react'; -import { grouped_payment_method_data } from './test-data'; - -describe('card.jsx', () => { - it('Should render a card', () => { - render(); - expect(screen.getByTestId(grouped_payment_method_data.visa.payment_method)).toBeInTheDocument(); - }); - it('Should render an expanded card on button click', () => { - render(); - const button = screen.getByTestId('dt_proof-of-ownership-button'); - fireEvent.click(button); - expect( - screen.getAllByText('Accepted formats: pdf, jpeg, jpg, and png. Max file size: 8MB')[0] - ).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/expanded-card.spec.js b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/expanded-card.spec.js deleted file mode 100644 index 875563a86978..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/expanded-card.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import ExpandedCard from '../expanded-card.jsx'; -import { grouped_payment_method_data } from './test-data.js'; - -describe('expanded-card.jsx', () => { - it('should display correct identifier', () => { - render( - {})} - /> - ); - const element = screen.getByDisplayValue('1234 56XX XXXX 1121'); - expect(element).toBeInTheDocument(); - }); - it('should show example link for credit/debit card and render the correct identifier label', () => { - render(); - const exampelLink = screen.getByText('See example'); - expect(exampelLink).toBeInTheDocument(); - const element = screen.getByText('Card number'); - expect(element).toBeInTheDocument(); - }); - it('should render payment method link in the description', () => { - render(); - const element = screen.getByText( - 'Upload a screenshot of your username on the General Information page at https://onlinenaira.com/members/index.htm' - ); - expect(element).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership-form.spec.js b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership-form.spec.js deleted file mode 100644 index 8f20ebe21696..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership-form.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; - -import { fireEvent, render, screen } from '@testing-library/react'; - -import ProofOfOwnershipForm from '../proof-of-ownership-form.jsx'; - -import { grouped_payment_method_data } from './test-data'; - -describe('proof-of-ownership-form.jsx', () => { - it('should render a single card item inside the form', () => { - render( - - ); - const cardItems = screen.getByRole('card-item'); - expect(cardItems).toBeInTheDocument(); - }); - it('should render multiple card items inside the form', () => { - render( - - ); - const cardItems = screen.getAllByRole('card-item'); - expect(cardItems).toHaveLength(Object.keys(grouped_payment_method_data).length); - }); - it('should format identifier', async () => { - render( - - ); - const poo_dropdown_button = await screen.findByTestId('dt_proof-of-ownership-button'); - fireEvent.click(poo_dropdown_button); - const identifier_input = await screen.findByTestId('dt_payment_method_identifier'); - fireEvent.change(identifier_input, { target: { value: '1234567891011121' } }); - fireEvent.blur(identifier_input); - const element = screen.getByDisplayValue('1234 56XX XXXX 1121'); - expect(element).toBeInTheDocument(); - }); -}); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership-form.spec.tsx b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership-form.spec.tsx new file mode 100644 index 000000000000..7ab08719edc1 --- /dev/null +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership-form.spec.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import { fireEvent, render, screen } from '@testing-library/react'; + +import ProofOfOwnershipForm from '../proof-of-ownership-form'; + +import { grouped_payment_method_data } from './test-data'; +import { StoreProvider, mockStore } from '@deriv/stores'; + +type TRenderComponentProps = { + props: React.ComponentProps; + store: ReturnType; +}; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useFileUploader: jest.fn(() => ({ + upload: jest.fn(), + })), +})); + +describe('proof-of-ownership-form.jsx', () => { + const mock_store = mockStore({ + client: { + email: 'test@testing.com', + }, + }); + + const renderComponent = ({ props, store = mock_store }: TRenderComponentProps) => { + return render( + + + + ); + }; + + it('should render a single card item inside the form', () => { + renderComponent({ props: { grouped_payment_method_data: { beyonic: grouped_payment_method_data.beyonic } } }); + + const cardItems = screen.getByTestId('beyonic'); + expect(cardItems).toBeInTheDocument(); + }); + + it('should render multiple card items inside the form', () => { + renderComponent({ + props: { grouped_payment_method_data }, + }); + + Object.keys(grouped_payment_method_data).forEach(key => { + const cardItem = screen.getByTestId(key); + expect(cardItem).toBeInTheDocument(); + }); + }); + + it('should format identifier', async () => { + renderComponent({ + props: { grouped_payment_method_data: { visa: grouped_payment_method_data.visa } }, + }); + + const poo_dropdown_button = await screen.findByTestId('dt_proof_of_ownership_button'); + fireEvent.click(poo_dropdown_button); + const identifier_input = await screen.findByTestId('dt_payment_method_identifier'); + fireEvent.change(identifier_input, { target: { value: '1234567891011121' } }); + fireEvent.blur(identifier_input); + const element = screen.getByDisplayValue('1234 56XX XXXX 1121'); + expect(element).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.tsx similarity index 82% rename from packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js rename to packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.tsx index 121fabf2f3bb..78b55a8c9c66 100644 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.tsx @@ -1,14 +1,27 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { ProofOfOwnership } from '../proof-of-ownership.jsx'; -import test_data from './test-data'; +import { GetAccountStatus } from '@deriv/api-types'; import { StoreProvider, mockStore } from '@deriv/stores'; +import { ProofOfOwnership } from '../proof-of-ownership'; +import test_data from './test-data'; + +type TRequests = DeepRequired['authentication']['ownership']['requests']; +type TStatus = DeepRequired['authentication']['ownership']['status']; + +jest.mock('@deriv/hooks', () => ({ + ...jest.requireActual('@deriv/hooks'), + useFileUploader: jest.fn(() => ({ + upload: jest.fn(), + })), +})); describe('proof-of-ownership.jsx', () => { - let ownership_temp; + let ownership_temp: typeof test_data; + beforeAll(() => { ownership_temp = test_data; }); + let store = mockStore({}); const ProofOfOwnershipScreen = () => { return ( @@ -16,7 +29,6 @@ describe('proof-of-ownership.jsx', () => { ); }; - let store = mockStore(); it('should render no poo required status page', () => { store = mockStore({ client: { @@ -94,7 +106,10 @@ describe('proof-of-ownership.jsx', () => { client: { account_status: { authentication: { - ownership: { requests: ownership_temp.requests, status: ownership_temp.status }, + ownership: { + requests: ownership_temp.requests as TRequests, + status: ownership_temp.status as TStatus, + }, needs_verification: ['ownership'], }, }, diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/test-data.js b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/test-data.ts similarity index 95% rename from packages/account/src/Sections/Verification/ProofOfOwnership/__test__/test-data.js rename to packages/account/src/Sections/Verification/ProofOfOwnership/__test__/test-data.ts index bef75681385b..cf7864e09c13 100644 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/test-data.js +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/test-data.ts @@ -1,12 +1,14 @@ export default { requests: [ { - id: '1', + creation_time: '123', + id: 1, payment_method: 'beyonic', documents_required: 1, }, { - id: '2', + creation_time: '234', + id: 2, payment_method: 'boleto (d24 voucher)', documents_required: 1, }, diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/card.jsx b/packages/account/src/Sections/Verification/ProofOfOwnership/card.jsx deleted file mode 100644 index 2221ec223559..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/card.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Button, Icon, Text } from '@deriv/components'; -import classNames from 'classnames'; -import React from 'react'; -import ExpandedCard from './expanded-card.jsx'; -import PropTypes from 'prop-types'; - -const Card = ({ card, error, index, setFieldValue, updateErrors, values }) => { - const [is_open, setIsOpen] = React.useState(false); - const onClickHandler = e => { - e.preventDefault(); - setIsOpen(!is_open); - }; - const icon = ( - - ); - - return ( -
-
- - - {card.payment_method} - -
- {is_open && ( - - )} -
- ); -}; - -Card.propTypes = { - card: PropTypes.object, - error: PropTypes.object, - index: PropTypes.number, - setFieldValue: PropTypes.func, - updateErrors: PropTypes.func, - values: PropTypes.object, -}; - -export default Card; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/constants/constants.js b/packages/account/src/Sections/Verification/ProofOfOwnership/constants/constants.js deleted file mode 100644 index 32ffad87b1df..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/constants/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -export const POO_STATUSES = { - none: 'none', - ownership: 'ownership', - pending: 'pending', - rejected: 'rejected', - verified: 'verified', -}; -export const IDENTIFIER_TYPES = { - account_id: 'account_id', - account_number: 'account_number', - bank_account_number: 'bank_account_number', - card_number: 'card_number', - email_address: 'email_address', - mobile_number: 'mobile_number', - user_id: 'user_id', -}; -export const VALIDATIONS = { - has_invalid_characters: target_string => /[^\dX\s]/.test(target_string), - is_formated_card_number: target_string => /(^\d{4})\s(\d{2}X{2})\s(X{4})\s(\d{4}$)/.test(target_string), -}; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/expanded-card.jsx b/packages/account/src/Sections/Verification/ProofOfOwnership/expanded-card.jsx deleted file mode 100644 index a975b7b685eb..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/expanded-card.jsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import FileUploader from './file-uploader'; -import { Input, Text } from '@deriv/components'; -import { localize } from '@deriv/translations'; -import SampleCreditCardModal from 'Components/sample-credit-card-modal'; -import classNames from 'classnames'; -import { IDENTIFIER_TYPES, VALIDATIONS } from './constants/constants.js'; - -const ExpandedCard = ({ card_details, error, index, setFieldValue, updateErrors, values }) => { - const [is_sample_modal_open, setIsSampleModalOpen] = useState(false); - const handleUploadedFile = async (name, file) => { - await setFieldValue(name, file); - }; - const handleBlur = (name, payment_method_identifier, identifier_type, item_id, item_index, documents_required) => { - handleIdentifierChange( - name, - formatIdentifier(payment_method_identifier, identifier_type), - item_id, - item_index, - documents_required - ); - }; - const handleIdentifierChange = (name, payment_method_identifier, item_id, item_index, documents_required) => { - setFieldValue(`${name}`, { - ...values.data?.[index]?.[item_index], - documents_required, - id: item_id, - payment_method_identifier, - is_generic_pm: card_details.is_generic_pm, - identifier_type: card_details.identifier_type, - }); - }; - const exampleLink = () => - card_details?.identifier_type === IDENTIFIER_TYPES.card_number && ( - { - setIsSampleModalOpen(true); - }} - > - {localize('See example')} - - ); - const formatIdentifier = (payment_method_identifier, identifier_type) => { - let formatted_id = payment_method_identifier?.replace(/\s/g, '') || ''; - if (identifier_type === IDENTIFIER_TYPES.card_number) { - if ( - formatted_id.length !== 16 || - (formatted_id.length === 16 && VALIDATIONS.has_invalid_characters(formatted_id)) - ) { - return formatted_id; - } - formatted_id = `${formatted_id.substring(0, 6)}XXXXXX${formatted_id.substring(12)}`; - } else if ([IDENTIFIER_TYPES.email_address, IDENTIFIER_TYPES.user_id].some(s => s === identifier_type)) { - return formatted_id; - } - return formatted_id.replace(/(\w{4})/g, '$1 ').trim(); - }; - const isSpecialPM = pm_icon => ['IcOnlineNaira', 'IcAstroPayLight', 'IcAstroPayDark'].some(ic => ic === pm_icon); - return ( - <> -
- {card_details?.instructions?.map(instruction => ( - - {instruction} {exampleLink()} - - ))} -
- {card_details.items.map((item, item_index) => { - const controls_to_show = [...Array(item?.documents_required).keys()]; - return ( -
- {card_details?.input_label && isSpecialPM(card_details?.icon) && ( -
- { - handleIdentifierChange( - `data[${index}].[${item_index}]`, - e.currentTarget.value.trim(), - item.id, - item_index, - card_details.documents_required - ); - }} - value={values?.data?.[index]?.[item_index]?.payment_method_identifier || ''} - onBlur={e => { - handleBlur( - `data[${index}].[${item_index}]`, - e.currentTarget.value.trim(), - card_details?.identifier_type, - item.id, - item_index, - card_details.documents_required - ); - }} - data-testid='dt_payment_method_identifier' - error={error?.[item_index]?.payment_method_identifier} - /> -
- )} - {controls_to_show.map(i => ( - - {card_details?.input_label && !isSpecialPM(card_details?.icon) && ( -
- { - handleIdentifierChange( - `data[${index}].[${item_index}]`, - e.currentTarget.value.trim(), - item.id, - item_index, - card_details.documents_required - ); - }} - value={ - values?.data?.[index]?.[item_index] - ?.payment_method_identifier ?? '' - } - onBlur={e => { - handleBlur( - `data[${index}].[${item_index}]`, - e.currentTarget.value.trim(), - card_details?.identifier_type, - item.id, - item_index, - card_details.documents_required - ); - }} - data-testid='dt_payment_method_identifier' - error={error?.[item_index]?.payment_method_identifier} - /> -
- )} -
- -
-
- ))} -
- ); - })} -
-
- { - setIsSampleModalOpen(false); - }} - /> - - ); -}; - -ExpandedCard.propTypes = { - card_details: PropTypes.object, - error: PropTypes.object, - index: PropTypes.number, - setFieldValue: PropTypes.func, - updateErrors: PropTypes.func, - values: PropTypes.object, -}; - -export default ExpandedCard; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/file-uploader.jsx b/packages/account/src/Sections/Verification/ProofOfOwnership/file-uploader.jsx deleted file mode 100644 index d82e68c5d713..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/file-uploader.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { localize } from '@deriv/translations'; -import { Button, Input, Icon } from '@deriv/components'; -import { compressImageFiles } from '@deriv/shared'; -import PropTypes from 'prop-types'; - -const FileUploader = ({ - class_name, - error, - file_name, - handleFile, - index, - item_index, - name, - sub_index, - updateErrors, -}) => { - const [show_browse_button, setShowBrowseButton] = React.useState(!file_name); - // Create a reference to the hidden file input element - const hidden_file_input = React.useRef(null); - const handleClick = e => { - e.nativeEvent.preventDefault(); - e.nativeEvent.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - hidden_file_input.current.click(); - }; - - const handleChange = async event => { - event.nativeEvent.preventDefault(); - event.nativeEvent.stopPropagation(); - event.nativeEvent.stopImmediatePropagation(); - const file_to_upload = await compressImageFiles([event.target.files[0]]); - handleFile(name, file_to_upload[0]); - setShowBrowseButton(!file_to_upload[0]); - }; - const handleIconClick = async e => { - e.nativeEvent.preventDefault(); - e.nativeEvent.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - hidden_file_input.current.value = ''; - await handleFile(name, ''); - setShowBrowseButton(prevState => !prevState); - await updateErrors(index, item_index, sub_index); - }; - return ( -
- - - } - /> -
- ); -}; - -FileUploader.propTypes = { - class_name: PropTypes.string, - error: PropTypes.string, - file_name: PropTypes.string, - handleFile: PropTypes.func, - index: PropTypes.number, - item_index: PropTypes.number, - name: PropTypes.string, - sub_index: PropTypes.number, - updateErrors: PropTypes.func, -}; - -export default FileUploader; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/index.js b/packages/account/src/Sections/Verification/ProofOfOwnership/index.js deleted file mode 100644 index 3e8b8cd6b9a9..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import POO from './proof-of-ownership.jsx'; - -export default POO; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/index.ts b/packages/account/src/Sections/Verification/ProofOfOwnership/index.ts new file mode 100644 index 000000000000..4ffb8f05098c --- /dev/null +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/index.ts @@ -0,0 +1,3 @@ +import POO from './proof-of-ownership'; + +export default POO; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership-form.jsx b/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership-form.jsx deleted file mode 100644 index 12157b2f0198..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership-form.jsx +++ /dev/null @@ -1,289 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, useStateCallback } from '@deriv/components'; -import { Formik } from 'formik'; -import { localize } from '@deriv/translations'; -import FormFooter from '../../../Components/form-footer'; -import FormBody from '../../../Components/form-body'; -import FormSubHeader from '../../../Components/form-sub-header'; -import FormBodySection from '../../../Components/form-body-section'; -import { isMobile, readFiles, WS, DOCUMENT_TYPE } from '@deriv/shared'; -import Card from './card.jsx'; -import DocumentUploader from '@binary-com/binary-document-uploader'; -import { IDENTIFIER_TYPES, VALIDATIONS } from './constants/constants'; - -const getScrollOffset = (items_count = 0) => { - if (isMobile()) return '200px'; - if (items_count <= 2) return '0px'; - return '80px'; -}; -const ProofOfOwnershipForm = ({ - client_email, - grouped_payment_method_data, - refreshNotifications, - updateAccountStatus, -}) => { - const grouped_payment_method_data_keys = Object.keys(grouped_payment_method_data); - const initial_values = {}; - const [form_state, setFormState] = useStateCallback({ should_show_form: true }); - const form_ref = React.useRef(); - const fileReadErrorMessage = filename => { - return localize('Unable to read file {{name}}', { name: filename }); - }; - const validateFields = values => { - let errors = {}; - errors.data = [...(form_ref?.current?.errors?.data || [])]; - let total_documents_uploaded = 0; - let has_errors = false; - let are_files_uploaded = false; - const cards = values?.data; - Object.keys(cards)?.forEach?.(card_key => { - const items = cards?.[card_key] ?? {}; - const item_keys = Object.keys(items); - item_keys?.forEach?.(item_key => { - if (!has_errors) { - errors.data[card_key] = errors.data?.[card_key] ?? {}; - errors.data[card_key][item_key] = errors.data?.[card_key]?.[item_key] ?? {}; - const payment_method = items?.[item_key]; - const payment_method_identifier = payment_method?.payment_method_identifier?.trim(); - const is_payment_method_identifier_provided = - payment_method?.is_generic_pm || payment_method_identifier?.length > 0; - const is_credit_or_debit_card = payment_method?.identifier_type === IDENTIFIER_TYPES.card_number; - total_documents_uploaded = payment_method?.files?.filter(Boolean)?.length ?? 0; - if (is_payment_method_identifier_provided) { - are_files_uploaded = total_documents_uploaded === payment_method.documents_required; - } else if ( - (!payment_method?.documents_required && total_documents_uploaded === 0) || - (!is_payment_method_identifier_provided && total_documents_uploaded === 0) - ) { - are_files_uploaded = true; - } else if ( - (payment_method?.documents_required && - is_payment_method_identifier_provided && - total_documents_uploaded === 0) || - (!is_payment_method_identifier_provided && - total_documents_uploaded === payment_method?.documents_required * 0.5) - ) { - are_files_uploaded = false; - } - delete errors.data[card_key][item_key].payment_method_identifier; - payment_method?.files?.forEach((file, i) => { - errors.data[card_key][item_key].files = errors?.data?.[card_key]?.[item_key]?.files ?? []; - if (file?.type && !/(image|application)\/(jpe?g|pdf|png)$/.test(file?.type)) { - errors.data[card_key][item_key].files[i] = localize( - "That file format isn't supported. Please upload .pdf, .png, .jpg, or .jpeg files only." - ); - } - if (file?.size / 1024 > 8000) { - errors.data[card_key][item_key].files[i] = localize( - 'That file is too big (only up to 8MB allowed). Please upload another file.' - ); - } - if (errors.data[card_key][item_key].files.length === 0) { - delete errors.data[card_key][item_key].files; - } - if ( - !is_payment_method_identifier_provided && - (total_documents_uploaded === payment_method?.documents_required || - (!payment_method?.documents_required && total_documents_uploaded > 0) || - total_documents_uploaded === payment_method?.documents_required * 0.5) - ) { - errors.data[card_key][item_key].payment_method_identifier = - localize('Please complete this field.'); - } - }); - if ( - is_credit_or_debit_card && - ((payment_method_identifier?.length !== 0 && - (payment_method_identifier?.length !== 16 || payment_method_identifier?.length > 19) && - !VALIDATIONS.is_formated_card_number(payment_method_identifier)) || - (payment_method_identifier?.length === 16 && - VALIDATIONS.has_invalid_characters(payment_method_identifier))) - ) { - errors.data[card_key][item_key].payment_method_identifier = - localize('Enter your full card number'); - } - if (!payment_method_identifier && total_documents_uploaded === 0) { - delete form_ref.current?.values?.data?.[card_key]?.[item_key]; - if ((form_ref.current?.values?.data?.[card_key]?.filter?.(Boolean)?.length || 0) === 0) { - delete form_ref.current?.values?.data?.[card_key]; - } - } - if (Object.keys(errors?.data?.[card_key]?.[item_key] || {}).length === 0) { - delete errors?.data?.[card_key]?.[item_key]; - if (Object.keys(errors?.data?.[card_key])?.length === 0) { - delete errors?.data?.[card_key]; - } - } - has_errors = - has_errors || - errors?.data?.[card_key]?.[item_key]?.payment_method_identifier?.trim?.()?.length > 0 || - errors?.data?.[card_key]?.[item_key]?.files?.length > 0 || - !are_files_uploaded; - } - }); - if ((form_ref.current?.values?.data?.[card_key]?.filter(Boolean)?.length || 0) === 0) { - delete form_ref.current?.values?.data?.[card_key]; - } - }); - has_errors = has_errors || (form_ref.current?.values?.data?.filter(Boolean).length || 0) === 0; - if (!has_errors) { - errors = {}; - } - return errors; - }; - - const updateErrors = async (index, item_index, sub_index) => { - let error_count = 0; - const errors = {}; - errors.data = [...(form_ref?.current?.errors?.data || [])]; - if (typeof errors.data[index] === 'object') { - delete errors?.data?.[index]?.[item_index]?.files?.[sub_index]; - const has_other_errors = errors?.data[index]?.[item_index]?.files?.some(error => error !== null); - if (!has_other_errors) { - delete errors?.data[index]?.[item_index]; - } - errors.data.forEach(e => { - error_count += Object.keys(e || {}).length; - }); - if (error_count === 0) { - errors.data = []; - } - } - await form_ref.current.setErrors(errors); - await form_ref.current.validateForm(); - }; - const handleFormSubmit = ({ data: form_values }) => { - try { - setFormState({ ...form_state, ...{ is_btn_loading: true } }); - const uploader = new DocumentUploader({ connection: WS.getSocket() }); - if (form_ref.current.errors.length > 0) { - // Only upload if no errors and a file has been attached - return; - } - Object.keys(form_values).forEach(card_key => { - Object.keys(form_values[card_key]).forEach(async card_item_key => { - const payment_method_details = form_values[card_key][card_item_key]; - if (payment_method_details.files.length > 0) { - const processed_files = await readFiles(payment_method_details.files, fileReadErrorMessage, { - documentType: DOCUMENT_TYPE.proof_of_ownership, - proof_of_ownership: { - details: { - email: client_email, - payment_identifier: payment_method_details.payment_method_identifier, - }, - id: payment_method_details.id, - }, - }); - processed_files.forEach(async (processed_file, sub_index) => { - const response = await uploader.upload(processed_file); - if (response.warning) { - if (response.warning.trim() === 'DuplicateUpload') { - let { errors: form_errors } = form_ref?.current; - if (!form_errors?.data) { - form_errors = {}; - form_errors.data = []; - form_errors.data[card_key] = {}; - } else if (!form_errors.data?.[card_key]) { - form_errors.data[card_key] = {}; - } - form_errors.data[card_key][card_item_key] = - form_errors?.data?.[card_key]?.[card_item_key] ?? {}; - form_errors.data[card_key][card_item_key].files = - form_errors?.data?.[card_key]?.[card_item_key]?.files ?? []; - form_errors.data[card_key][card_item_key].files[sub_index] = response.message; // Document already uploaded - await form_ref.current.setErrors(form_errors); - await form_ref.current.validateForm(); - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - } else { - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - } - } else { - updateAccountStatus(); - refreshNotifications(); - } - }); - } - }); - }); - } catch (err) { - setFormState({ ...form_state, ...{ is_btn_loading: false } }); - } - }; - return ( - - {({ values, errors, setFieldValue, handleSubmit, isValid, dirty }) => ( -
{ - e.nativeEvent.preventDefault(); - e.nativeEvent.stopPropagation(); - e.nativeEvent.stopImmediatePropagation(); - handleSubmit(e); - }} - > - - - -
- {grouped_payment_method_data_keys?.map((grouped_payment_method_data_key, index) => ( -
- {grouped_payment_method_data_keys.length > 1 && ( -
-
{index + 1}
- {index !== grouped_payment_method_data_keys.length - 1 && ( -
- )} -
- )} - -
- ))} -
-
-
- - +
+ + )} +
+ ); +}); + +ProofOfOwnershipForm.displayName = 'ProofOfOwnershipForm'; + +export default ProofOfOwnershipForm; diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx b/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx deleted file mode 100644 index bda5290247f3..000000000000 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { withRouter } from 'react-router-dom'; -import ProofOfOwnershipForm from './proof-of-ownership-form.jsx'; -import { POONotRequired, POOVerified, POORejected, POOSubmitted } from 'Components/poo/statuses'; -import { Loading } from '@deriv/components'; -import { POO_STATUSES } from './constants/constants'; -import getPaymentMethodsConfig from './payment-method-config.js'; -import { observer, useStore } from '@deriv/stores'; - -export const ProofOfOwnership = observer(() => { - const { client, notifications, ui } = useStore(); - const { account_status, email: client_email, updateAccountStatus } = client; - const { refreshNotifications } = notifications; - const { is_dark_mode_on: is_dark_mode } = ui; - const cards = account_status?.authentication?.ownership?.requests; - const [status, setStatus] = useState(POO_STATUSES.none); - const citizen = client?.account_settings?.citizen || client?.account_settings?.country_code; - - const grouped_payment_method_data = React.useMemo(() => { - const groups = {}; - const payment_methods_config = getPaymentMethodsConfig(); - cards?.forEach(card => { - const card_details = - payment_methods_config[card.payment_method.toLowerCase()] || payment_methods_config.other; - if (groups[card?.payment_method?.toLowerCase()]) { - groups[card?.payment_method?.toLowerCase()].items.push(card); - } else { - groups[card?.payment_method?.toLowerCase()] = { - documents_required: card?.documents_required, - icon: is_dark_mode ? card_details?.icon_dark : card_details?.icon_light, - payment_method: card?.payment_method, - items: [card], - instructions: card_details.instructions, - input_label: card_details?.input_label, - identifier_type: card_details.identifier_type, - is_generic_pm: !card_details?.input_label, - }; - } - }); - return { groups }; - }, [cards, is_dark_mode]); - useEffect(() => { - setStatus(account_status?.authentication?.ownership?.status?.toLowerCase()); - }, [account_status]); - const onTryAgain = () => { - setStatus(POO_STATUSES.none); - }; - if (cards?.length > 0 && status !== POO_STATUSES.rejected) { - return ( - - ); // Proof of ownership is required. - } - if (status === POO_STATUSES.verified) { - return ; // Proof of ownership verified - } - if (status === POO_STATUSES.pending) { - return ; // Proof of ownership submitted pending review - } - if (status === POO_STATUSES.none) { - return ; // Client does not need proof of ownership. - } - if (status === POO_STATUSES.rejected) { - return ; // Proof of ownership rejected - } - return ; -}); - -export default withRouter(ProofOfOwnership); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.tsx b/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.tsx new file mode 100644 index 000000000000..7e1a92889676 --- /dev/null +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react'; +import { withRouter } from 'react-router-dom'; +import { GetAccountStatus } from '@deriv/api-types'; +import { Loading } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; +import { AUTH_STATUS_CODES } from '@deriv/shared'; +import ProofOfOwnershipForm from './proof-of-ownership-form'; +import { POONotRequired, POOVerified, POORejected, POOSubmitted } from '../../../Components/poo/statuses'; +import getPaymentMethodsConfig from '../../../Configs/payment-method-config'; +import { TPaymentMethod, TPaymentMethodIdentifier, TPaymentMethodInfo, TAuthStatusCodes } from '../../../Types'; + +type TPaymentData = DeepRequired['authentication']['ownership']['requests']; + +export const ProofOfOwnership = observer(() => { + const { client, ui } = useStore(); + const { account_status } = client; + const { is_dark_mode_on: is_dark_mode } = ui; + const cards = account_status?.authentication?.ownership?.requests; + const [status, setStatus] = useState(); + + const grouped_payment_method_data = React.useMemo(() => { + const groups: Partial> = {}; + const payment_methods_config = getPaymentMethodsConfig(); + cards?.forEach(card => { + const card_payment_method = card?.payment_method?.toLowerCase(); + const card_details = + payment_methods_config[card_payment_method as TPaymentMethod] || payment_methods_config.other; + + if (groups[card_payment_method as TPaymentMethod]) { + groups[card_payment_method as TPaymentMethod]?.items?.push(card as TPaymentData[number]); + } else { + groups[card_payment_method as TPaymentMethod] = { + documents_required: (card as TPaymentData[number])?.documents_required, + icon: is_dark_mode ? card_details?.icon_dark : card_details?.icon_light, + payment_method: (card as TPaymentData[number])?.payment_method, + items: [card as TPaymentData[number]], + instructions: card_details.instructions, + input_label: card_details?.input_label, + identifier_type: card_details.identifier_type as TPaymentMethodIdentifier, + is_generic_pm: !card_details?.input_label, + }; + } + }); + return { groups }; + }, [cards, is_dark_mode]); + + useEffect(() => { + setStatus(account_status?.authentication?.ownership?.status?.toLowerCase() as TAuthStatusCodes); + }, [account_status]); + + const onTryAgain = () => { + setStatus(AUTH_STATUS_CODES.NONE); + }; + + if (cards?.length && status !== AUTH_STATUS_CODES.REJECTED) { + return ; + } + if (status === AUTH_STATUS_CODES.VERIFIED) return ; + if (status === AUTH_STATUS_CODES.PENDING) return ; + if (status === AUTH_STATUS_CODES.NONE) return ; + if (status === AUTH_STATUS_CODES.REJECTED) return ; + return ; +}); + +ProofOfOwnership.displayName = 'ProofOfOwnership'; + +export default withRouter(ProofOfOwnership); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/validation.ts b/packages/account/src/Sections/Verification/ProofOfOwnership/validation.ts new file mode 100644 index 000000000000..6b5f1a1d4ccc --- /dev/null +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/validation.ts @@ -0,0 +1,35 @@ +import { hasInvalidCharacters, isFormattedCardNumber, validFile } from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import { CARD_NUMBER, IDENTIFIER_TYPES, MAX_FILE_SIZE } from 'Constants/poo-identifier'; +import { TPaymentMethodIdentifier } from 'Types'; + +export const isValidPaymentMethodIdentifier = ( + payment_method_identifier: string, + identifier_type: TPaymentMethodIdentifier | '' +) => { + const default_error_message = localize('Enter your full card number'); + + if (identifier_type === IDENTIFIER_TYPES.CARD_NUMBER) { + if (payment_method_identifier.length < CARD_NUMBER.MIN_LENGTH) { + return default_error_message; + } else if (payment_method_identifier.length === CARD_NUMBER.MIN_LENGTH) { + return !hasInvalidCharacters(payment_method_identifier) ? null : default_error_message; + } else if ( + payment_method_identifier.length !== CARD_NUMBER.MIN_LENGTH || + payment_method_identifier.length > CARD_NUMBER.MAX_LENGTH + ) { + return isFormattedCardNumber(payment_method_identifier) ? null : default_error_message; + } + return null; + } + return null; +}; + +export const isValidFile = (file: File) => { + if (!validFile(file)) { + return localize("That file format isn't supported. Please upload .pdf, .png, .jpg, or .jpeg files only."); + } else if (file?.size / 1024 > MAX_FILE_SIZE) { + return localize('That file is too big (only up to 8MB allowed). Please upload another file.'); + } + return null; +}; diff --git a/packages/account/src/Types/common.type.ts b/packages/account/src/Types/common.type.ts index 1ff8d926dc3b..9d40f7791f18 100644 --- a/packages/account/src/Types/common.type.ts +++ b/packages/account/src/Types/common.type.ts @@ -1,9 +1,12 @@ /** Add types that are shared between components */ import React from 'react'; +import { IDENTIFIER_TYPES } from '../Constants/poo-identifier'; +import getPaymentMethodsConfig from '../Configs/payment-method-config'; import { Redirect, RouteProps } from 'react-router-dom'; import { TPage404 } from '../Constants/routes-config'; import { Authorize, + GetAccountStatus, DetailsOfEachMT5Loginid, GetFinancialAssessment, IdentityVerificationAddDocumentResponse, @@ -110,16 +113,6 @@ type TIdentity = { }; }; -export type TFile = { - path: string; - lastModified: number; - lastModifiedDate: Date; - name: string; - size: number; - type: string; - webkitRelativePath: string; -}; - export type TPOIStatus = { needs_poa?: boolean; redirect_button?: React.ReactElement; @@ -149,10 +142,6 @@ export type TDocument = { }; }; -export type TVerificationStatus = Readonly< - Record<'none' | 'pending' | 'rejected' | 'verified' | 'expired' | 'suspected', string> ->; - export type TIDVFormValues = { document_type: TDocument; document_number: string; @@ -239,6 +228,39 @@ export type TLoginHistoryItems = { status: string; }; +export type TPaymentMethodIdentifier = typeof IDENTIFIER_TYPES[keyof typeof IDENTIFIER_TYPES]; + +export type TPaymentMethodInfo = { + documents_required: number; + icon: string; + payment_method: string; + items: DeepRequired['authentication']['ownership']['requests']; + instructions: string[] | JSX.Element[]; + input_label: string | null; + identifier_type: TPaymentMethodIdentifier | ''; + is_generic_pm: boolean; +}; + +export type TFile = File & { file: Blob }; + +export type TPaymentMethod = keyof ReturnType; + +export type TProofOfOwnershipFormValue = Record>; + +export type TProofOfOwnershipData = { + documents_required: number; + id: number; + identifier_type: TPaymentMethodIdentifier | ''; + is_generic_pm: boolean; + files: Array; + payment_method_identifier: string; +}; + +export type TProofOfOwnershipErrors = Record< + TPaymentMethod, + Array<{ payment_method_identifier?: string; files?: Array }> +>; + export type TAuthStatusCodes = typeof AUTH_STATUS_CODES[keyof typeof AUTH_STATUS_CODES]; export type TMT5AccountStatus = typeof MT5_ACCOUNT_STATUS[keyof typeof MT5_ACCOUNT_STATUS]; diff --git a/packages/core/src/App/Components/Layout/Header/toggle-menu-drawer.jsx b/packages/core/src/App/Components/Layout/Header/toggle-menu-drawer.jsx index 370fd80ba495..b83b5e48ba33 100644 --- a/packages/core/src/App/Components/Layout/Header/toggle-menu-drawer.jsx +++ b/packages/core/src/App/Components/Layout/Header/toggle-menu-drawer.jsx @@ -43,7 +43,7 @@ const ToggleMenuDrawer = observer(({ platform_config }) => { should_allow_poinc_authentication, landing_company_shortcode: active_account_landing_company, is_landing_company_loaded, - is_pending_proof_of_ownership, + is_proof_of_ownership_enabled, is_eu, } = client; const { cashier } = modules; @@ -149,7 +149,7 @@ const ToggleMenuDrawer = observer(({ platform_config }) => { } else if (/proof-of-income/.test(route_path)) { return !should_allow_poinc_authentication; } else if (/proof-of-ownership/.test(route_path)) { - return is_virtual || !is_pending_proof_of_ownership; + return is_virtual || !is_proof_of_ownership_enabled; } return false; }; diff --git a/packages/core/src/App/Containers/RealAccountSignup/proof-of-address-form.js b/packages/core/src/App/Containers/RealAccountSignup/proof-of-address-form.js deleted file mode 100644 index 68ec96526756..000000000000 --- a/packages/core/src/App/Containers/RealAccountSignup/proof-of-address-form.js +++ /dev/null @@ -1,26 +0,0 @@ -import { localize } from '@deriv/translations'; -import { ProofOfAddressContainer } from '@deriv/account'; -import { generateValidationFunction, getDefaultFields } from '@deriv/shared'; - -const proof_of_address_config = { - poi_state: { - supported_in: ['maltainvest', 'malta', 'svg', 'iom'], - default_value: '', - rules: [], - }, -}; - -export const proofOfAddressConfig = ({ real_account_signup_target }) => { - return { - header: { - active_title: localize('Complete your proof of address'), - title: localize('Proof of address'), - }, - body: ProofOfAddressContainer, - form_value: getDefaultFields(real_account_signup_target, proof_of_address_config), - props: { - validate: generateValidationFunction(real_account_signup_target, proof_of_address_config), - }, - passthrough: ['refreshNotifications'], - }; -}; diff --git a/packages/core/src/App/Containers/RealAccountSignup/proof-of-identity-form.js b/packages/core/src/App/Containers/RealAccountSignup/proof-of-identity-form.js deleted file mode 100644 index 51d7ca43fe00..000000000000 --- a/packages/core/src/App/Containers/RealAccountSignup/proof-of-identity-form.js +++ /dev/null @@ -1,26 +0,0 @@ -import { localize } from '@deriv/translations'; -import { generateValidationFunction, getDefaultFields } from '@deriv/shared'; -import ProofOfIdentityForm from './proof-of-identity.jsx'; - -const proof_of_identity_config = { - poi_state: { - supported_in: ['maltainvest', 'malta', 'svg', 'iom'], - default_value: '', - rules: [], - }, -}; - -export const proofOfIdentityConfig = ({ real_account_signup_target }) => { - return { - header: { - active_title: localize('Complete your proof of identity'), - title: localize('Proof of identity'), - }, - body: ProofOfIdentityForm, - form_value: getDefaultFields(real_account_signup_target, proof_of_identity_config), - props: { - validate: generateValidationFunction(real_account_signup_target, proof_of_identity_config), - }, - passthrough: ['refreshNotifications'], - }; -}; diff --git a/packages/core/src/App/Containers/RealAccountSignup/proof-of-identity.jsx b/packages/core/src/App/Containers/RealAccountSignup/proof-of-identity.jsx deleted file mode 100644 index fc2f2650ad2c..000000000000 --- a/packages/core/src/App/Containers/RealAccountSignup/proof-of-identity.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import { AutoHeightWrapper, FormSubmitButton } from '@deriv/components'; - -import { Formik } from 'formik'; -import ProofOfIdentityContainer from '@deriv/account'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { isMobile } from '@deriv/shared'; -import { localize } from '@deriv/translations'; - -const ProofOfIdentityForm = ({ form_error, index, onCancel, onSubmit, value }) => { - const [poi_state, setPoiState] = React.useState('none'); - - const validateForm = () => { - const errors = {}; - if (!['pending', 'verified'].includes(poi_state)) { - errors.poi_state = true; - } - - return errors; - }; - - return ( - onSubmit(index, { poi_state }, actions.setSubmitting)} - > - {({ handleSubmit }) => ( - - {({ setRef, height }) => ( -
-
- - setPoiState({ status })} - is_from_external={true} - /> - -
-
- )} -
- )} -
- ); -}; - -ProofOfIdentityForm.propTypes = { - form_error: PropTypes.string, - index: PropTypes.number, - onCancel: PropTypes.func, - onSubmit: PropTypes.func, - value: PropTypes.object, -}; - -export default ProofOfIdentityForm; diff --git a/packages/core/src/Stores/client-store.js b/packages/core/src/Stores/client-store.js index 900169dcebb3..2b83c542937c 100644 --- a/packages/core/src/Stores/client-store.js +++ b/packages/core/src/Stores/client-store.js @@ -297,7 +297,7 @@ export default class ClientStore extends BaseStore { is_eu_country: computed, is_options_blocked: computed, is_multipliers_only: computed, - is_pending_proof_of_ownership: computed, + is_proof_of_ownership_enabled: computed, resetLocalStorageValues: action.bound, getBasicUpgradeInfo: action.bound, setMT5DisabledSignupTypes: action.bound, @@ -2568,8 +2568,10 @@ export default class ClientStore extends BaseStore { return !!this.accounts[this.loginid]?.residence; } - get is_pending_proof_of_ownership() { - return this.account_status?.authentication?.needs_verification?.includes('ownership'); + get is_proof_of_ownership_enabled() { + if (!this.account_status?.authentication) return false; + const { ownership, needs_verification } = this.account_status.authentication; + return needs_verification?.includes('ownership') || ownership?.status === 'verified'; } fetchFinancialAssessment() { diff --git a/packages/core/src/Stores/notification-store.js b/packages/core/src/Stores/notification-store.js index 12e9799a9fc9..c92bfe1e0899 100644 --- a/packages/core/src/Stores/notification-store.js +++ b/packages/core/src/Stores/notification-store.js @@ -318,7 +318,7 @@ export default class NotificationStore extends BaseStore { is_financial_information_incomplete, has_restricted_mt5_account, has_mt5_account_with_rejected_poa, - is_pending_proof_of_ownership, + is_proof_of_ownership_enabled, p2p_advertiser_info, is_p2p_enabled, is_poa_expired, @@ -433,7 +433,7 @@ export default class NotificationStore extends BaseStore { const poinc_upload_limited = needs_verification.includes('income') && income?.status === 'locked'; const onfido_submissions_left = identity?.services.onfido.submissions_left; const poo_required = ownership?.requests?.length > 0 && ownership?.status?.toLowerCase() !== 'rejected'; - const poo_rejected = is_pending_proof_of_ownership && ownership?.status?.toLowerCase() === 'rejected'; + const poo_rejected = is_proof_of_ownership_enabled && ownership?.status?.toLowerCase() === 'rejected'; const svg_needs_poi_poa = cr_account && status.includes('allow_document_upload') && diff --git a/packages/shared/src/utils/files/file-uploader-utils.ts b/packages/shared/src/utils/files/file-uploader-utils.ts index 7846634f73f0..5c52e25e91dc 100644 --- a/packages/shared/src/utils/files/file-uploader-utils.ts +++ b/packages/shared/src/utils/files/file-uploader-utils.ts @@ -26,7 +26,7 @@ export const compressImageFiles = (files?: File[]) => { const promises: Promise[] = []; Array.from(files).forEach(file => { const promise = new Promise(resolve => { - if (isImageType(file.type)) { + if (isImageType(file?.type)) { convertToBase64(file).then(img => { compressImg(img as TImage).then(resolve); }); @@ -56,7 +56,7 @@ export const readFiles = ( buffer: fr.result, documentFormat: getFormatFromMIME(f), file_size: f.size, - documentType: settings?.document_type ?? DOCUMENT_TYPES.utility_bill, + documentType: settings?.document_type ?? UPLOAD_FILE_TYPE.utility_bill, documentId: settings?.document_id, expirationDate: settings?.expiration_date, lifetimeValid: settings?.lifetime_valid, @@ -89,21 +89,27 @@ export const max_document_size = 8388608; export const supported_filetypes = 'image/png, image/jpeg, image/jpg, image/gif, application/pdf'; -export const getSupportedFiles = (filename: string) => - /^.*\.(png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF|pdf|PDF)$/.test(filename); - -export const DOCUMENT_TYPES = { - passport: 'passport', - national_identity_card: 'national_identity_card', - driving_licence: 'driving_licence', - utility_bill: 'utility_bill', - bankstatement: 'bankstatement', - power_of_attorney: 'power_of_attorney', +export const UPLOAD_FILE_TYPE = Object.freeze({ amlglobalcheck: 'amlglobalcheck', + bankstatement: 'bankstatement', docverification: 'docverification', - proofid: 'proofid', driverslicense: 'driverslicense', - proofaddress: 'proofaddress', - proof_of_ownership: 'proof_of_ownership', + driving_licence: 'driving_licence', + national_identity_card: 'national_identity_card', other: 'other', -} as const; + passport: 'passport', + power_of_attorney: 'power_of_attorney', + proof_of_ownership: 'proof_of_ownership', + proofaddress: 'proofaddress', + proofid: 'proofid', + utility_bill: 'utility_bill', +}); + +export const PAGE_TYPE = Object.freeze({ + back: 'back', + front: 'front', + photo: 'photo', +}); + +export const getSupportedFiles = (filename: string) => + /^.*\.(png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF|pdf|PDF)$/.test(filename); diff --git a/packages/shared/src/utils/validation/declarative-validation-rules.ts b/packages/shared/src/utils/validation/declarative-validation-rules.ts index 29578f216009..07d871131a71 100644 --- a/packages/shared/src/utils/validation/declarative-validation-rules.ts +++ b/packages/shared/src/utils/validation/declarative-validation-rules.ts @@ -56,6 +56,10 @@ const validRegular = (value: string, options: TOptions) => options.regex?.test(v const confirmRequired = (value: string) => !!value; const checkPOBox = (value: string) => !/p[.\s]+o[.\s]+box/i.test(value); const validEmailToken = (value: string) => value.trim().length === 8; +export const hasInvalidCharacters = (target_string: string) => /[^\dX\s]/.test(target_string); +export const isFormattedCardNumber = (target_string: string) => + /(^\d{4})\s(\d{2}X{2})\s(X{4})\s(\d{4}$)/.test(target_string); +export const validFile = (file: File) => file?.type && /(image|application)\/(jpe?g|pdf|png)$/.test(file?.type); let pre_build_dvrs: TInitPreBuildDVRs, form_error_messages: TFormErrorMessagesTypes; diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index c95a6d905fde..afd3722b7d6f 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -152,7 +152,7 @@ const mock = (): TStores & { is_mock: boolean } => { is_logging_in: false, is_mt5_password_not_set: false, is_mt5_account_list_updated: false, - is_pending_proof_of_ownership: false, + is_proof_of_ownership_enabled: false, is_poa_expired: false, is_populating_dxtrade_account_list: false, is_social_signup: false, diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 724cb7b79deb..4ed2f93c7150 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -463,7 +463,7 @@ type TClientStore = { is_low_risk: boolean; is_mt5_password_not_set: boolean; is_mt5_account_list_updated: boolean; - is_pending_proof_of_ownership: boolean; + is_proof_of_ownership_enabled: boolean; is_poa_expired: boolean; is_populating_dxtrade_account_list: boolean; is_switching: boolean;