diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2563643202fb..b40465371db3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -73,7 +73,7 @@ # deriv-app/account # ============================================================== -/packages/account/**/* @matin-deriv @amina-deriv +/packages/account/**/* @matin-deriv @amina-deriv @maryia-deriv # ============================================================== @@ -118,13 +118,13 @@ # ============================================================== /packages/api/**/* @ali-hosseini-deriv @matin-deriv -/packages/core/**/* @ali-hosseini-deriv @matin-deriv -/packages/shared/**/* @ali-hosseini-deriv @matin-deriv -/packages/components/**/* @ali-hosseini-deriv @matin-deriv -/packages/translations/**/* @ali-hosseini-deriv @matin-deriv -/packages/utils/**/* @ali-hosseini-deriv @matin-deriv +/packages/core/**/* @ali-hosseini-deriv @matin-deriv @maryia-deriv +/packages/shared/**/* @ali-hosseini-deriv @matin-deriv @maryia-deriv +/packages/components/**/* @ali-hosseini-deriv @matin-deriv @maryia-deriv +/packages/translations/**/* @ali-hosseini-deriv @matin-deriv @maryia-deriv +/packages/utils/**/* @ali-hosseini-deriv @matin-deriv @maryia-deriv /packages/hooks/**/* @ali-hosseini-deriv @matin-deriv -/packages/stores/**/* @ali-hosseini-deriv @matin-deriv +/packages/stores/**/* @ali-hosseini-deriv @matin-deriv @maryia-deriv # ============================================================== diff --git a/package-lock.json b/package-lock.json index 60d6071a6059..aef2a19e6b57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@datadog/browser-rum": "^4.37.0", "@deriv/api-types": "^1.0.94", "@deriv/deriv-api": "^1.0.11", - "@deriv/deriv-charts": "1.3.1", + "@deriv/deriv-charts": "1.3.2", "@deriv/js-interpreter": "^3.0.0", "@deriv/ui": "^0.6.0", "@livechat/customer-sdk": "^2.0.4", @@ -2894,9 +2894,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@deriv/deriv-charts": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.1.tgz", - "integrity": "sha512-pjtaePIMhe1R5MkbOwnhAFAXGKZb3WI9JtNpgVS3uL8j07kzAzk2SsFOBSNDggwUX1GW1SI/4bPjcDyXTtA2sA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.2.tgz", + "integrity": "sha512-j1xgloqF9jVPiCsfQJGKduivU7r42vQCeT+URCKz82dltlJAw7dmfxY3GgQHjBobkG+SK7ANG/rBzK1vyZn7bA==", "dependencies": { "@welldone-software/why-did-you-render": "^3.3.8", "classnames": "^2.3.1", @@ -51290,9 +51290,9 @@ } }, "@deriv/deriv-charts": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.1.tgz", - "integrity": "sha512-pjtaePIMhe1R5MkbOwnhAFAXGKZb3WI9JtNpgVS3uL8j07kzAzk2SsFOBSNDggwUX1GW1SI/4bPjcDyXTtA2sA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.2.tgz", + "integrity": "sha512-j1xgloqF9jVPiCsfQJGKduivU7r42vQCeT+URCKz82dltlJAw7dmfxY3GgQHjBobkG+SK7ANG/rBzK1vyZn7bA==", "requires": { "@welldone-software/why-did-you-render": "^3.3.8", "classnames": "^2.3.1", diff --git a/packages/account/package.json b/packages/account/package.json index 4e8c7a4c4c3b..e05b3edcab01 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -28,11 +28,11 @@ }, "dependencies": { "@binary-com/binary-document-uploader": "^2.4.8", - "@deriv/api-types": "^1.0.94", + "@deriv/api-types": "^1.0.116", "@deriv/components": "^1.0.0", "@deriv/hooks": "^1.0.0", "@deriv/shared": "^1.0.0", - "@deriv/stores":"^1.0.0", + "@deriv/stores": "^1.0.0", "@deriv/translations": "^1.0.0", "bowser": "^2.9.0", "classnames": "^2.2.6", diff --git a/packages/account/src/App.tsx b/packages/account/src/App.tsx index f658afe8facd..23cbccb72a8e 100644 --- a/packages/account/src/App.tsx +++ b/packages/account/src/App.tsx @@ -17,8 +17,11 @@ const App = ({ passthrough }: TAppProps) => { const { root_store, WS } = passthrough; setWebsocket(WS); + const { notification_messages_ui: Notifications } = root_store.ui; + return ( + {Notifications && } diff --git a/packages/account/src/Components/account-limits/account-limits-article.tsx b/packages/account/src/Components/account-limits/account-limits-article.tsx index ab843d110855..212ffb2c747c 100644 --- a/packages/account/src/Components/account-limits/account-limits-article.tsx +++ b/packages/account/src/Components/account-limits/account-limits-article.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import * as React from 'react'; import { StaticUrl } from '@deriv/components'; import { Localize, localize } from '@deriv/translations'; @@ -29,8 +28,4 @@ const AccountLimitsArticle = ({ is_from_derivgo }: TAccountLimitsArticle) => { return ; }; -AccountLimitsArticle.propTypes = { - is_from_derivgo: PropTypes.bool, -}; - export default AccountLimitsArticle; diff --git a/packages/account/src/Components/forms/idv-form.tsx b/packages/account/src/Components/forms/idv-form.tsx index 48a5a5fd0c1d..961802b60799 100644 --- a/packages/account/src/Components/forms/idv-form.tsx +++ b/packages/account/src/Components/forms/idv-form.tsx @@ -1,35 +1,11 @@ import React from 'react'; import classNames from 'classnames'; -import { Field, FormikProps, FormikHandlers, FieldProps } from 'formik'; -import { ResidenceList } from '@deriv/api-types'; +import { Field, FieldProps } from 'formik'; import { localize } from '@deriv/translations'; -import { formatInput, IDV_NOT_APPLICABLE_OPTION } from '@deriv/shared'; +import { formatInput, getIDVNotApplicableOption } from '@deriv/shared'; import { Autocomplete, DesktopWrapper, Input, MobileWrapper, SelectNative, Text } from '@deriv/components'; import { getDocumentData, preventEmptyClipboardPaste, generatePlaceholderText, getExampleFormat } from 'Helpers/utils'; - -type TDocumentList = Array<{ - id: string; - text: string; - value?: string; - sample_image?: string; - example_format?: string; - additional?: any; -}>; - -type TFormProps = { - document_type: TDocumentList[0]; - document_number: string; - document_additional?: string; - error_message?: string; -}; - -type TIDVForm = { - selected_country: ResidenceList[0]; - hide_hint?: boolean; - class_name?: string; - can_skip_document_verification: boolean; -} & Partial & - FormikProps; +import { TDocumentList, TIDVForm } from 'Types'; const IDVForm = ({ errors, @@ -43,7 +19,7 @@ const IDVForm = ({ hide_hint, can_skip_document_verification = false, }: TIDVForm) => { - const [document_list, setDocumentList] = React.useState([]); + const [document_list, setDocumentList] = React.useState([]); const [document_image, setDocumentImage] = React.useState(null); const [selected_doc, setSelectedDoc] = React.useState(''); @@ -57,6 +33,8 @@ const IDVForm = ({ sample_image: '', }; + const IDV_NOT_APPLICABLE_OPTION = React.useMemo(() => getIDVNotApplicableOption(), []); + React.useEffect(() => { if (document_data && selected_country && selected_country.value) { const document_types = Object.keys(document_data); @@ -100,7 +78,7 @@ const IDVForm = ({ setDocumentList([...new_document_list]); } } - }, [document_data, selected_country, can_skip_document_verification]); + }, [document_data, selected_country, can_skip_document_verification, IDV_NOT_APPLICABLE_OPTION]); const resetDocumentItemSelected = () => { setFieldValue('document_type', default_document, true); @@ -120,7 +98,7 @@ const IDVForm = ({ setFieldValue(document_name, current_input, true); }; - const bindDocumentData = (item: TDocumentList[0]) => { + const bindDocumentData = (item: TDocumentList) => { setFieldValue('document_type', item, true); setSelectedDoc(item?.id); if (item?.id === IDV_NOT_APPLICABLE_OPTION.id) { @@ -171,7 +149,7 @@ const IDVForm = ({ } }} onChange={handleChange} - onItemSelection={(item: TDocumentList[0]) => { + onItemSelection={(item: TDocumentList) => { if ( item.text === 'No results found' || !item.text diff --git a/packages/account/src/Components/personal-details/personal-details.jsx b/packages/account/src/Components/personal-details/personal-details.jsx index dfd5045bee50..4d2823e1e0b4 100644 --- a/packages/account/src/Components/personal-details/personal-details.jsx +++ b/packages/account/src/Components/personal-details/personal-details.jsx @@ -13,7 +13,7 @@ import { isDesktop, isMobile, PlatformContext, - IDV_NOT_APPLICABLE_OPTION, + getIDVNotApplicableOption, removeEmptyPropertiesFromObject, } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; @@ -83,6 +83,7 @@ const PersonalDetails = ({ residence_list, real_account_signup_target, }); + const IDV_NOT_APPLICABLE_OPTION = React.useMemo(() => getIDVNotApplicableOption(), []); const validateIDV = values => { const errors = {}; diff --git a/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx b/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx index 347cb848e396..c81e867290ea 100644 --- a/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx +++ b/packages/account/src/Components/poi/idv-document-submit/idv-document-submit.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Button } from '@deriv/components'; import { Formik } from 'formik'; import { localize } from '@deriv/translations'; import { WS, - IDV_NOT_APPLICABLE_OPTION, + getIDVNotApplicableOption, toMoment, filterObjProperties, isDesktop, removeEmptyPropertiesFromObject, + formatIDVFormValues, } from '@deriv/shared'; import { documentAdditionalError, getRegex, validate, makeSettingsRequest, validateName } from 'Helpers/utils'; import FormFooter from 'Components/form-footer'; @@ -18,6 +18,17 @@ import BackButtonIcon from 'Assets/ic-poi-back-btn.svg'; import IDVForm from 'Components/forms/idv-form'; import PersonalDetailsForm from 'Components/forms/personal-details-form'; import FormSubHeader from 'Components/form-sub-header'; +import { GetSettings, IdentityVerificationAddDocumentResponse, ResidenceList } from '@deriv/api-types'; +import { TIDVFormValues, TInputFieldValues, TDocumentList } from 'Types'; + +type TIDVDocumentSubmitProps = { + account_settings: GetSettings; + getChangeableFields: () => Array; + handleBack: React.MouseEventHandler; + handleViewComplete: () => void; + is_from_external: boolean; + selected_country: ResidenceList[0]; +}; const IdvDocumentSubmit = ({ handleBack, @@ -26,7 +37,7 @@ const IdvDocumentSubmit = ({ is_from_external, account_settings, getChangeableFields, -}) => { +}: TIDVDocumentSubmitProps) => { const visible_settings = ['first_name', 'last_name', 'date_of_birth']; const form_initial_values = filterObjProperties(account_settings, visible_settings) || {}; @@ -48,20 +59,21 @@ const IdvDocumentSubmit = ({ ...form_initial_values, }; - const getExampleFormat = example_format => { + const getExampleFormat = (example_format: string) => { return example_format ? localize('Example: ') + example_format : ''; }; + const IDV_NOT_APPLICABLE_OPTION = React.useMemo(() => getIDVNotApplicableOption(), []); - const shouldHideHelperImage = document_id => document_id === IDV_NOT_APPLICABLE_OPTION.id; + const shouldHideHelperImage = (document_id: string) => document_id === IDV_NOT_APPLICABLE_OPTION.id; - const isDocumentTypeValid = document_type => { + const isDocumentTypeValid = (document_type: TDocumentList) => { if (!document_type?.text) { return localize('Please select a document type.'); } return undefined; }; - const isAdditionalDocumentValid = (document_type, document_additional) => { + const isAdditionalDocumentValid = (document_type: TDocumentList, document_additional: string) => { const error_message = documentAdditionalError(document_additional, document_type.additional?.format); if (error_message) { return localize(error_message) + getExampleFormat(document_type.additional?.example_format); @@ -69,7 +81,7 @@ const IdvDocumentSubmit = ({ return undefined; }; - const isDocumentNumberValid = (document_number, document_type) => { + const isDocumentNumberValid = (document_number: string, document_type: Required) => { const is_document_number_invalid = document_number === document_type.example_format; if (!document_number) { return localize('Please enter your document number. ') + getExampleFormat(document_type.example_format); @@ -83,8 +95,8 @@ const IdvDocumentSubmit = ({ return undefined; }; - const validateFields = values => { - const errors = {}; + const validateFields = (values: TIDVFormValues) => { + const errors: Partial = {}; const { document_type, document_number, document_additional } = values; const needs_additional_document = !!document_type.additional; @@ -127,25 +139,22 @@ const IdvDocumentSubmit = ({ setSubmitting(false); return; } + const submit_data = { identity_verification_document_add: 1, - document_number: values.document_number, - document_additional: values.document_additional || '', - document_type: values.document_type.id, - issuing_country: selected_country.value, + ...formatIDVFormValues(values, selected_country.value), }; - if (submit_data.document_type === IDV_NOT_APPLICABLE_OPTION.id) { - return; - } - WS.send(submit_data).then(response => { - setSubmitting(false); - if (response.error) { - setErrors({ error_message: response.error.message }); - return; + WS.send(submit_data).then( + (response: IdentityVerificationAddDocumentResponse & { error: { message: string } }) => { + setSubmitting(false); + if (response.error) { + setErrors({ error_message: response.error.message }); + return; + } + handleViewComplete(); } - handleViewComplete(); - }); + ); }; return ( @@ -225,13 +234,4 @@ const IdvDocumentSubmit = ({ ); }; -IdvDocumentSubmit.propTypes = { - account_settings: PropTypes.object, - getChangeableFields: PropTypes.func, - handleBack: PropTypes.func, - handleViewComplete: PropTypes.func, - is_from_external: PropTypes.bool, - selected_country: PropTypes.object, -}; - export default IdvDocumentSubmit; diff --git a/packages/account/src/Constants/idv-document-config.ts b/packages/account/src/Constants/idv-document-config.ts new file mode 100644 index 000000000000..2202ab910848 --- /dev/null +++ b/packages/account/src/Constants/idv-document-config.ts @@ -0,0 +1,128 @@ +import { getUrlBase } from '@deriv/shared'; +import { localize } from '@deriv/translations'; + +const getImageLocation = (image_name: string) => getUrlBase(`/public/images/common/${image_name}`); + +// Note: Ensure that the object keys matches BE API's keys. This is simply a mapping for FE templates + +export const getIDVDocumentConfig = () => ({ + ke: { + alien_card: { + new_display_name: '', + example_format: '123456', + sample_image: getImageLocation('ke_alien_card.png'), + }, + national_id: { + new_display_name: '', + example_format: '12345678', + sample_image: getImageLocation('ke_national_identity_card.png'), + }, + passport: { + new_display_name: '', + example_format: 'A12345678', + sample_image: getImageLocation('ke_passport.png'), + }, + }, + za: { + national_id: { + new_display_name: localize('National ID'), + example_format: '1234567890123', + sample_image: getImageLocation('za_national_identity_card.png'), + }, + national_id_no_photo: { + new_display_name: localize('National ID (No Photo)'), + example_format: '1234567890123', + sample_image: '', + }, + }, + ng: { + bvn: { + new_display_name: localize('Bank Verification Number'), + example_format: '12345678901', + sample_image: '', + }, + cac: { + new_display_name: localize('Corporate Affairs Commission'), + example_format: '12345678', + sample_image: '', + }, + drivers_license: { + new_display_name: '', + example_format: 'ABC123456789', + sample_image: getImageLocation('ng_drivers_license.png'), + }, + nin: { + new_display_name: localize('National Identity Number'), + example_format: '12345678901', + sample_image: '', + }, + nin_slip: { + new_display_name: localize('National Identity Number Slip'), + example_format: '12345678901', + sample_image: getImageLocation('ng_nin_slip.png'), + }, + tin: { + new_display_name: localize('Taxpayer identification number'), + example_format: '12345678-1234', + sample_image: '', + }, + voter_id: { + new_display_name: localize('Voter ID'), + example_format: '1234567890123456789', + sample_image: getImageLocation('ng_voter_id.png'), + }, + }, + gh: { + drivers_license: { + new_display_name: '', + example_format: 'B1234567', + sample_image: '', + }, + national_id: { + new_display_name: localize('National ID'), + example_format: 'GHA-123456789-1', + sample_image: '', + }, + passport: { + new_display_name: localize('Passport'), + example_format: 'G1234567', + sample_image: '', + }, + ssnit: { + new_display_name: localize('Social Security and National Insurance Trust'), + example_format: 'C123456789012', + sample_image: '', + }, + voter_id: { + new_display_name: localize('Voter ID'), + example_format: '01234567890', + sample_image: '', + }, + }, + br: { + cpf: { + new_display_name: localize('CPF'), + example_format: '123.456.789-12', + sample_image: '', + }, + }, + ug: { + national_id: { + new_display_name: localize('National ID'), + example_format: 'CM12345678PE1D', + sample_image: getImageLocation('ug_national_identity_card.png'), + }, + national_id_no_photo: { + new_display_name: localize('National ID (No Photo)'), + example_format: 'CM12345678PE1D', + sample_image: '', + }, + }, + zw: { + national_id: { + new_display_name: localize('National ID'), + example_format: '081234567F53', + sample_image: getImageLocation('zw_national_identity_card.png'), + }, + }, +}); diff --git a/packages/account/src/Helpers/utils.ts b/packages/account/src/Helpers/utils.ts index 68484b8ec624..581e4ae4b117 100644 --- a/packages/account/src/Helpers/utils.ts +++ b/packages/account/src/Helpers/utils.ts @@ -1,16 +1,8 @@ -import { - getUrlBase, - filterObjProperties, - toMoment, - validLength, - validName, - IDV_NOT_APPLICABLE_OPTION, -} from '@deriv/shared'; +import { filterObjProperties, toMoment, validLength, validName, getIDVNotApplicableOption } from '@deriv/shared'; import { localize } from '@deriv/translations'; import { ResidenceList, GetSettings, GetAccountStatus } from '@deriv/api-types'; -import { FormikErrors, FormikValues } from 'formik'; - -const getImageLocation = (image_name: string) => getUrlBase(`/public/images/common/${image_name}`); +import { FormikValues } from 'formik'; +import { getIDVDocumentConfig } from '../Constants/idv-document-config'; export const documentAdditionalError = (document_additional: string, document_additional_format: string) => { let error_message = null; @@ -35,128 +27,7 @@ const regex = [ }, ]; -// Note: Ensure that the object keys matches BE API's keys. This is simply a mapping for FE templates -const idv_document_data = Object.freeze({ - ke: { - alien_card: { - new_display_name: '', - example_format: '123456', - sample_image: getImageLocation('ke_alien_card.png'), - }, - national_id: { - new_display_name: '', - example_format: '12345678', - sample_image: getImageLocation('ke_national_identity_card.png'), - }, - passport: { - new_display_name: '', - example_format: 'A12345678', - sample_image: getImageLocation('ke_passport.png'), - }, - }, - za: { - national_id: { - new_display_name: localize('National ID'), - example_format: '1234567890123', - sample_image: getImageLocation('za_national_identity_card.png'), - }, - national_id_no_photo: { - new_display_name: localize('National ID (No Photo)'), - example_format: '1234567890123', - sample_image: '', - }, - }, - ng: { - bvn: { - new_display_name: localize('Bank Verification Number'), - example_format: '12345678901', - sample_image: '', - }, - cac: { - new_display_name: localize('Corporate Affairs Commission'), - example_format: '12345678', - sample_image: '', - }, - drivers_license: { - new_display_name: '', - example_format: 'ABC123456789', - sample_image: getImageLocation('ng_drivers_license.png'), - }, - nin: { - new_display_name: localize('National Identity Number'), - example_format: '12345678901', - sample_image: '', - }, - nin_slip: { - new_display_name: localize('National Identity Number Slip'), - example_format: '12345678901', - sample_image: getImageLocation('ng_nin_slip.png'), - }, - tin: { - new_display_name: localize('Taxpayer identification number'), - example_format: '12345678-1234', - sample_image: '', - }, - voter_id: { - new_display_name: localize('Voter ID'), - example_format: '1234567890123456789', - sample_image: getImageLocation('ng_voter_id.png'), - }, - }, - gh: { - drivers_license: { - new_display_name: '', - example_format: 'B1234567', - sample_image: '', - }, - national_id: { - new_display_name: localize('National ID'), - example_format: 'GHA-123456789-1', - sample_image: '', - }, - passport: { - new_display_name: localize('Passport'), - example_format: 'G1234567', - sample_image: '', - }, - ssnit: { - new_display_name: localize('Social Security and National Insurance Trust'), - example_format: 'C123456789012', - sample_image: '', - }, - voter_id: { - new_display_name: localize('Voter ID'), - example_format: '01234567890', - sample_image: '', - }, - }, - br: { - cpf: { - new_display_name: localize('CPF'), - example_format: '123.456.789-12', - sample_image: '', - }, - }, - ug: { - national_id: { - new_display_name: localize('National ID'), - example_format: 'CM12345678PE1D', - sample_image: getImageLocation('ug_national_identity_card.png'), - }, - national_id_no_photo: { - new_display_name: localize('National ID (No Photo)'), - example_format: 'CM12345678PE1D', - sample_image: '', - }, - }, - zw: { - national_id: { - new_display_name: localize('National ID'), - example_format: '081234567F53', - sample_image: getImageLocation('zw_national_identity_card.png'), - }, - }, -}); +const IDV_NOT_APPLICABLE_OPTION = getIDVNotApplicableOption(); type TIDVSupportCheck = { residence_list: ResidenceList; @@ -183,9 +54,10 @@ export const shouldShowIdentityInformation = ({ }; export const getDocumentData = (country_code: string, document_type: string) => { + const IDV_DOCUMENT_DATA = getIDVDocumentConfig(); return ( - (Object.keys(idv_document_data).includes(country_code) && - (idv_document_data as any)[country_code][document_type]) || { + (Object.keys(IDV_DOCUMENT_DATA).includes(country_code) && + (IDV_DOCUMENT_DATA as any)[country_code][document_type]) || { new_display_name: '', example_format: '', sample_image: '', @@ -261,7 +133,7 @@ export const validateName = (name: string) => { return ''; }; -export const getExampleFormat = (example_format: string) => +export const getExampleFormat = (example_format: string | undefined) => example_format ? localize('Example: ') + example_format : ''; export const isDocumentTypeValid = (document_type: FormikValues) => { diff --git a/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx b/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx index 9632ae82057b..8b45270125a4 100644 --- a/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx +++ b/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx @@ -104,7 +104,7 @@ export const PersonalDetailsForm = observer(({ history }) => { const [is_btn_loading, setIsBtnLoading] = React.useState(false); const [is_submit_success, setIsSubmitSuccess] = useStateCallback(false); - const { client, notifications, ui, common } = useStore(); + const { client, notifications, common } = useStore(); const { authentication_status, @@ -132,7 +132,6 @@ export const PersonalDetailsForm = observer(({ history }) => { showPOAAddressMismatchFailureNotification, } = notifications; - const { Notifications } = ui; const { is_language_changing } = common; const is_mf = landing_company_shortcode === 'maltainvest'; const has_poa_address_mismatch = account_status.status?.includes('poa_address_mismatch'); @@ -561,7 +560,6 @@ export const PersonalDetailsForm = observer(({ history }) => { dirty, }) => ( - {Notifications && } {show_form && (
{ /(?[0-9a-zA-Z-]+\s[0-9:]+GMT)[\s](IP=)(?[\w:.]+)\sIP_COUNTRY=(?([a-zA-Z]{2}))\s(User_AGENT=)(\w.*)(?iPhone|Android)([\W\w]+)\s(?Deriv P2P|Deriv GO)(?[\w\W]+)\s(LANG=)([\w]{2})/ ); const date = environment_split[0]; - const time = environment_split[1].replace('GMT', ''); - const date_time = convertDateFormat(`${date} ${time}`, 'D-MMMM-YY hh:mm:ss', 'YYYY-MM-DD hh:mm:ss'); - data[i].date = `${date_time} GMT`; + const time = environment_split[1].replace('GMT', ' GMT'); + data[i].date = `${moment(date).format('YYYY-MM-DD')} ${time}`; data[i].action = login_history[i].action === 'login' ? localize('Login') : localize('Logout'); const user_agent = environment.substring(environment.indexOf('User_AGENT'), environment.indexOf('LANG')); const ua = mobile_app_UA ? mobile_app_UA.groups : Bowser.getParser(user_agent)?.getBrowser(); diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx index afada8e544c8..e9663fed49ac 100644 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx +++ b/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx @@ -19,11 +19,10 @@ import TwoFactorAuthenticationArticle from './two-factor-authentication-article. import { observer, useStore } from '@deriv/stores'; const TwoFactorAuthentication = observer(() => { - const { client, ui, common } = useStore(); + const { client, common } = useStore(); const { email_address, getTwoFAStatus, has_enabled_two_fa, is_switching, setTwoFAStatus, setTwoFAChangedStatus } = client; const { is_language_changing } = common; - const { notification_messages_ui: Notifications } = ui; const [is_loading, setLoading] = React.useState(true); const [is_qr_loading, setQrLoading] = React.useState(false); const [error_message, setErrorMessage] = React.useState(''); @@ -198,7 +197,6 @@ const TwoFactorAuthentication = observer(() => { 'two-factor__wrapper-dashboard': is_appstore, })} > - {Notifications && } {has_enabled_two_fa ? TwoFactorEnabled : TwoFactorDisabled} diff --git a/packages/account/src/Sections/Verification/ProofOfIdentity/__tests__/proof-of-identity-container.spec.js b/packages/account/src/Sections/Verification/ProofOfIdentity/__tests__/proof-of-identity-container.spec.js new file mode 100644 index 000000000000..8603b4187173 --- /dev/null +++ b/packages/account/src/Sections/Verification/ProofOfIdentity/__tests__/proof-of-identity-container.spec.js @@ -0,0 +1,304 @@ +import React from 'react'; +import { render, screen, act, waitFor } from '@testing-library/react'; +import ProofOfIdentityContainer from '../proof-of-identity-container'; +import { populateVerificationStatus } from '../../Helpers/verification.js'; +import { identity_status_codes, service_code } from '../proof-of-identity-utils'; + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + WS: { + authorized: { + getAccountStatus: jest.fn().mockResolvedValue({ get_account_status: 1 }), + }, + }, +})); + +jest.mock('@deriv/components', () => ({ + ...jest.requireActual('@deriv/components'), + Loading: jest.fn(() => 'mockedLoading'), +})); + +jest.mock('onfido-sdk-ui', () => ({ + init: jest.fn().mockResolvedValue({}), +})); + +jest.mock('../../Helpers/verification.js', () => ({ + populateVerificationStatus: jest.fn().mockReturnValue({ + is_age_verified: false, + }), +})); + +jest.mock('Components/demo-message', () => jest.fn(() => 'mockedDemoMessage')); +jest.mock('Sections/Verification/ProofOfIdentity/idv.jsx', () => jest.fn(() => 'mockedIDV')); +jest.mock('Sections/Verification/ProofOfIdentity/onfido.jsx', () => jest.fn(() => 'mockedOnfido')); +jest.mock('Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx', () => + jest.fn(() => 'mockedProofOfIdentitySubmission') +); +jest.mock('Components/poi/status/unsupported', () => jest.fn(() => 'mockedUnsupported')); +jest.mock('Components/poi/status/not-required', () => jest.fn(() => 'mockedNotRequired')); +jest.mock('Components/error-component', () => jest.fn(() => 'mockedErrorMessage')); +jest.mock('Components/poi/status/upload-complete', () => jest.fn(() => 'mockedUploadComplete')); +jest.mock('Components/poi/status/verified', () => jest.fn(() => 'mockedVerified')); +jest.mock('Components/poi/status/limited', () => jest.fn(() => 'mockedLimited')); +jest.mock('Components/poi/status/expired', () => jest.fn(() => 'mockedExpired')); + +const mock_props = { + account_settings: {}, + account_status: { + authentication: { + attempts: { + count: 1, + history: [ + { + country_code: 'id', + id: '8919', + service: 'manual', + status: 'verified', + timestamp: 1674633681, + }, + ], + latest: { + country_code: 'id', + id: '8919', + service: 'manual', + status: 'verified', + timestamp: 1674633681, + }, + }, + document: { + status: 'verified', + }, + + identity: { + services: { + idv: { + last_rejected: [], + reported_properties: {}, + status: 'none', + submissions_left: 3, + }, + manual: { + status: 'none', + }, + onfido: { + country_code: 'IDN', + documents_supported: [ + 'Driving Licence', + 'National Identity Card', + 'Passport', + 'Residence Permit', + ], + is_country_supported: 1, + last_rejected: [], + reported_properties: {}, + status: 'none', + submissions_left: 3, + }, + }, + status: 'verified', + }, + income: { + status: 'none', + }, + needs_verification: [], + ownership: { + requests: [], + status: 'none', + }, + }, + currency_config: { + USD: { + is_deposit_suspended: 0, + is_withdrawal_suspended: 0, + }, + }, + p2p_status: 'none', + prompt_client_to_authenticate: 0, + risk_classification: 'low', + status: [ + 'age_verification', + 'allow_document_upload', + 'authenticated', + 'dxtrade_password_not_set', + 'financial_information_not_complete', + 'idv_disallowed', + 'mt5_password_not_set', + 'trading_experience_not_complete', + ], + }, + app_routing_history: [ + { + pathname: '/account/proof-of-identity', + }, + ], + fetchResidenceList: jest.fn().mockResolvedValue({ + residence_list: [], + }), + getChangeableFields: [], + is_from_external: false, + is_switching: false, + is_virtual: false, + is_high_risk: false, + is_withdrawal_lock: false, + onStateChange: jest.fn(), + refreshNotifications: jest.fn(), + routeBackInApp: jest.fn(), + should_allow_authentication: false, + setIsCfdPoiCompleted: jest.fn(), + updateAccountStatus: jest.fn(), +}; + +describe('ProofOfIdentityContainer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render proof of identity container with loader', async () => { + const new_props = { + ...mock_props, + is_switching: true, + }; + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedLoading')).toBeInTheDocument(); + }); + + it('should render message when account is virtual', async () => { + const new_props = { + ...mock_props, + is_virtual: true, + }; + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedDemoMessage')).toBeInTheDocument(); + }); + + it('should render API error message returned in response', async () => { + const new_props = { + ...mock_props, + fetchResidenceList: jest.fn().mockResolvedValue({ + error: { + message: 'API error', + }, + }), + }; + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedErrorMessage')).toBeInTheDocument(); + }); + + it('should render messages that POA is not required', async () => { + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedNotRequired')).toBeInTheDocument(); + }); + + it('should render POI submission section when status is none', async () => { + populateVerificationStatus.mockReturnValue({ + identity_status: identity_status_codes.none, + is_age_verified: true, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedProofOfIdentitySubmission')).toBeInTheDocument(); + }); + + it('should render POI submission section when allow_poi_resubmission is set', async () => { + populateVerificationStatus.mockReturnValue({ + allow_poi_resubmission: true, + is_age_verified: true, + identity_status: identity_status_codes.verified, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedProofOfIdentitySubmission')).toBeInTheDocument(); + }); + + it('should render Upload complete section when status is pending', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: null, + is_age_verified: true, + identity_status: identity_status_codes.pending, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedUploadComplete')).toBeInTheDocument(); + }); + + it('should render Verified section when status is verified', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: null, + is_age_verified: true, + identity_status: identity_status_codes.verified, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedVerified')).toBeInTheDocument(); + }); + + it('should render Expired section when status is expired', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: null, + is_age_verified: true, + identity_status: identity_status_codes.expired, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedExpired')).toBeInTheDocument(); + }); + + it('should render Limited section when status is rejected', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: null, + is_age_verified: true, + identity_status: identity_status_codes.rejected, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedLimited')).toBeInTheDocument(); + }); + + it('should render Onfido section when there was a previous onfido submission', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: { service: service_code.onfido }, + is_age_verified: true, + identity_status: identity_status_codes.rejected, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedOnfido')).toBeInTheDocument(); + }); + + it('should render IDV section when there was a previous IDV submission', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: { service: service_code.idv }, + is_age_verified: true, + identity_status: identity_status_codes.rejected, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedIDV')).toBeInTheDocument(); + }); + + it('should render Manual section when there was a previous manual submission', async () => { + populateVerificationStatus.mockReturnValue({ + identity_last_attempt: { service: service_code.manual }, + is_age_verified: true, + identity_status: identity_status_codes.rejected, + }); + + render(); + await waitFor(() => {}); + expect(screen.getByText('mockedUnsupported')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-container.jsx b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-container.jsx index a77b3ea5d4d9..f9fc84bb6fe4 100644 --- a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-container.jsx +++ b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-container.jsx @@ -98,9 +98,6 @@ const ProofOfIdentityContainer = ({ needs_poa, onfido, } = verification_status; - const last_attempt_status = identity_last_attempt?.status; - const is_last_attempt_idv = identity_last_attempt?.service === 'idv'; - const is_last_attempt_onfido = identity_last_attempt?.service === 'onfido'; const should_ignore_idv = is_high_risk && is_withdrawal_lock; if (!should_allow_authentication && !is_age_verified) { @@ -124,13 +121,7 @@ const ProofOfIdentityContainer = ({ ); - if ( - identity_status === identity_status_codes.none || - has_require_submission || - allow_poi_resubmission || - (should_ignore_idv && is_last_attempt_idv && manual?.status !== 'verified' && manual?.status !== 'pending') || - (should_ignore_idv && is_last_attempt_onfido && last_attempt_status === 'rejected') - ) { + if (identity_status === identity_status_codes.none || has_require_submission || allow_poi_resubmission) { return ( { setSubmitting(true); - const { document_number, document_type } = values; const request = makeSettingsRequest(values, [...getChangeableFields()]); @@ -72,16 +70,9 @@ const POISubmissionForMT5 = ({ const submit_data = { identity_verification_document_add: 1, - document_number, - document_type: document_type.id, - issuing_country: citizen_data.value, + ...formatIDVFormValues(values, citizen_data.value), }; - if (submit_data.document_type === IDV_NOT_APPLICABLE_OPTION.id) { - handlePOIComplete(); - return; - } - WS.send(submit_data).then(response => { setSubmitting(false); if (response.error) { diff --git a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx index 81e2c69d245f..3f4efcae812f 100644 --- a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx +++ b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity-submission.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import React from 'react'; import { WS } from '@deriv/shared'; import CountrySelector from 'Components/poi/poi-country-selector'; diff --git a/packages/account/src/Types/common-prop.type.ts b/packages/account/src/Types/common-prop.type.ts index 204e11a8985e..5298105baa8b 100644 --- a/packages/account/src/Types/common-prop.type.ts +++ b/packages/account/src/Types/common-prop.type.ts @@ -1,6 +1,6 @@ /** Add types that are shared between components */ -import { FormikProps, FormikValues } from 'formik'; -import { Authorize, ResidenceList } from '@deriv/api-types'; +import { FormikHandlers, FormikProps, FormikValues } from 'formik'; +import { Authorize, IdentityVerificationAddDocumentResponse, ResidenceList } from '@deriv/api-types'; import { Redirect } from 'react-router-dom'; export type TToken = { @@ -150,6 +150,54 @@ export type TPersonalDetailsForm = { export type TInputFieldValues = Record; +export type TIDVVerificationResponse = IdentityVerificationAddDocumentResponse & { error: { message: string } }; + +export type TDocumentList = { + id: string; + text: string; + value?: string; + sample_image?: string; + example_format?: string; + additional?: { + display_name?: string; + example_format?: string; + }; +}; + +type TFormProps = { + document_type: TDocumentList; + document_number: string; + document_additional?: string; + error_message?: string; +}; + +export type TIDVForm = { + selected_country: ResidenceList[0]; + hide_hint?: boolean; + class_name?: string; + can_skip_document_verification: boolean; +} & Partial & + FormikProps; + export type TVerificationStatus = Readonly< Record<'none' | 'pending' | 'rejected' | 'verified' | 'expired' | 'suspected', string> >; + +type TDocumentList = Array<{ + id: string; + text: string; + value?: string; + sample_image?: string; + example_format?: string; + additional?: { + display_name: string; + format: string; + }; +}>; + +export type TIDVFormValues = { + document_type: TDocumentList[0]; + document_number: string; + document_additional?: string; + error_message?: string; +}; diff --git a/packages/api/package.json b/packages/api/package.json index 603e827785a0..3e0a17020cf0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -10,7 +10,7 @@ "@tanstack/react-query-devtools": "^4.28.0" }, "devDependencies": { - "@deriv/api-types": "^1.0.94", + "@deriv/api-types": "^1.0.116", "@testing-library/react": "^12.0.0", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", diff --git a/packages/api/src/__tests__/usePaginatedFetch.spec.tsx b/packages/api/src/__tests__/usePaginatedFetch.spec.tsx new file mode 100644 index 000000000000..70943d802d3c --- /dev/null +++ b/packages/api/src/__tests__/usePaginatedFetch.spec.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { TSocketResponse } from '../../types'; +import APIProvider from '../APIProvider'; +import usePaginatedFetch from '../usePaginatedFetch'; + +jest.mock('@deriv/shared', () => ({ + WS: { + send: jest.fn(() => + Promise.resolve>({ + p2p_advert_list: { + list: [ + // @ts-expect-error need to come up with a way to mock the return type of useFetch + { + account_currency: 'USD', + amount: 50, + amount_display: '50.00', + }, + ], + }, + echo_req: {}, + msg_type: 'p2p_advert_list', + req_id: 1, + }) + ), + }, +})); + +describe('usePaginatedFetch', () => { + it('should call p2p_advert_list and get data in response', async () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + + const { result, waitFor } = renderHook(() => usePaginatedFetch('p2p_advert_list'), { wrapper }); + + await waitFor(() => result.current.isSuccess, { timeout: 10000 }); + + const adverts_list = result.current.data?.p2p_advert_list?.list; + + expect(adverts_list).toHaveLength(1); + expect(adverts_list?.[0].amount).toBe(50); + expect(adverts_list?.[0].account_currency).toBe('USD'); + expect(adverts_list?.[0].amount_display).toBe('50.00'); + }); +}); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index f0ccb910ded4..c57ebbc8d920 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,4 +2,5 @@ export { default as APIProvider } from './APIProvider'; export { default as useFetch } from './useFetch'; export { default as useInvalidateQuery } from './useInvalidateQuery'; export { default as useRequest } from './useRequest'; +export { default as usePaginatedFetch } from './usePaginatedFetch'; export { default as useSubscription } from './useSubscription'; diff --git a/packages/api/src/usePaginatedFetch.ts b/packages/api/src/usePaginatedFetch.ts new file mode 100644 index 000000000000..98e57cc90ef3 --- /dev/null +++ b/packages/api/src/usePaginatedFetch.ts @@ -0,0 +1,47 @@ +import { useCallback, useState } from 'react'; +import useFetch from './useFetch'; +import { + TSocketAcceptableProps, + TSocketRequestPayload, + TSocketRequestQueryOptions, + TSocketPaginateableEndpointNames, +} from '../types'; + +const usePaginatedFetch = ( + name: T, + ...props: TSocketAcceptableProps +) => { + const prop = props?.[0]; + const payload = prop && 'payload' in prop ? (prop.payload as TSocketRequestPayload) : undefined; + const options = prop && 'options' in prop ? (prop.options as TSocketRequestQueryOptions) : undefined; + + // @ts-expect-error The `limit` parameter is always present in + // the `payload` for the paginateable endpoints. + const limit: number = payload?.payload?.limit || 10; + // @ts-expect-error The `offset` parameter is always present in + // the `payload` for the paginateable endpoints. + const [offset, setOffset] = useState(payload?.payload?.offset || 0); + + // @ts-expect-error It's safe to ignore the TS error here since the + // exact type of the payload is not determined at this point. + const { remove, ...rest } = useFetch(name, { + payload: { ...payload, offset, limit }, + options: { ...options, keepPreviousData: !!offset }, + }); + + const loadMore = useCallback(() => setOffset(prev => prev + limit), [limit]); + + const reset = useCallback(() => { + remove(); + setOffset(0); + }, [remove]); + + return { + ...rest, + remove, + loadMore, + reset, + }; +}; + +export default usePaginatedFetch; diff --git a/packages/api/types.ts b/packages/api/types.ts index dacad83af719..500ba46ebe9f 100644 --- a/packages/api/types.ts +++ b/packages/api/types.ts @@ -863,3 +863,8 @@ export type TSocketAcceptableProps> extends TSocketRequestProps ? [TSocketRequestProps?] : [TSocketRequestProps]; + +export type TSocketPaginateableEndpointNames = KeysMatching< + TSocketEndpoints, + { request: { limit?: number; offset?: number } } +>; diff --git a/packages/appstore/package.json b/packages/appstore/package.json index c41dc858fab8..68143a7d4bd4 100644 --- a/packages/appstore/package.json +++ b/packages/appstore/package.json @@ -25,7 +25,7 @@ "license": "Apache-2.0", "dependencies": { "@deriv/account": "^1.0.0", - "@deriv/api-types": "^1.0.94", + "@deriv/api-types": "^1.0.116", "@deriv/cashier": "^1.0.0", "@deriv/components": "^1.0.0", "@deriv/cfd": "^1.0.0", diff --git a/packages/appstore/src/components/cfds-listing/__tests__/index.spec.tsx b/packages/appstore/src/components/cfds-listing/__tests__/index.spec.tsx new file mode 100644 index 000000000000..bdd7c41d2589 --- /dev/null +++ b/packages/appstore/src/components/cfds-listing/__tests__/index.spec.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import CFDsListing from '../index'; + +jest.mock('Components/containers/listing-container', () => + jest.fn(({ children }) =>
{children}
) +); + +describe('CFDsListing', () => { + const mock = mockStore({ + traders_hub: { + selected_region: 'NON-EU', + has_any_real_account: true, + is_real: true, + can_get_more_cfd_mt5_accounts: true, + no_MF_account: true, + is_demo_low_risk: true, + }, + client: { + is_landing_company_loaded: true, + real_account_creation_unlock_date: '2022-02-02', + }, + modules: { + cfd: { + toggleCompareAccountsModal: jest.fn(), + setAccountType: jest.fn(), + }, + }, + }); + + it('should render the component', () => { + const wrapper = ({ children }: { children: JSX.Element }) => ( + {children} + ); + + render(, { wrapper }); + expect(screen.getByTestId('listing-container')).toBeInTheDocument(); + }); +}); diff --git a/packages/appstore/src/components/cfds-listing/index.tsx b/packages/appstore/src/components/cfds-listing/index.tsx index 2b2986b48f75..bbe800358463 100644 --- a/packages/appstore/src/components/cfds-listing/index.tsx +++ b/packages/appstore/src/components/cfds-listing/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { observer } from 'mobx-react-lite'; +import { observer, useStore } from '@deriv/stores'; import { Text, StaticUrl } from '@deriv/components'; import { isMobile, formatMoney, getAuthenticationStatusInfo, Jurisdiction } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; @@ -10,7 +10,6 @@ import PlatformLoader from 'Components/pre-loader/platform-loader'; import GetMoreAccounts from 'Components/get-more-accounts'; import { Actions } from 'Components/containers/trading-app-card-actions'; import { getHasDivider } from 'Constants/utils'; -import { useStores } from 'Stores/index'; import { AvailableAccount, TDetailsOfEachMT5Loginid } from 'Types'; import './cfds-listing.scss'; @@ -20,14 +19,14 @@ type TDetailedExistingAccount = AvailableAccount & key: string; }; -const CFDsListing = () => { +const CFDsListing = observer(() => { const { client, modules: { cfd }, traders_hub, common, ui, - } = useStores(); + } = useStore(); const { available_dxtrade_accounts, available_derivez_accounts, @@ -62,8 +61,12 @@ const CFDsListing = () => { const accounts_sub_text = !is_eu_user || is_demo_low_risk ? localize('Compare accounts') : localize('Account Information'); - const { poi_pending_for_bvi_labuan, poi_resubmit_for_bvi_labuan, poa_resubmit_for_labuan, is_idv_revoked } = - getAuthenticationStatusInfo(account_status); + const { + poi_pending_for_bvi_labuan_vanuatu, + poi_resubmit_for_bvi_labuan_vanuatu, + poa_resubmit_for_labuan, + is_idv_revoked, + } = getAuthenticationStatusInfo(account_status); const getAuthStatus = (status_list: boolean[]) => status_list.some(status => status); @@ -74,13 +77,16 @@ const CFDsListing = () => { if ( getAuthStatus([ is_idv_revoked, - poi_resubmit_for_bvi_labuan, + poi_resubmit_for_bvi_labuan_vanuatu, current_acc_status === 'proof_failed', ]) ) { return 'failed'; } else if ( - getAuthStatus([poi_pending_for_bvi_labuan, current_acc_status === 'verification_pending']) + getAuthStatus([ + poi_pending_for_bvi_labuan_vanuatu, + current_acc_status === 'verification_pending', + ]) ) { return 'pending'; } @@ -91,20 +97,27 @@ const CFDsListing = () => { getAuthStatus([ poa_resubmit_for_labuan, is_idv_revoked, - poi_resubmit_for_bvi_labuan, + poi_resubmit_for_bvi_labuan_vanuatu, current_acc_status === 'proof_failed', ]) ) { return 'failed'; } else if ( - getAuthStatus([poi_pending_for_bvi_labuan, current_acc_status === 'verification_pending']) + getAuthStatus([ + poi_pending_for_bvi_labuan_vanuatu, + current_acc_status === 'verification_pending', + ]) ) { return 'pending'; } return null; } default: - return null; + if (current_acc_status === 'proof_failed') { + return 'failed'; + } else if (current_acc_status === 'verification_pending') { + return 'pending'; + } } } return null; @@ -183,7 +196,7 @@ const CFDsListing = () => { existing_account.status || is_idv_revoked ? getMT5AccountAuthStatus( existing_account.status, - existing_account?.short_code_and_region?.toLowerCase() + existing_account?.landing_company_short ) : null; @@ -389,6 +402,6 @@ const CFDsListing = () => { : !is_real && } */} ); -}; +}); -export default observer(CFDsListing); +export default CFDsListing; diff --git a/packages/appstore/src/constants/platform-config.ts b/packages/appstore/src/constants/platform-config.ts index 961a11fb1326..cba13cf731b9 100644 --- a/packages/appstore/src/constants/platform-config.ts +++ b/packages/appstore/src/constants/platform-config.ts @@ -41,6 +41,7 @@ export const getAppstorePlatforms = (): PlatformConfig[] => [ name: getPlatformSettingsAppstore('dbot').name, app_desc: localize('Automate your trading, no coding needed.'), link_to: routes.bot, + is_external: true, }, { name: getPlatformSettingsAppstore('smarttrader').name, diff --git a/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_multiplier.js b/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_multiplier.js index 2f95f7ea648e..7c7da79c804e 100644 --- a/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_multiplier.js +++ b/packages/bot-skeleton/src/scratch/blocks/Binary/Trade Definition/trade_definition_multiplier.js @@ -80,6 +80,20 @@ Blockly.Blocks.trade_definition_multiplier = { const block_types_in_multiplier = []; blocks_in_multiplier.forEach(block => { block_types_in_multiplier.push(block.type); + const block_multiplier_take_profit = block.childValueToCode('multiplier_take_profit', 'AMOUNT'); + const block_multiplier_stop_loss = block.childValueToCode('multiplier_stop_loss', 'AMOUNT'); + if (block_multiplier_take_profit <= 0 || block_multiplier_stop_loss <= 0) { + block.setDisabled(true); + } + + if (block.type === 'multiplier_stop_loss' && block_multiplier_stop_loss > 0) { + block.setDisabled(false); + } + + if (block.type === 'multiplier_take_profit' && block_multiplier_take_profit > 0) { + block.setDisabled(false); + } + if ( !/^multiplier_.+$/.test(block.type) || new Set(block_types_in_multiplier).size !== block_types_in_multiplier.length diff --git a/packages/bot-skeleton/src/services/api/active-symbols.js b/packages/bot-skeleton/src/services/api/active-symbols.js index acde7caa0e8c..3d0bd2bbe3c2 100644 --- a/packages/bot-skeleton/src/services/api/active-symbols.js +++ b/packages/bot-skeleton/src/services/api/active-symbols.js @@ -7,7 +7,7 @@ export default class ActiveSymbols { this.active_symbols = []; this.disabled_markets = []; this.disabled_symbols = ['frxGBPNOK', 'frxUSDNOK', 'frxUSDNEK', 'frxUSDSEK']; // These are only forward-starting. - this.disabled_submarkets = ['energy', 'step_index', 'crash_index']; + this.disabled_submarkets = ['energy', 'step_index']; this.init_promise = new PendingPromise(); this.is_initialised = false; this.processed_symbols = {}; diff --git a/packages/bot-web-ui/package.json b/packages/bot-web-ui/package.json index 57765bbc5db1..532c9240f475 100644 --- a/packages/bot-web-ui/package.json +++ b/packages/bot-web-ui/package.json @@ -70,7 +70,7 @@ "@datadog/browser-logs": "^4.36.0", "@deriv/bot-skeleton": "^1.0.0", "@deriv/components": "^1.0.0", - "@deriv/deriv-charts": "1.3.1", + "@deriv/deriv-charts": "1.3.2", "@deriv/shared": "^1.0.0", "@deriv/stores": "^1.0.0", "@deriv/translations": "^1.0.0", diff --git a/packages/bot-web-ui/src/app/__tests__/dbot-providers.spec.tsx b/packages/bot-web-ui/src/app/__tests__/dbot-providers.spec.tsx new file mode 100644 index 000000000000..67e57037bb6d --- /dev/null +++ b/packages/bot-web-ui/src/app/__tests__/dbot-providers.spec.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { mockStore, useStore } from '@deriv/stores'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { render, screen } from '@testing-library/react'; +import { mock_ws } from '../../utils/mock'; +import DBotProviders from '../dbot-providers'; + +jest.mock('@deriv/bot-skeleton/src/scratch/blockly', () => jest.fn()); +jest.mock('@deriv/bot-skeleton/src/scratch/dbot', () => ({ + saveRecentWorkspace: jest.fn(), + unHighlightAllBlocks: jest.fn(), +})); +jest.mock('@deriv/bot-skeleton/src/scratch/hooks/block_svg', () => jest.fn()); + +const TestStoreComponent = () => { + const { common } = useStore(); + const { platform } = common; + return
{platform}
; +}; + +describe('DBotProviders', () => { + let wrapper: ({ children }: { children: JSX.Element }) => JSX.Element; + + beforeAll(() => { + const mock_store = mockStore({ common: { platform: 'ctrader' } }); + wrapper = ({ children }: { children: JSX.Element }) => ( + + {children} + + ); + }); + + it('should render DBotProviders with children', () => { + render(
Test
, { + wrapper, + }); + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + it('should access useStore platform value from component', () => { + render(, { + wrapper, + }); + expect(screen.getByText('ctrader')).toBeInTheDocument(); + }); +}); diff --git a/packages/bot-web-ui/src/app/app-content.jsx b/packages/bot-web-ui/src/app/app-content.jsx index 745e1aa9aadb..dd3fd3d50e07 100644 --- a/packages/bot-web-ui/src/app/app-content.jsx +++ b/packages/bot-web-ui/src/app/app-content.jsx @@ -4,6 +4,7 @@ import { Loading } from '@deriv/components'; import { observer, useStore } from '@deriv/stores'; import { Audio, BotNotificationMessages, Dashboard, NetworkToastPopup, RoutePromptDialog } from 'Components'; import BotBuilder from 'Components/dashboard/bot-builder'; +import TransactionDetailsModal from 'Components/transaction-details'; import GTM from 'Utils/gtm'; import { useDBotStore } from 'Stores/useDBotStore'; import BlocklyLoading from '../components/blockly-loading'; @@ -107,6 +108,7 @@ const AppContent = observer(() => { + ); diff --git a/packages/bot-web-ui/src/app/app.scss b/packages/bot-web-ui/src/app/app.scss index 69d3a1efdb22..9348e176719c 100644 --- a/packages/bot-web-ui/src/app/app.scss +++ b/packages/bot-web-ui/src/app/app.scss @@ -11,4 +11,7 @@ --tab-content-height: calc(100vh - 16.6rem); --tab-content-height-mobile: calc(100vh - 12.6rem); + + --zindex-drawer: 5; + --zindex-modal: 6; } diff --git a/packages/bot-web-ui/src/components/audio/__tests__/audio.spec.tsx b/packages/bot-web-ui/src/components/audio/__tests__/audio.spec.tsx new file mode 100644 index 000000000000..8384cc3f15c6 --- /dev/null +++ b/packages/bot-web-ui/src/components/audio/__tests__/audio.spec.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import exp from 'constants'; +import { render, screen } from '@testing-library/react'; +import Audio from '../audio'; + +describe('Audio', () => { + it('should render Audio', () => { + const { container } = render(