diff --git a/.circleci/config.yml b/.circleci/config.yml index 8347ff534c47..eeb27370b4eb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -223,15 +223,17 @@ jobs: name: "Check TypeScript for @deriv/stores" command: npx tsc --project packages/stores/tsconfig.json -noEmit - run: - name: "Check TypeScript and linting for @deriv/wallets" - command: | - npx tsc --project packages/wallets/tsconfig.json -noEmit - npx eslint --fix --config packages/wallets/.eslintrc.js packages/wallets - npx stylelint packages/wallets/**/*.scss - + name: "Check TypeScript for @deriv/wallets" + command: npx tsc --project packages/wallets/tsconfig.json -noEmit # - run: # name: "Check TypeScript for @deriv/cashier" # command: npx tsc --project packages/cashier/tsconfig.json -noEmit + - run: + name: "Check ESLint for @deriv/wallets" + command: npx eslint --fix --config packages/wallets/.eslintrc.js packages/wallets + - run: + name: "Check Stylelint for @deriv/wallets" + command: npx stylelint packages/wallets/**/*.scss - run: name: "Check tests for @deriv/hooks" command: bash ./scripts/check-tests.sh packages/hooks/src diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b40465371db3..ea6f587833d1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -155,3 +155,15 @@ /packages/trader/**/* @matin-deriv @maryia-deriv +# ============================================================== +# deriv-app/wallets +# ============================================================== + +/packages/wallets/**/* @adrienne-deriv @thisyahlen-deriv @farhan-nurzi-deriv + + +# ============================================================== +# deriv-app/api +# ============================================================== + +/packages/api/**/* @adrienne-deriv @thisyahlen-deriv @farhan-nurzi-deriv diff --git a/package-lock.json b/package-lock.json index d50e58fdee0b..cef1435dc71a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@datadog/browser-rum": "^4.37.0", "@deriv/api-types": "^1.0.118", "@deriv/deriv-api": "^1.0.11", - "@deriv/deriv-charts": "1.3.2", + "@deriv/deriv-charts": "1.3.5", "@deriv/js-interpreter": "^3.0.0", "@deriv/ui": "^0.6.0", "@livechat/customer-sdk": "^2.0.4", @@ -37,6 +37,7 @@ "@types/js-cookie": "^3.0.1", "@types/jsdom": "^20.0.0", "@types/loadjs": "^4.0.1", + "@types/lodash.debounce": "^4.0.7", "@types/lodash.merge": "^4.6.7", "@types/lodash.throttle": "^4.1.7", "@types/object.fromentries": "^2.0.0", @@ -2892,9 +2893,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@deriv/deriv-charts": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.2.tgz", - "integrity": "sha512-j1xgloqF9jVPiCsfQJGKduivU7r42vQCeT+URCKz82dltlJAw7dmfxY3GgQHjBobkG+SK7ANG/rBzK1vyZn7bA==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.5.tgz", + "integrity": "sha512-qD4R/Lanf3xXBsOhTjubPcdXL6QMy7zR0O+Zq9KQiGx4lf3LES0FbQsLRlE2ebwAUK8hxD10jsUqTTS+zCpWBw==", "dependencies": { "@welldone-software/why-did-you-render": "^3.3.8", "classnames": "^2.3.1", @@ -15955,6 +15956,14 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==" }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz", + "integrity": "sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.merge": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.7.tgz", @@ -51015,9 +51024,9 @@ } }, "@deriv/deriv-charts": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.2.tgz", - "integrity": "sha512-j1xgloqF9jVPiCsfQJGKduivU7r42vQCeT+URCKz82dltlJAw7dmfxY3GgQHjBobkG+SK7ANG/rBzK1vyZn7bA==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@deriv/deriv-charts/-/deriv-charts-1.3.5.tgz", + "integrity": "sha512-qD4R/Lanf3xXBsOhTjubPcdXL6QMy7zR0O+Zq9KQiGx4lf3LES0FbQsLRlE2ebwAUK8hxD10jsUqTTS+zCpWBw==", "requires": { "@welldone-software/why-did-you-render": "^3.3.8", "classnames": "^2.3.1", @@ -60583,6 +60592,14 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==" }, + "@types/lodash.debounce": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz", + "integrity": "sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==", + "requires": { + "@types/lodash": "*" + } + }, "@types/lodash.merge": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.7.tgz", diff --git a/packages/account/src/Components/Routes/helpers.ts b/packages/account/src/Components/Routes/helpers.ts index bfe30ed07e9c..90edbdc87467 100644 --- a/packages/account/src/Components/Routes/helpers.ts +++ b/packages/account/src/Components/Routes/helpers.ts @@ -2,7 +2,7 @@ import { match, matchPath, RouteProps } from 'react-router'; import { routes } from '@deriv/shared'; import { TRouteConfig } from 'Types'; -export const normalizePath = (path: string) => (/^\//.test(path) ? path : `/${path || ''}`); // Default to '/' +export const normalizePath = (path: string) => (path.startsWith('/') ? path : `/${path || ''}`); // Default to '/' export const findRouteByPath = (path: string, routes_config?: TRouteConfig[]): RouteProps | undefined => { let result: RouteProps | undefined; @@ -31,7 +31,7 @@ export const findRouteByPath = (path: string, routes_config?: TRouteConfig[]): R }; export const isRouteVisible = (route?: { is_authenticated: boolean }, is_logged_in?: boolean) => - !(route && route.is_authenticated && !is_logged_in); + !(route?.is_authenticated && !is_logged_in); export const getPath = (route_path: string, params: { [key: string]: string } = {}) => Object.keys(params).reduce((p, name) => p.replace(`:${name}`, params[name]), route_path); diff --git a/packages/account/src/Components/Routes/route-with-sub-routes.tsx b/packages/account/src/Components/Routes/route-with-sub-routes.tsx index 197309cc7411..6a08505ee426 100644 --- a/packages/account/src/Components/Routes/route-with-sub-routes.tsx +++ b/packages/account/src/Components/Routes/route-with-sub-routes.tsx @@ -51,7 +51,7 @@ const RouteWithSubRoutes = (route: TRouteWithSubRoutesProps) => { ); } - const title = route.getTitle?.() || ''; + const title = route.getTitle?.() ?? ''; document.title = `${title} | ${default_title}`; alternateLinkTagChange(); diff --git a/packages/account/src/Components/account-limits/account-limits-table-cell.tsx b/packages/account/src/Components/account-limits/account-limits-table-cell.tsx index 28d652764d39..762f214742a4 100644 --- a/packages/account/src/Components/account-limits/account-limits-table-cell.tsx +++ b/packages/account/src/Components/account-limits/account-limits-table-cell.tsx @@ -40,7 +40,7 @@ const AccountLimitsTableCell = ({ {children} )} - {renderExtraInfo && renderExtraInfo()} + {renderExtraInfo?.()} ); diff --git a/packages/account/src/Components/account-limits/account-limits-table-header.tsx b/packages/account/src/Components/account-limits/account-limits-table-header.tsx index 76ecb6144cb2..ce78bed50735 100644 --- a/packages/account/src/Components/account-limits/account-limits-table-header.tsx +++ b/packages/account/src/Components/account-limits/account-limits-table-header.tsx @@ -33,7 +33,7 @@ const AccountLimitsTableHeader = ({ {children} )} - {renderExtraInfo && renderExtraInfo()} + {renderExtraInfo?.()} ); }; diff --git a/packages/account/src/Components/account-limits/account-limits.tsx b/packages/account/src/Components/account-limits/account-limits.tsx index 0bb41406c1ab..d0e95f4ef5c3 100644 --- a/packages/account/src/Components/account-limits/account-limits.tsx +++ b/packages/account/src/Components/account-limits/account-limits.tsx @@ -1,10 +1,12 @@ import React from 'react'; +import { FormikValues } from 'formik'; import classNames from 'classnames'; -import { Loading, ThemedScrollbars, Text, ButtonLink } from '@deriv/components'; import { formatMoney, isDesktop, isMobile, useIsMounted, PlatformContext } from '@deriv/shared'; +import { Loading, ThemedScrollbars } from '@deriv/components'; import { Localize, localize } from '@deriv/translations'; -import LoadErrorMessage from 'Components/load-error-message'; +import { observer, useStore } from '@deriv/stores'; import DemoMessage from 'Components/demo-message'; +import LoadErrorMessage from 'Components/load-error-message'; import AccountLimitsArticle from './account-limits-article'; import AccountLimitsContext, { TAccountLimitsContext } from './account-limits-context'; import AccountLimitsExtraInfo from './account-limits-extra-info'; @@ -13,8 +15,7 @@ import AccountLimitsOverlay from './account-limits-overlay'; import AccountLimitsTableCell from './account-limits-table-cell'; import AccountLimitsTableHeader from './account-limits-table-header'; import AccountLimitsTurnoverLimitRow from './account-limits-turnover-limit-row'; -import { observer, useStore } from '@deriv/stores'; -import { FormikValues } from 'formik'; +import WithdrawalLimitsTable from './withdrawal-limits-table'; type TAccountLimits = { footer_ref?: React.RefObject; @@ -36,20 +37,22 @@ const AccountLimits = observer( should_show_article = true, }: TAccountLimits) => { const { client, common } = useStore(); - const { account_limits, currency, getLimits, is_fully_authenticated, is_virtual, is_switching } = client; + const { account_limits, currency, getLimits, is_virtual, is_switching } = client; const { is_from_derivgo } = common; const isMounted = useIsMounted(); const [is_loading, setLoading] = React.useState(false); const [is_overlay_shown, setIsOverlayShown] = React.useState(false); const { is_appstore } = React.useContext(PlatformContext); + const handleGetLimitsResponse = () => { + if (isMounted()) setLoading(false); + }; + React.useEffect(() => { if (is_virtual) { setLoading(false); } else { - getLimits().then(() => { - if (isMounted()) setLoading(false); - }); + getLimits().then(handleGetLimitsResponse); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -105,9 +108,7 @@ const AccountLimits = observer( } const { commodities, forex, indices, synthetic_index } = { ...market_specific }; - const forex_ordered = forex - ?.slice() - .sort((a: FormikValues, b: FormikValues) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)); + const forex_ordered = forex?.slice().sort((a: FormikValues, b: FormikValues) => a.name.localeCompare(b.name)); const derived_ordered = synthetic_index ?.slice() .sort((a: FormikValues, b: FormikValues) => (a.level > b.level ? 1 : -1)); @@ -131,7 +132,7 @@ const AccountLimits = observer( )}
- + @@ -232,115 +233,14 @@ const AccountLimits = observer(
{/* We only show "Withdrawal Limits" on account-wide settings pages. */} + {!is_app_settings && ( - - - - - - - - {is_fully_authenticated && ( - - - - )} - - - - {is_fully_authenticated ? ( - - - - - {localize( - 'Your account is fully authenticated and your withdrawal limits have been lifted.' - )} - - - - - - ) : ( - - - - {is_appstore ? ( - - ) : ( - - )} - {is_appstore && !is_fully_authenticated && ( - - - {localize( - 'To increase limit please verify your identity' - )} - - - - {localize('Verify')} - - - - )} - - - {formatMoney(currency, num_of_days_limit, true)} - - - - - - - - {formatMoney( - currency, - withdrawal_since_inception_monetary, - true - )} - - - - - - - - {formatMoney(currency, remainder, true)} - - - - )} - -
- {(!is_appstore || isMobile()) && ( -
- - {is_fully_authenticated ? ( - - ) : ( - - )} - -
- )} -
+ )}
diff --git a/packages/account/src/Components/account-limits/withdrawal-limits-table.tsx b/packages/account/src/Components/account-limits/withdrawal-limits-table.tsx new file mode 100644 index 000000000000..0310cc2bf92e --- /dev/null +++ b/packages/account/src/Components/account-limits/withdrawal-limits-table.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { ButtonLink, Text } from '@deriv/components'; +import { formatMoney, isMobile } from '@deriv/shared'; +import { Localize, localize } from '@deriv/translations'; +import { observer, useStore } from '@deriv/stores'; +import AccountLimitsTableCell from './account-limits-table-cell'; +import AccountLimitsTableHeader from './account-limits-table-header'; + +type TWithdrawalLimitsTable = { + is_appstore: boolean; + num_of_days_limit?: string | number; + withdrawal_since_inception_monetary?: string | number; + remainder?: string | number; +}; + +const AccountLimitsWithdrawalContent = ({ is_appstore }: { is_appstore: boolean }) => { + return is_appstore ? ( + + ) : ( + + ); +}; + +const AccountLimitsVerificationNotice = observer(({ is_appstore }: { is_appstore: boolean }) => { + const { client } = useStore(); + const { is_fully_authenticated } = client; + return ( + + + {is_appstore && !is_fully_authenticated && ( + + + {localize('To increase limit please verify your identity')} + + + + {localize('Verify')} + + + + )} + + ); +}); + +const WithdrawalLimitsTable = observer( + ({ is_appstore, num_of_days_limit, withdrawal_since_inception_monetary, remainder }: TWithdrawalLimitsTable) => { + const { client } = useStore(); + const { currency, is_fully_authenticated } = client; + return ( + + + + + + + + {is_fully_authenticated && ( + + + + )} + + + + {is_fully_authenticated ? ( + + + + {localize( + 'Your account is fully authenticated and your withdrawal limits have been lifted.' + )} + + + + + ) : ( + + + + + + + {formatMoney(currency, num_of_days_limit ?? 0, true)} + + + + + + + + {formatMoney(currency, withdrawal_since_inception_monetary ?? 0, true)} + + + + + + + + {formatMoney(currency, remainder ?? '', true)} + + + + )} + +
+ {(!is_appstore || isMobile()) && ( +
+ + {is_fully_authenticated ? ( + + ) : ( + + )} + +
+ )} +
+ ); + } +); + +export default WithdrawalLimitsTable; diff --git a/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx b/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx index 2a80c98ddc93..a3b1b44581a8 100644 --- a/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx +++ b/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx @@ -10,8 +10,8 @@ type TApiTokenTableRowTokenCell = { const HiddenPasswordDots = () => (
- {[...Array(15)].map((el, index) => ( -
+ {[...Array(15).keys()].map(el => ( +
))}
); diff --git a/packages/account/src/Components/api-token/api-token-table-row.tsx b/packages/account/src/Components/api-token/api-token-table-row.tsx index 2666164b09e4..7306b3ab77e9 100644 --- a/packages/account/src/Components/api-token/api-token-table-row.tsx +++ b/packages/account/src/Components/api-token/api-token-table-row.tsx @@ -13,10 +13,10 @@ const ApiTokenTableRow = ({ token }: TApiTokenTableRow) => ( {token.display_name} - + - + {token.last_used} diff --git a/packages/account/src/Components/api-token/api-token-table.tsx b/packages/account/src/Components/api-token/api-token-table.tsx index d4896fa598dc..22f0acf83c3d 100644 --- a/packages/account/src/Components/api-token/api-token-table.tsx +++ b/packages/account/src/Components/api-token/api-token-table.tsx @@ -14,7 +14,7 @@ const ApiTokenTable = () => { const { api_tokens } = React.useContext(ApiTokenContext); const formatTokenScopes = (str: string) => { - const replace_filter = str.replace(/-|_/g, ' '); + const replace_filter = str.replace(/[-_]/g, ' '); const sentenced_case = replace_filter[0].toUpperCase() + replace_filter.slice(1).toLowerCase(); return sentenced_case; }; @@ -75,10 +75,7 @@ const ApiTokenTable = () => { - +
@@ -94,10 +91,10 @@ const ApiTokenTable = () => { - +
- +
diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index 558e16a5725c..0bf5117fddfe 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -100,7 +100,7 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown const validateFields = (values: FormikValues) => { const errors: FormikErrors = {}; - const token_name = values.token_name && values.token_name.trim(); + const token_name = values?.token_name?.trim(); if (!token_name) { errors.token_name = localize('Please enter a token name.'); diff --git a/packages/account/src/Components/error-component/error-component.tsx b/packages/account/src/Components/error-component/error-component.tsx index bd0f3e3bee39..0594680098ea 100644 --- a/packages/account/src/Components/error-component/error-component.tsx +++ b/packages/account/src/Components/error-component/error-component.tsx @@ -30,7 +30,7 @@ const ErrorComponent = ({ } redirect_urls={[routes.trade]} redirect_labels={[redirect_label || localize('Refresh')]} - buttonOnClick={redirectOnClick || (() => location.reload())} + buttonOnClick={redirectOnClick ?? (() => location.reload())} /> ); }; diff --git a/packages/account/src/Components/financial-details/financial-details.tsx b/packages/account/src/Components/financial-details/financial-details.tsx index f4b7148d5a70..9eed2b5c395f 100644 --- a/packages/account/src/Components/financial-details/financial-details.tsx +++ b/packages/account/src/Components/financial-details/financial-details.tsx @@ -134,10 +134,7 @@ const FinancialDetails = (props: TFinancialDetails & TFinancialInformationAndTra - + = 890} height={Number(height) - 77}>
{ - 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(''); @@ -45,10 +50,8 @@ const IDVForm = ({ const new_document_list = filtered_documents.map(key => { const { display_name, format } = document_data[key]; - const { new_display_name, example_format, sample_image } = getDocumentData( - selected_country.value ?? '', - key - ); + const { new_display_name, example_format, sample_image, additional_document_example_format } = + getDocumentData(selected_country.value ?? '', key); const needs_additional_document = !!document_data[key].additional; if (needs_additional_document) { @@ -58,6 +61,7 @@ const IDVForm = ({ additional: { display_name: document_data[key].additional?.display_name, format: document_data[key].additional?.format, + example_format: additional_document_example_format, }, value: format, sample_image, @@ -98,7 +102,7 @@ const IDVForm = ({ setFieldValue(document_name, current_input, true); }; - const bindDocumentData = (item: TDocumentList) => { + const bindDocumentData = (item: TDocument) => { setFieldValue('document_type', item, true); setSelectedDoc(item?.id); if (item?.id === IDV_NOT_APPLICABLE_OPTION.id) { @@ -189,76 +193,75 @@ const IDVForm = ({ )} -
- - {({ field }: FieldProps) => ( - - - onKeyUp(e, 'document_number') - } - required - label={generatePlaceholderText(selected_doc)} - /> - {values.document_type.additional?.display_name && ( + {values.document_type.id !== IDV_NOT_APPLICABLE_OPTION.id && ( +
+ + {({ field }: FieldProps) => ( + - onKeyUp(e, 'document_additional') + onKeyUp(e, 'document_number') } + className='additional-field' required + label={generatePlaceholderText(selected_doc)} /> - )} - - )} - -
+ {values.document_type.additional?.display_name && ( + + onKeyUp(e, 'document_additional') + } + required + /> + )} +
+ )} +
+
+ )}
{document_image && (
diff --git a/packages/account/src/Components/forms/personal-details-form.jsx b/packages/account/src/Components/forms/personal-details-form.jsx index 4be847596d3e..4eafaca31919 100644 --- a/packages/account/src/Components/forms/personal-details-form.jsx +++ b/packages/account/src/Components/forms/personal-details-form.jsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { Field, useFormikContext } from 'formik'; +import { Link } from 'react-router-dom'; import { Autocomplete, Checkbox, @@ -15,13 +15,13 @@ import { } from '@deriv/components'; import { getLegalEntityName, isDesktop, isMobile, routes, validPhone } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; -import InlineNoteWithIcon from '../inline-note-with-icon'; -import { DateOfBirthField, FormInputField } from './form-fields.jsx'; -import FormBodySection from '../form-body-section'; import FormSubHeader from '../form-sub-header'; import PoiNameDobExample from '../../Assets/ic-poi-name-dob-example.svg'; -import { isFieldImmutable } from '../../Helpers/utils'; +import InlineNoteWithIcon from '../inline-note-with-icon'; +import FormBodySection from '../form-body-section'; +import { DateOfBirthField, FormInputField } from './form-fields.jsx'; import { getEmploymentStatusList } from '../../Sections/Assessment/FinancialAssessment/financial-information-list'; +import { isFieldImmutable } from '../../Helpers/utils'; const PersonalDetailsForm = props => { const { @@ -419,10 +419,7 @@ const PersonalDetailsForm = props => { {'tax_residence' in values && ( { is_tin_popover_open={is_tin_popover_open} setIsTinPopoverOpen={setIsTinPopoverOpen} setIsTaxResidencePopoverOpen={setIsTaxResidencePopoverOpen} - disabled={ - isFieldImmutable('tax_identification_number', editable_fields) || - (values?.tax_identification_number && has_real_account) - } + disabled={isFieldImmutable('tax_identification_number', editable_fields)} required /> )} @@ -551,12 +545,8 @@ const PersonalDetailsForm = props => { {'tax_residence' in values && ( { is_tin_popover_open={is_tin_popover_open} setIsTinPopoverOpen={setIsTinPopoverOpen} setIsTaxResidencePopoverOpen={setIsTaxResidencePopoverOpen} - disabled={ - isFieldImmutable('tax_identification_number', editable_fields) || - (values?.tax_identification_number && has_real_account) - } - required + disabled={isFieldImmutable('tax_identification_number', editable_fields)} /> )} {'account_opening_reason' in values && ( @@ -657,7 +643,7 @@ const PlaceOfBirthField = ({ handleChange, setFieldValue, disabled, residence_li const TaxResidenceField = ({ setFieldValue, residence_list, - required, + required = false, setIsTaxResidencePopoverOpen, setIsTinPopoverOpen, is_tax_residence_popover_open, @@ -679,6 +665,7 @@ const TaxResidenceField = ({ list_portal_id='modal_root' data-testid='tax_residence' disabled={disabled} + required={required} /> @@ -695,7 +682,7 @@ const TaxResidenceField = ({ setFieldValue('tax_residence', e.target.value, true); }} {...field} - required + required={required} data_testid='tax_residence_mobile' disabled={disabled} /> @@ -729,7 +716,7 @@ const TaxIdentificationNumberField = ({ setIsTinPopoverOpen, setIsTaxResidencePopoverOpen, disabled, - required, + required = false, }) => (
{ return show_more ? ( - {message_list.map((text, idx) => ( - + {message_list.map(text => ( + ))} + ); +}; + +export default TourButton; diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/index.ts b/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/index.ts new file mode 100644 index 000000000000..3b2bbf0f6e0b --- /dev/null +++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/index.ts @@ -0,0 +1,3 @@ +import MobileTours from './mobile-tours'; + +export default MobileTours; diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/mobile-tours.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/mobile-tours.tsx new file mode 100644 index 000000000000..8e418111a0aa --- /dev/null +++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/mobile-tours.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { observer } from '@deriv/stores'; +import { useDBotStore } from 'Stores/useDBotStore'; +import BotBuilderTour from './bot-builder-tour'; +import OnboardingTour from './onboarding-tour'; + +const MobileTours = observer(() => { + const { dashboard } = useDBotStore(); + const { has_started_onboarding_tour } = dashboard; + return <>{has_started_onboarding_tour ? : }; +}); + +export default MobileTours; diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/onboarding-tour.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/onboarding-tour.tsx new file mode 100644 index 000000000000..4547334acc03 --- /dev/null +++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/mobile-tours/onboarding-tour.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Icon, ProgressBarTracker, Text } from '@deriv/components'; +import { observer } from '@deriv/stores'; +import { localize } from '@deriv/translations'; +import { useDBotStore } from 'Stores/useDBotStore'; +import { DBOT_ONBOARDING_MOBILE, TMobileTourConfig } from '../config'; +import TourButton from './common/tour-button'; + +const default_tour_data = { + content: [], + header: '', + img: '', + tour_step_key: 1, +}; + +type TTourData = TMobileTourConfig & { + img: string; + tour_step_key: number; +}; + +const OnboardingTour = observer(() => { + const { dashboard } = useDBotStore(); + const { onCloseTour, onTourEnd, setTourActiveStep } = dashboard; + const [tour_step, setStep] = React.useState(1); + const [tour_data, setTourData] = React.useState(default_tour_data); + const { content, header, img, media, tour_step_key } = tour_data; + const start_button = tour_step === 1 ? localize('Start') : localize('Next'); + const tour_button_text = tour_step === 8 ? localize('Got it, thanks!') : start_button; + const test_id = tour_step_key === 8 ? 'finish-onboard-tour' : 'next-onboard-tour'; + const hide_prev_button = [1, 2, 8]; + + React.useEffect(() => { + DBOT_ONBOARDING_MOBILE.forEach(data => { + if (data.tour_step_key === tour_step) { + setTourData(data); + } + setTourActiveStep(tour_step); + }); + }, [tour_step]); + return ( +
+ {tour_step_key !== 1 && ( +
+ {`${tour_step_key - 1}/7`} + + + +
+ )} + {header && ( + + {localize(header)} + + )} + {media && ( +
+
+ )} + {img && ( +
+ +
+ )} + + {content && ( + <> + {content.map(data => { + return ( + + {data} + + ); + })} + + )} +
+
+ v.tour_step_key.toString())} + setStep={setStep} + /> +
+
+ {tour_step === 1 && ( + { + onCloseTour(); + }} + label={localize('Skip')} + data-testid='skip-onboard-tour' + /> + )} + {!hide_prev_button.includes(tour_step) && ( + { + setStep(tour_step - 1); + }} + label={localize('Previous')} + data-testid='prev-onboard-tour' + /> + )} + { + setStep(tour_step + 1); + onTourEnd(tour_step, true); + }} + label={tour_button_text} + data-testid={test_id} + /> +
+
+
+ ); +}); + +export default OnboardingTour; diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/utils/index.ts b/packages/bot-web-ui/src/components/dashboard/dbot-tours/utils/index.ts new file mode 100644 index 000000000000..f6509623f26e --- /dev/null +++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/utils/index.ts @@ -0,0 +1,70 @@ +import { CallBackProps } from 'react-joyride'; +import { getSetting, storeSetting } from 'Utils/settings'; + +type TTourStatus = { + key: string; + toggle: string; + type: string; +}; + +export const tour_type: TTourType = { + key: 'onboard_tour', +}; + +export const tour_status_ended: TTourStatus = { + key: '', + toggle: '', + type: `${tour_type.key}_status`, +}; + +let tour: Record = {}; +let current_target: number | undefined; + +export const handleJoyrideCallback = (data: CallBackProps) => { + const { action, index, status } = data; + if (status === 'finished') { + tour_status_ended.key = status; + } + if (action === 'close') { + tour_status_ended.toggle = action; + } + if (current_target !== index) { + tour = {}; + tour.status = status; + tour.action = action; + } + current_target = index; + setTourSettings(tour, 'tour'); + //added trigger to create new listner on local storage + window.dispatchEvent(new Event('storage')); + setTourSettings(new Date().getTime(), `${tour_type.key}_token`); +}; + +export type TTourType = Pick; + +export const setTourType = (param: string) => { + tour_type.key = param; +}; + +export const highlightLoadModalButton = (tour_active: boolean, step: number) => { + const el_ref = document.querySelector('.toolbar__group-btn svg:nth-child(2)'); + if (tour_active && step === 1) { + el_ref?.classList.add('dbot-tour-blink'); + } else { + el_ref?.classList.remove('dbot-tour-blink'); + } +}; + +export const setTourSettings = (param: number | { [key: string]: string }, type: string) => { + if (type === `${tour_type.key}_token`) { + return storeSetting(`${tour_type.key}_token`, param); + } + return storeSetting(`${tour_type.key}_status`, param); +}; + +export const getTourSettings = (type: string) => { + if (type === 'token') { + return getSetting(`${tour_type.key}_token`); + } + return getSetting(`${tour_type.key}_status`); +}; diff --git a/packages/bot-web-ui/src/components/dashboard/quick-strategy/quick-strategy.types.ts b/packages/bot-web-ui/src/components/dashboard/quick-strategy/quick-strategy.types.ts index 26bcc06be1a7..a70f9c950840 100644 --- a/packages/bot-web-ui/src/components/dashboard/quick-strategy/quick-strategy.types.ts +++ b/packages/bot-web-ui/src/components/dashboard/quick-strategy/quick-strategy.types.ts @@ -82,11 +82,6 @@ export type TInputUniqFields = 'input_martingale_size' | 'input_alembert_unit' | export type TInputBaseFields = 'input_duration_value' | 'input_stake' | 'input_loss' | 'input_profit'; export type TInputCommonFields = TInputBaseFields | TInputUniqFields; -export type TSetFieldValue = ( - element: 'button' | 'quick-strategy__duration-unit' | 'quick-strategy__duration-value' | string, - action: 'run' | 'edit' | string | number -) => void; - export type TSelectsFieldNames = | 'quick-strategy__type-strategy' | 'quick-strategy__symbol' @@ -94,6 +89,11 @@ export type TSelectsFieldNames = | 'quick-strategy__duration-unit' | ''; +export type TSetFieldValue = ( + element: 'button' | 'quick-strategy__duration-unit' | 'quick-strategy__duration-value' | TSelectsFieldNames, + action: string | number +) => void; + export type TInputsFieldNames = | 'quick-strategy__duration-value' | 'quick-strategy__stake' diff --git a/packages/bot-web-ui/src/components/dashboard/tour-slider.tsx b/packages/bot-web-ui/src/components/dashboard/tour-slider.tsx deleted file mode 100644 index abc7eb722880..000000000000 --- a/packages/bot-web-ui/src/components/dashboard/tour-slider.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { Icon, ProgressBarTracker, Text } from '@deriv/components'; -import { observer } from '@deriv/stores'; -import { localize } from '@deriv/translations'; -import { useDBotStore } from 'Stores/useDBotStore'; -import { BOT_BUILDER_MOBILE, DBOT_ONBOARDING_MOBILE, TStepMobile } from './joyride-config'; - -type TTourButton = { - type?: string; - onClick: () => void; - label: string; -}; - -type TAccordion = { - content_data: TStepMobile; - expanded: boolean; -}; - -const TourButton = ({ label, type = 'default', ...props }: TTourButton) => { - return ( - - ); -}; - -const Accordion = ({ content_data, expanded = false, ...props }: TAccordion) => { - const [is_open, setOpen] = React.useState(expanded); - const { content, header } = content_data; - - return ( -
-
-
setOpen(!is_open)}> -
- - {header} - -
-
- -
-
-
- - {content} - -
-
-
- ); -}; - -const TourSlider = observer(() => { - const { dashboard, load_modal } = useDBotStore(); - const { has_started_bot_builder_tour, has_started_onboarding_tour, onCloseTour, onTourEnd, setTourActiveStep } = - dashboard; - const { toggleTourLoadModal } = load_modal; - const [step, setStep] = React.useState(1); - const [slider_content, setContent] = React.useState([]); - const [slider_header, setHeader] = React.useState(''); - const [slider_image, setImg] = React.useState(''); - const [slider_media, setMedia] = React.useState(''); - const [step_key, setStepKey] = React.useState(0); - - React.useEffect(() => { - setTourActiveStep(step); - Object.values(!has_started_onboarding_tour ? BOT_BUILDER_MOBILE : DBOT_ONBOARDING_MOBILE).forEach(data => { - if (data.key === step) { - setContent(data?.content); - setHeader(data?.header); - setImg(data?.img || ''); - setMedia(data?.media || ''); - setStepKey(data?.step_key || 0); - } - }); - const el_ref = document.querySelector('.toolbar__group-btn svg:nth-child(2)'); - if (has_started_bot_builder_tour && step === 1) { - //component does not rerender - el_ref?.classList.add('dbot-tour-blink'); - } else { - el_ref?.classList.remove('dbot-tour-blink'); - } - if (has_started_bot_builder_tour && step === 2) { - toggleTourLoadModal(true); - } else toggleTourLoadModal(false); - }, [step]); - - const onChange = React.useCallback( - (param: string) => { - const MOBILE_TOUR = !has_started_onboarding_tour ? BOT_BUILDER_MOBILE : DBOT_ONBOARDING_MOBILE; - if (param === 'inc' && step < Object.keys(MOBILE_TOUR).length) setStep(step + 1); - else if (param === 'dec' && step > 1) setStep(step - 1); - else if (param === 'skip') onCloseTour(); - }, - [step] - ); - - const content_data = BOT_BUILDER_MOBILE.find(({ key }) => key === step); - const onClickNext = React.useCallback(() => { - onChange('inc'); - onTourEnd(step, has_started_onboarding_tour); - }, [step]); - - const bot_tour_text = !has_started_onboarding_tour && step === 3 ? localize('Finish') : localize('Next'); - - const tour_button_text = has_started_onboarding_tour && step_key === 0 ? localize('Start') : bot_tour_text; - - const onboarding_completed_text = - has_started_onboarding_tour && step === 8 ? localize('Got it, thanks!') : tour_button_text; - - return ( - <> -
- {has_started_onboarding_tour && slider_header && step_key !== 0 && ( -
- {`${step_key}/7`} - - - -
- )} - - {has_started_onboarding_tour && slider_header && ( - - {localize(slider_header)} - - )} - {has_started_onboarding_tour && - // eslint-disable-next-line no-nested-ternary - (slider_media ? ( -
-
- ) : slider_image ? ( -
- -
- ) : null)} - {has_started_onboarding_tour && slider_content && ( - <> - {slider_content?.map((data, index) => { - return ( - - {data} - - ); - })} - - )} - {!has_started_onboarding_tour && content_data && } -
-
- {(!has_started_onboarding_tour || (has_started_onboarding_tour && step !== 1)) && ( - - )} -
-
- {has_started_onboarding_tour && step === 1 && ( - { - onChange('skip'); - }} - label={localize('Skip')} - /> - )} - {((has_started_bot_builder_tour && step !== 1) || - (has_started_onboarding_tour && step !== 1 && step !== 2 && step !== 8)) && ( - { - onChange('dec'); - }} - label={localize('Previous')} - /> - )} - -
-
-
- - ); -}); - -export default TourSlider; diff --git a/packages/bot-web-ui/src/components/dashboard/tour-trigger-dialog.spec.tsx b/packages/bot-web-ui/src/components/dashboard/tour-trigger-dialog.spec.tsx deleted file mode 100644 index d9bbd0d994ec..000000000000 --- a/packages/bot-web-ui/src/components/dashboard/tour-trigger-dialog.spec.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react'; -import { mockStore, StoreProvider } from '@deriv/stores'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { act, fireEvent, render, screen } from '@testing-library/react'; -import RootStore from '../../stores/root-store'; -import { DBotStoreProvider, mockDBotStore } from '../../stores/useDBotStore'; -import { setTourType } from './joyride-config'; -import TourTriggrerDialog from './tour-trigger-dialog'; - -jest.mock('@deriv/shared', () => ({ - ...jest.requireActual('@deriv/shared'), - isMobile: jest.fn(() => false), -})); - -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', () => ({ - blocksCoordinate: jest.fn(), -})); - -const mock_ws = { - authorized: { - subscribeProposalOpenContract: jest.fn(), - send: jest.fn(), - }, - storage: { - send: jest.fn(), - }, - contractUpdate: jest.fn(), - subscribeTicksHistory: jest.fn(), - forgetStream: jest.fn(), - activeSymbols: jest.fn(), - send: jest.fn(), -}; - -describe('', () => { - let wrapper: ({ children }: { children: JSX.Element }) => JSX.Element, mock_DBot_store: RootStore | undefined; - - beforeAll(() => { - const mock_store = mockStore({}); - mock_DBot_store = mockDBotStore(mock_store, mock_ws); - - wrapper = ({ children }: { children: JSX.Element }) => ( - - - {children} - - - ); - }); - - it('renders tour trigger component', () => { - const { container } = render(, { - wrapper, - }); - expect(container).toBeInTheDocument(); - }); - - it('should open tour trigger dialog', () => { - act(() => { - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - }); - render(, { - wrapper, - }); - expect(screen.getByText(/Get started on Deriv Bot/i)).toBeInTheDocument(); - }); - - it('should show tour end message', () => { - act(() => { - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setHasTourEnded(true); - }); - render(, { - wrapper, - }); - expect(screen.getByText(/Want to retake the tour?/i)).toBeInTheDocument(); - }); - - it('should start onboarding tour', () => { - act(() => { - mock_DBot_store?.dashboard?.setActiveTab(1); - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setHasTourEnded(false); - }); - render(, { - wrapper, - }); - - act(() => { - const buttonElement = screen.getByText('Start', { selector: 'span' }); - fireEvent.click(buttonElement); - }); - - expect(screen.getByText('OK')).toBeInTheDocument(); - }); - - it('should show tour success message', () => { - act(() => { - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setActiveTab(1); - mock_DBot_store?.dashboard?.setHasTourEnded(true); - }); - render(, { - wrapper, - }); - expect(screen.getByTestId('tour-success-message')).toBeInTheDocument(); - }); - - it('should cancel tour', () => { - act(() => { - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setActiveTab(1); - mock_DBot_store?.dashboard?.setHasTourEnded(true); - }); - render(, { - wrapper, - }); - - act(() => { - const buttonElement = screen.getByText('Skip', { selector: 'span' }); - fireEvent.click(buttonElement); - }); - - expect(mock_DBot_store?.dashboard?.is_tour_dialog_visible).toBeFalsy(); - }); - - it('should render bot builder tour', () => { - act(() => { - mock_DBot_store?.dashboard?.setActiveTab(1); - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setHasTourEnded(false); - }); - render(, { - wrapper, - }); - expect(screen.getByText("Let's build a Bot!")).toBeInTheDocument(); - }); - - it('should start bot builder tour', () => { - act(() => { - setTourType('bot_builder'); - mock_DBot_store?.dashboard?.setActiveTab(2); - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setHasTourEnded(false); - }); - render(, { - wrapper, - }); - - act(() => { - const buttonElement = screen.getByText('Start', { selector: 'span' }); - fireEvent.click(buttonElement); - }); - - expect(screen.getByText('OK')).toBeInTheDocument(); - }); - - it('should exit bot builder tour', () => { - act(() => { - setTourType('bot_builder'); - mock_DBot_store?.dashboard?.setActiveTab(2); - mock_DBot_store?.dashboard?.setTourDialogVisibility(true); - mock_DBot_store?.dashboard?.setHasTourEnded(true); - }); - render(, { - wrapper, - }); - - act(() => { - const buttonElement = screen.getByText('Skip', { selector: 'span' }); - fireEvent.click(buttonElement); - }); - - expect(mock_DBot_store?.dashboard?.is_tour_dialog_visible).toBeFalsy(); - }); -}); diff --git a/packages/bot-web-ui/src/components/dashboard/tour-trigger-dialog.tsx b/packages/bot-web-ui/src/components/dashboard/tour-trigger-dialog.tsx deleted file mode 100644 index d20903539ec2..000000000000 --- a/packages/bot-web-ui/src/components/dashboard/tour-trigger-dialog.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { Dialog, Text } from '@deriv/components'; -import { isMobile } from '@deriv/shared'; -import { observer } from '@deriv/stores'; -import { Localize, localize } from '@deriv/translations'; -import { useDBotStore } from '../../stores/useDBotStore'; -import { setTourSettings, tour_status_ended, tour_type } from './joyride-config'; - -const TourTriggrerDialog = observer(() => { - const { dashboard } = useDBotStore(); - const { active_tab, has_tour_ended, is_tour_dialog_visible, setTourDialogVisibility, toggleOnConfirm } = dashboard; - - const is_mobile = isMobile(); - - const toggleTour = (value: boolean, type: string) => { - if (tour_type.key === 'onboard_tour') { - if (type === 'onConfirm') { - toggleOnConfirm(active_tab, value); - } else { - setTourSettings(new Date().getTime(), `${tour_type.key}_token`); - } - tour_type.key = 'onboard_tour'; - } else if (tour_type.key === 'bot_builder') { - if (type === 'onConfirm') { - toggleOnConfirm(active_tab, value); - } else { - setTourSettings(new Date().getTime(), `${tour_type.key}_token`); - } - tour_type.key = 'bot_builder'; - } - setTourDialogVisibility(false); - }; - - const dashboardTourContent = () => { - if (!has_tour_ended) { - return ( - ]} - /> - ); - } - return ( - Tutorials.'} components={[]} /> - ); - }; - - const getTourHeaders = (tour_check: boolean, tab_id: number) => { - let text; - if (!tour_check) { - if (tab_id === 1 && is_mobile) text = localize('Bot Builder guide'); - else if (tab_id === 1) text = localize("Let's build a Bot!"); - else text = localize('Get started on Deriv Bot'); - } else if (tab_id === 1) text = localize('Congratulations'); - else text = localize('Want to retake the tour?'); - return text; - }; - - const tourDialogInfo = is_mobile - ? localize('Here’s a quick guide on how to use Deriv Bot on the go.') - : localize('Learn how to build your bot from scratch using a simple strategy.'); - - const tourDialogAction = is_mobile - ? localize( - 'You can import a bot from your mobile device or from Google drive, see a preview in the bot builder, and start trading by running the bot.' - ) - : localize('Hit the <0>Start button to begin and follow the tutorial.'); - - const getTourContent = (type: string) => { - return ( - <> - {type === 'header' && getTourHeaders(has_tour_ended, active_tab)} - {type === 'content' && active_tab === 0 && dashboardTourContent()} - {type === 'content' && - active_tab === 1 && - (!has_tour_ended ? ( - <> -
- -
-
- ]} - /> -
-
- Tutorials tab.' - } - components={[]} - /> -
- - ) : ( - <> -
- -
-
- run the bot to test out the strategy.'} - components={[]} - /> -
-
- Tutorials tab.' - } - components={[]} - /> -
- - ))} - - ); - }; - - const confirm_button = active_tab === 0 ? localize('Got it, thanks!') : localize('OK'); - - const onHandleConfirm = React.useCallback(() => { - const status = tour_status_ended.key === 'finished'; - toggleTour(status ? false : !has_tour_ended, 'onConfirm'); - tour_status_ended.key = ''; - return status ? tour_status_ended.key : null; - }, [has_tour_ended, active_tab]); - - return ( -
- toggleTour(false, 'onCancel')} - confirm_button_text={has_tour_ended ? confirm_button : localize('Start')} - onConfirm={onHandleConfirm} - is_mobile_full_width - className={classNames('dc-dialog', { - 'tour-dialog': active_tab === 0 || active_tab === 1, - 'tour-dialog--end': (active_tab === 0 || active_tab === 1) && has_tour_ended, - })} - has_close_icon={false} - > -
- - {is_tour_dialog_visible && getTourContent('header')} - -
-
- - {is_tour_dialog_visible && getTourContent('content')} - -
-
-
- ); -}); - -export default TourTriggrerDialog; diff --git a/packages/bot-web-ui/src/components/dashboard/tutorial-tab/faq-content.tsx b/packages/bot-web-ui/src/components/dashboard/tutorial-tab/faq-content.tsx index 9db0d765338e..02d4ab58ef94 100644 --- a/packages/bot-web-ui/src/components/dashboard/tutorial-tab/faq-content.tsx +++ b/packages/bot-web-ui/src/components/dashboard/tutorial-tab/faq-content.tsx @@ -33,10 +33,41 @@ const FAQ = ({ type, content, src, imageclass }: TDescription) => { ); }; +const scrollToElement = (wrapper_element: HTMLElement, offset: number) => { + if (wrapper_element) { + wrapper_element.scrollTo({ + top: offset, + behavior: 'smooth', + }); + } +}; + const FAQContent = observer(({ faq_list, hide_header = false }: TFAQContent) => { const { dashboard } = useDBotStore(); - const { faq_search_value } = dashboard; + const faq_wrapper_element = React.useRef(null); + const timer_id = React.useRef | null>(null); + + const handleAccordionClick = () => { + // Scroll to the top of the open accordion item. + // Need timer to first close the accordion item then scroll the new item to top. + timer_id.current = setTimeout(() => { + const open_accordion_element: HTMLElement | null | undefined = + faq_wrapper_element.current?.querySelector('.dc-accordion__item--open'); + const previous_sibling_element = open_accordion_element?.previousElementSibling as HTMLElement; + if (faq_wrapper_element.current && open_accordion_element) { + const offset = previous_sibling_element ? previous_sibling_element.offsetTop - 80 : 0; + scrollToElement(faq_wrapper_element?.current, offset); + } + if (timer_id?.current) clearTimeout(timer_id.current); + }, 5); + }; + + React.useEffect(() => { + return () => { + if (timer_id.current) clearTimeout(timer_id.current); + }; + }, []); const getList = () => { return faq_list.map(({ title, description }: TFAQList) => ({ @@ -52,20 +83,24 @@ const FAQContent = observer(({ faq_list, hide_header = false }: TFAQContent) => {title} ), - content: description.map(item => ), + content: description.map((item, index) => ( + + )), })); }; return (
-
+
{!hide_header && ( {localize('FAQ')} )} {faq_list?.length ? ( - +
+ +
) : (
diff --git a/packages/bot-web-ui/src/components/dashboard/tutorial-tab/guide-content.tsx b/packages/bot-web-ui/src/components/dashboard/tutorial-tab/guide-content.tsx index 7f5486abc4d0..429b37a0d8a5 100644 --- a/packages/bot-web-ui/src/components/dashboard/tutorial-tab/guide-content.tsx +++ b/packages/bot-web-ui/src/components/dashboard/tutorial-tab/guide-content.tsx @@ -7,7 +7,7 @@ import { localize } from '@deriv/translations'; import { DBOT_TABS } from 'Constants/bot-contents'; import { useDBotStore } from 'Stores/useDBotStore'; import { removeKeyValue } from '../../../utils/settings'; -import { tour_type } from '../joyride-config'; +import { tour_type } from '../dbot-tours/utils'; type TGuideContent = { guide_list: []; @@ -68,35 +68,34 @@ const GuideContent = observer(({ guide_list }: TGuideContent) => { )}
- {guide_list && - guide_list.map(({ id, content, src, type, subtype }) => { - return ( - type === 'Tour' && ( + {guide_list?.map(({ id, content, src, type, subtype }) => { + return ( + type === 'Tour' && ( +
triggerTour(subtype)} + >
triggerTour(subtype)} + className={classNames('tutorials-wrap__placeholder__tours', { + 'tutorials-wrap__placeholder--disabled': !src, + })} + style={{ + backgroundImage: `url(${src})`, + }} + /> + -
- - {content} - -
- ) - ); - })} + {content} +
+
+ ) + ); + })}
{guide_list?.length > 0 && ( @@ -104,46 +103,45 @@ const GuideContent = observer(({ guide_list }: TGuideContent) => { )}
- {guide_list && - guide_list.map(({ id, content, url, type, src }) => { - return ( - type !== 'Tour' && ( -
-
-
- - showVideoDialog({ - type: 'url', - url, - }) - } - /> -
+ {guide_list?.map(({ id, content, url, type, src }) => { + return ( + type !== 'Tour' && ( +
+
+
+ + showVideoDialog({ + type: 'url', + url, + }) + } + />
- - {content} -
- ) - ); - })} + + {content} + +
+ ) + ); + })} {!guide_list.length && (
diff --git a/packages/bot-web-ui/src/components/dashboard/tutorial-tab/index.scss b/packages/bot-web-ui/src/components/dashboard/tutorial-tab/index.scss index c25ec8616484..edcad55406a2 100644 --- a/packages/bot-web-ui/src/components/dashboard/tutorial-tab/index.scss +++ b/packages/bot-web-ui/src/components/dashboard/tutorial-tab/index.scss @@ -103,17 +103,11 @@ .faq { &__wrapper { overflow: auto; - height: 100vh; - padding-bottom: 20rem; + height: calc(100vh - 180px); + padding-bottom: 18px; @include mobile { - height: calc(100vh - 27rem); - .dc-accordion__wrapper > div:last-child { - //iphone 14 specific - @media (min-height: 450px) and (max-height: 750px) { - padding-bottom: 10rem; - } - } + height: calc(var(--vh) - 280px); } &__nosearch { diff --git a/packages/bot-web-ui/src/components/download/download.scss b/packages/bot-web-ui/src/components/download/download.scss index 29728f4a077d..2de887754279 100644 --- a/packages/bot-web-ui/src/components/download/download.scss +++ b/packages/bot-web-ui/src/components/download/download.scss @@ -5,15 +5,4 @@ padding-left: 16px; border: solid 1px var(--general-section-1); } - &__icon { - align-self: left; - width: 16px; - height: 16px; - } - &__button { - background-color: transparent; - width: 23px; - height: 16px !important; - padding: 0 !important; - } } diff --git a/packages/bot-web-ui/src/components/download/download.tsx b/packages/bot-web-ui/src/components/download/download.tsx index a58e87cb1519..affec4f835a9 100644 --- a/packages/bot-web-ui/src/components/download/download.tsx +++ b/packages/bot-web-ui/src/components/download/download.tsx @@ -27,15 +27,14 @@ const Download = observer(({ tab }: TDownloadProps) => { classNameBubble='run-panel__info--bubble' alignment='bottom' message={popover_message} - zIndex={5} + zIndex='5' >
@@ -87,10 +86,10 @@ const Transactions = observer(({ is_drawer_open }) => { })} >
- {elements.length ? ( + {transaction_list?.length ? ( } keyMapper={row => { switch (row.type) { @@ -106,7 +105,7 @@ const Transactions = observer(({ is_drawer_open }) => { } }} getRowSize={({ index }) => { - const row = elements[index]; + const row = transaction_list?.[index]; switch (row.type) { case transaction_elements.CONTRACT: { return 50; diff --git a/packages/bot-web-ui/src/components/transactions/transactions.scss b/packages/bot-web-ui/src/components/transactions/transactions.scss index 8823e0f5e546..d8a74238333f 100644 --- a/packages/bot-web-ui/src/components/transactions/transactions.scss +++ b/packages/bot-web-ui/src/components/transactions/transactions.scss @@ -3,6 +3,12 @@ $grid-template-columns: 0.8fr 1fr 1.1fr; flex-direction: column; + .download__container { + &__view-detail-button { + margin-left: 12px; + } + } + .transaction-details { &__button-container { display: flex; diff --git a/packages/bot-web-ui/src/public-path.ts b/packages/bot-web-ui/src/public-path.ts index 3c1441472533..7383746cd5be 100644 --- a/packages/bot-web-ui/src/public-path.ts +++ b/packages/bot-web-ui/src/public-path.ts @@ -5,7 +5,7 @@ const getUrlBase = (path = '') => { if (!/^\/(br_)/.test(l.pathname)) return path; - const get_path = /^\//.test(path) ? path : `/${path}`; + const get_path = path.startsWith('/') ? path : `/${path}`; return `/${l.pathname.split('/')[1]}${get_path}`; }; @@ -16,5 +16,26 @@ export function setBotPublicPath(path: string) { export const getImageLocation = (image_name: string) => getUrlBase(`/public/images/common/${image_name}`); +// eslint-disable-next-line import/no-mutable-exports +let initSurvicateCalled = false; +const initSurvicate = () => { + initSurvicateCalled = true; + if (document.getElementById('dbot-survicate')) { + const survicate_box = document.getElementById('survicate-box') || undefined; + if (survicate_box) { + survicate_box.style.display = 'block'; + } + return; + } + + const script = document.createElement('script'); + script.id = 'dbot-survicate'; + script.async = true; + script.src = 'https://survey.survicate.com/workspaces/83b651f6b3eca1ab4551d95760fe5deb/web_surveys.js'; + document.body.appendChild(script); +}; + +export { initSurvicate, initSurvicateCalled }; + setBotPublicPath(getUrlBase('/')); setSmartChartsPublicPath(getUrlBase('/js/smartcharts/')); diff --git a/packages/bot-web-ui/src/stores/app-store.js b/packages/bot-web-ui/src/stores/app-store.js index c2f11a8c52e9..cc8d13314baa 100644 --- a/packages/bot-web-ui/src/stores/app-store.js +++ b/packages/bot-web-ui/src/stores/app-store.js @@ -273,8 +273,17 @@ export default class AppStore { setDBotEngineStores() { // DO NOT pass the rootstore in, if you need a prop define it in dbot-skeleton-store ans pass it through. - const { flyout, toolbar, save_modal, dashboard, quick_strategy, load_modal, blockly_store, summary_card } = - this.root_store; + const { + flyout, + toolbar, + save_modal, + dashboard, + quick_strategy, + load_modal, + run_panel, + blockly_store, + summary_card, + } = this.root_store; const { client } = this.core; const { handleFileChange } = load_modal; const { loadDataStrategy } = quick_strategy; @@ -288,6 +297,7 @@ export default class AppStore { save_modal, dashboard, load_modal, + run_panel, setLoading, setContractUpdateConfig, loadDataStrategy, diff --git a/packages/bot-web-ui/src/stores/dashboard-store.ts b/packages/bot-web-ui/src/stores/dashboard-store.ts index ce69e3804cd4..54710dc0d635 100644 --- a/packages/bot-web-ui/src/stores/dashboard-store.ts +++ b/packages/bot-web-ui/src/stores/dashboard-store.ts @@ -2,7 +2,7 @@ import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { setColors } from '@deriv/bot-skeleton'; import { isMobile } from '@deriv/shared'; import { clearInjectionDiv } from 'Constants/load-modal'; -import { setTourSettings, tour_type, TTourType } from '../components/dashboard/joyride-config'; +import { setTourSettings, tour_type, TTourType } from '../components/dashboard/dbot-tours/utils'; import RootStore from './root-store'; export interface IDashboardStore { @@ -15,6 +15,7 @@ export interface IDashboardStore { has_started_bot_builder_tour: boolean; has_started_onboarding_tour: boolean; has_tour_ended: boolean; + is_web_socket_intialised: boolean; initInfoPanel: () => void; is_dialog_open: boolean; is_file_supported: boolean; @@ -30,6 +31,7 @@ export interface IDashboardStore { setInfoPanelVisibility: (visibility: boolean) => void; setIsFileSupported: (is_file_supported: boolean) => void; setOnBoardTourRunState: (has_started_onboarding_tour: boolean) => void; + setWebSocketState: (is_web_socket_intialised: boolean) => void; setOpenSettings: (toast_message: string, show_toast: boolean) => void; setPreviewOnDialog: (has_mobile_preview_loaded: boolean) => void; setStrategySaveType: (param: string) => void; @@ -63,12 +65,14 @@ export default class DashboardStore implements IDashboardStore { is_info_panel_visible: observable, is_preview_on_popup: observable, is_tour_dialog_visible: observable, + is_web_socket_intialised: observable, is_dark_mode: computed, onCloseDialog: action.bound, onCloseTour: action.bound, onTourEnd: action.bound, setActiveTab: action.bound, setActiveTabTutorial: action.bound, + setWebSocketState: action.bound, setBotBuilderTokenCheck: action.bound, setBotBuilderTourState: action.bound, setFAQSearchValue: action.bound, @@ -130,7 +134,7 @@ export default class DashboardStore implements IDashboardStore { active_tab_tutorials = 0; active_tour_step_number = 0; dialog_options = {}; - faq_search_value = null || ''; + faq_search_value = ''; getFileArray = []; has_builder_token = ''; has_file_loaded = false; @@ -148,6 +152,7 @@ export default class DashboardStore implements IDashboardStore { show_toast = false; strategy_save_type = 'unsaved'; toast_message = ''; + is_web_socket_intialised = true; get is_dark_mode() { const { @@ -160,6 +165,10 @@ export default class DashboardStore implements IDashboardStore { return is_dark_mode_on; } + setWebSocketState = (is_web_socket_intialised: boolean) => { + this.is_web_socket_intialised = is_web_socket_intialised; + }; + setOpenSettings = (toast_message: string, show_toast = true) => { this.toast_message = toast_message; this.show_toast = show_toast; diff --git a/packages/bot-web-ui/src/stores/load-modal-store.ts b/packages/bot-web-ui/src/stores/load-modal-store.ts index 0e34f531a8b3..acf1871b7320 100644 --- a/packages/bot-web-ui/src/stores/load-modal-store.ts +++ b/packages/bot-web-ui/src/stores/load-modal-store.ts @@ -146,7 +146,7 @@ export default class LoadModalStore implements ILoadModalStore { get selected_strategy(): TStrategy { return ( - this.dashboard_strategies.find((ws: { id: string }) => ws.id === this.selected_strategy_id) || + this.dashboard_strategies.find((ws: { id: string }) => ws.id === this.selected_strategy_id) ?? this.dashboard_strategies[0] ); } @@ -290,16 +290,13 @@ export default class LoadModalStore implements ILoadModalStore { onActiveIndexChange = (): void => { if (this.tab_name === tabs_title.TAB_RECENT) { this.previewRecentStrategy(this.selected_strategy_id); - } else { - // eslint-disable-next-line no-lonely-if - if (this.recent_workspace) { - setTimeout(() => { - // Dispose of recent workspace when switching away from Recent tab. - // Process in next cycle so user doesn't have to wait. - this.recent_workspace?.dispose(); - this.recent_workspace = null; - }); - } + } else if (this.recent_workspace) { + setTimeout(() => { + // Dispose of recent workspace when switching away from Recent tab. + // Process in next cycle so user doesn't have to wait. + this.recent_workspace?.dispose(); + this.recent_workspace = null; + }); } if (this.tab_name === tabs_title.TAB_LOCAL) { @@ -310,16 +307,15 @@ export default class LoadModalStore implements ILoadModalStore { this.drop_zone.addEventListener('drop', event => this.handleFileChange(event, false)); } } - } else { - // Dispose of local workspace when switching away from Local tab. - // eslint-disable-next-line no-lonely-if - if (this.local_workspace) { - setTimeout(() => { - this.local_workspace?.dispose(); - this.local_workspace = null; - this.setLoadedLocalFile(null); - }, 0); - } + } + + // Dispose of local workspace when switching away from Local tab. + else if (this.local_workspace) { + setTimeout(() => { + this.local_workspace?.dispose(); + this.local_workspace = null; + this.setLoadedLocalFile(null); + }, 0); } // Forget about drop zone when not on Local tab. @@ -410,7 +406,7 @@ export default class LoadModalStore implements ILoadModalStore { this.recent_workspace.dispose(); this.recent_workspace = null; } - if (!this.recent_workspace || !this.recent_workspace?.rendered) { + if (!this.recent_workspace?.rendered) { this.recent_workspace = Blockly.inject(ref, { media: `${__webpack_public_path__}media/`, zoom: { @@ -494,7 +490,7 @@ export default class LoadModalStore implements ILoadModalStore { }; readFile = (is_preview: boolean, drop_event: DragEvent, file: File): void => { - const file_name = file && file.name.replace(/\.[^/.]+$/, ''); + const file_name = file?.name.replace(/\.[^/.]+$/, ''); const reader = new FileReader(); reader.onload = action(e => { const load_options = { diff --git a/packages/bot-web-ui/src/stores/quick-strategy-store.ts b/packages/bot-web-ui/src/stores/quick-strategy-store.ts index e813330d173a..b7a6476a685d 100644 --- a/packages/bot-web-ui/src/stores/quick-strategy-store.ts +++ b/packages/bot-web-ui/src/stores/quick-strategy-store.ts @@ -93,15 +93,15 @@ export default class QuickStrategyStore { selected_trade_type: TTradeType = (this.qs_cache.selected_trade_type as TTradeType) || {}; selected_type_strategy: TTypeStrategy = (this.qs_cache.selected_type_strategy as TTypeStrategy) || {}; selected_duration_unit: TDurationOptions = (this.qs_cache.selected_duration_unit as TDurationOptions) || {}; - input_duration_value: string | number = this.qs_cache.input_duration_value || ''; - input_stake: string = this.qs_cache.input_stake || ''; - input_martingale_size: string = this.qs_cache.input_martingale_size || ''; - input_alembert_unit: string = this.qs_cache.input_alembert_unit || ''; - input_oscar_unit: string = this.qs_cache.input_oscar_unit || ''; - input_loss: string = this.qs_cache.input_loss || ''; - input_profit: string = this.qs_cache.input_profit || ''; - active_index: number = this.selected_type_strategy.index || 0; - description: string = this.qs_cache.selected_type_strategy?.description || ''; + input_duration_value: string | number = this.qs_cache.input_duration_value ?? ''; + input_stake: string = this.qs_cache.input_stake ?? ''; + input_martingale_size: string = this.qs_cache.input_martingale_size ?? ''; + input_alembert_unit: string = this.qs_cache.input_alembert_unit ?? ''; + input_oscar_unit: string = this.qs_cache.input_oscar_unit ?? ''; + input_loss: string = this.qs_cache.input_loss ?? ''; + input_profit: string = this.qs_cache.input_profit ?? ''; + active_index: number = this.selected_type_strategy.index ?? 0; + description: string = this.qs_cache.selected_type_strategy?.description ?? ''; types_strategies_dropdown: TTypeStrategiesDropdown = []; symbol_dropdown: TSymbolDropdown = []; trade_type_dropdown: TTradeTypeDropdown = []; @@ -139,7 +139,7 @@ export default class QuickStrategyStore { setDescription(type_strategy: TTypeStrategy): void { this.description = - this.types_strategies_dropdown?.find(strategy => strategy.value === type_strategy.value)?.description || ''; + this.types_strategies_dropdown?.find(strategy => strategy.value === type_strategy.value)?.description ?? ''; } setDurationUnitDropdown(duration_unit_options: TDurationUnitDropdown): void { @@ -485,7 +485,7 @@ export default class QuickStrategyStore { let first_duration_unit: TDurationOptions = duration_options[0]; if (this.selected_duration_unit && duration_options?.some(e => e.value === this.selected_duration_unit.value)) { first_duration_unit = - duration_options?.find(e => e.value === this.selected_duration_unit.value) || + duration_options?.find(e => e.value === this.selected_duration_unit.value) ?? this.selected_duration_unit; runInAction(() => { first_duration_unit.text = this.getFieldValue(duration_options, this.selected_duration_unit.value); @@ -496,7 +496,7 @@ export default class QuickStrategyStore { if (first_duration_unit) { this.setSelectedDurationUnit(first_duration_unit); this.updateDurationValue( - this.qs_cache?.selected_duration_unit?.value || this.selected_duration_unit.value, + this.qs_cache?.selected_duration_unit?.value ?? this.selected_duration_unit.value, setFieldValue ); diff --git a/packages/bot-web-ui/src/stores/root-store.ts b/packages/bot-web-ui/src/stores/root-store.ts index 785f18bdc804..582beb41ea4a 100644 --- a/packages/bot-web-ui/src/stores/root-store.ts +++ b/packages/bot-web-ui/src/stores/root-store.ts @@ -1,4 +1,5 @@ -import type { TDbot, TRootStore, TWebSocket } from 'Types'; +import { TStores } from '@deriv/stores/types'; +import type { TDbot, TWebSocket } from 'Types'; import AppStore from './app-store'; import BlocklyStore from './blockly-store'; import ChartStore from './chart-store'; @@ -48,7 +49,7 @@ export default class RootStore { public blockly_store: BlocklyStore; public data_collection_store: DataCollectionStore; - constructor(core: TRootStore, ws: TWebSocket, dbot: TDbot) { + constructor(core: TStores, ws: TWebSocket, dbot: TDbot) { this.ws = ws; this.dbot = dbot; this.app = new AppStore(this, core); diff --git a/packages/bot-web-ui/src/stores/run-panel-store.js b/packages/bot-web-ui/src/stores/run-panel-store.js index e0621ea424db..8638f694440c 100644 --- a/packages/bot-web-ui/src/stores/run-panel-store.js +++ b/packages/bot-web-ui/src/stores/run-panel-store.js @@ -27,6 +27,7 @@ export default class RunPanelStore { is_stop_button_disabled: computed, is_clear_stat_disabled: computed, onStopButtonClick: action.bound, + onStopBotClick: action.bound, stopBot: action.bound, onClearStatClick: action.bound, clearStat: action.bound, @@ -141,7 +142,7 @@ export default class RunPanelStore { return ( this.is_running || this.has_open_contract || - (journal.unfiltered_messages.length === 0 && transactions.elements.length === 0) + (journal.unfiltered_messages.length === 0 && transactions?.transactions?.length === 0) ); } @@ -220,6 +221,16 @@ export default class RunPanelStore { onStopButtonClick() { const { is_multiplier } = this.root_store.summary_card; + + if (is_multiplier) { + this.showStopMultiplierContractDialog(); + } else { + this.stopBot(); + } + } + + onStopBotClick() { + const { is_multiplier } = this.root_store.summary_card; const { summary_card } = this.root_store; if (is_multiplier) { diff --git a/packages/bot-web-ui/src/stores/save-modal-store.ts b/packages/bot-web-ui/src/stores/save-modal-store.ts index 0de8819576ee..5ac3581c7163 100644 --- a/packages/bot-web-ui/src/stores/save-modal-store.ts +++ b/packages/bot-web-ui/src/stores/save-modal-store.ts @@ -160,14 +160,15 @@ export default class SaveModalStore implements ISaveModalStore { this.setButtonStatus(button_status.COMPLETED); } + this.updateBotName(bot_name); + if (active_tab === 0) { - const workspace_id = selected_strategy.id || Blockly?.utils?.genUid(); + const workspace_id = selected_strategy.id ?? Blockly?.utils?.genUid(); await this.addStrategyToWorkspace(workspace_id, is_local, save_as_collection, bot_name, xml); if (main_strategy) await loadStrategyToBuilder(main_strategy); } else { await saveWorkspaceToRecent(xml, is_local ? save_types.LOCAL : save_types.GOOGLE_DRIVE); } - this.updateBotName(bot_name); this.toggleSaveModal(); } diff --git a/packages/bot-web-ui/src/stores/transactions-store.js b/packages/bot-web-ui/src/stores/transactions-store.js index c66fb80f75cf..ebe74cdb5b0b 100644 --- a/packages/bot-web-ui/src/stores/transactions-store.js +++ b/packages/bot-web-ui/src/stores/transactions-store.js @@ -1,6 +1,6 @@ import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { log_types } from '@deriv/bot-skeleton'; -import { formatDate, isBot, isEnded } from '@deriv/shared'; +import { formatDate, isEnded } from '@deriv/shared'; import { transaction_elements } from '../constants/transactions'; import { getStoredItemsByKey, getStoredItemsByUser, setStoredItemsByKey } from '../utils/session-storage'; @@ -34,10 +34,9 @@ export default class TransactionsStore { this.core = core; this.disposeReactionsFn = this.registerReactions(); } - TRANSACTION_CACHE = 'transaction_cache'; - elements = getStoredItemsByUser(this.TRANSACTION_CACHE, this.core?.client.loginid, []); + elements = getStoredItemsByUser(this.TRANSACTION_CACHE, this.core?.client?.loginid, []); active_transaction_id = null; recovered_completed_transactions = []; recovered_transactions = []; @@ -45,7 +44,11 @@ export default class TransactionsStore { is_transaction_details_modal_open = false; get transactions() { - return this.elements.filter(element => element.type === transaction_elements.CONTRACT); + return ( + this.elements[this.core?.client?.loginid]?.filter( + element => element.type === transaction_elements.CONTRACT + ) ?? [] + ); } toggleTransactionDetailsModal(is_open) { @@ -59,6 +62,7 @@ export default class TransactionsStore { pushTransaction(data) { const is_completed = isEnded(data); const { run_id } = this.root_store.run_panel; + const current_account = this.core?.client?.loginid; const contract = { barrier: data.barrier, buy_price: data.buy_price, @@ -83,7 +87,14 @@ export default class TransactionsStore { underlying: data.underlying, }; - const same_contract_index = this.elements.findIndex( + if (!this.elements[current_account]) { + this.elements = { + ...this.elements, + [current_account]: [], + }; + } + + const same_contract_index = this.elements[current_account]?.findIndex( c => c.type === transaction_elements.CONTRACT && c.data.transaction_ids && @@ -92,32 +103,32 @@ export default class TransactionsStore { if (same_contract_index === -1) { // Render a divider if the "run_id" for this contract is different. - if (this.elements.length > 0) { + if (this.elements[current_account]?.length > 0) { const is_new_run = - this.elements[0].type === transaction_elements.CONTRACT && - contract.run_id !== this.elements[0].data.run_id; + this.elements[current_account]?.[0].type === transaction_elements.CONTRACT && + contract.run_id !== this.elements[current_account]?.[0].data.run_id; if (is_new_run) { - this.elements.unshift({ + this.elements[current_account]?.unshift({ type: transaction_elements.DIVIDER, data: contract.run_id, }); } } - this.elements.unshift({ + this.elements[current_account]?.unshift({ type: transaction_elements.CONTRACT, data: contract, }); } else { // If data belongs to existing contract in memory, update it. - this.elements.splice(same_contract_index, 1, { + this.elements[current_account]?.splice(same_contract_index, 1, { type: transaction_elements.CONTRACT, data: contract, }); } - this.elements = this.elements.slice(); // force array update + this.elements = { ...this.elements }; // force update } setActiveTransactionId(transaction_id) { @@ -149,9 +160,9 @@ export default class TransactionsStore { } clear() { - this.elements = this.elements.slice(0, 0); - this.recovered_completed_transactions = this.recovered_completed_transactions.slice(0, 0); - this.recovered_transactions = this.recovered_transactions.slice(0, 0); + this.elements[this.core?.client?.loginid] = []; + this.recovered_completed_transactions = this.recovered_completed_transactions?.slice(0, 0); + this.recovered_transactions = this.recovered_transactions?.slice(0, 0); this.is_transaction_details_modal_open = false; } @@ -160,10 +171,10 @@ export default class TransactionsStore { // Write transactions to session storage on each change in transaction elements. const disposeTransactionElementsListener = reaction( - () => this.elements, + () => this.elements[client?.loginid], elements => { const stored_transactions = getStoredItemsByKey(this.TRANSACTION_CACHE, {}); - stored_transactions[client.loginid] = elements.slice(0, 5000); + stored_transactions[client.loginid] = elements?.slice(0, 5000) ?? []; setStoredItemsByKey(this.TRANSACTION_CACHE, stored_transactions); } ); @@ -176,24 +187,16 @@ export default class TransactionsStore { () => this.recoverPendingContracts() ); - const disposeSwitchAcountListener = reaction( - () => client.loginid, - () => this.clear() - ); - return () => { disposeTransactionElementsListener(); disposeRecoverContracts(); - if (typeof this.disposeSwitchAcountListener === 'function') { - disposeSwitchAcountListener(); - } }; } - recoverPendingContracts() { + recoverPendingContracts(contract = null) { this.transactions.forEach(({ data: trx }) => { if (trx.is_completed || this.recovered_transactions.includes(trx.contract_id)) return; - this.recoverPendingContractsById(trx.contract_id); + this.recoverPendingContractsById(trx.contract_id, contract); }); } @@ -228,28 +231,23 @@ export default class TransactionsStore { }); } - recoverPendingContractsById(contract_id) { - const { ws } = this.root_store; + recoverPendingContractsById(contract_id, contract = null) { const positions = this.core.portfolio.positions; - // TODO: the idea is to remove the POC calls completely - // but adding this check to prevent making POC calls only for bot as of now - if (!isBot()) { - ws.authorized.subscribeProposalOpenContract(contract_id, response => { - this.is_called_proposal_open_contract = true; - if (!response.error) { - const { proposal_open_contract } = response; - this.updateResultsCompletedContract(proposal_open_contract); - } - }); + if (contract) { + this.is_called_proposal_open_contract = true; + if (contract.contract_id === contract_id) { + this.updateResultsCompletedContract(contract); + } } if (!this.is_called_proposal_open_contract) { - if (!this.elements.length) { + const current_account = this.core?.client?.loginid; + if (!this.elements[current_account]?.length) { this.sortOutPositionsBeforeAction(positions); } - if (this.elements.length && !this.elements[0].data.profit) { - const element_id = this.elements[0].data.contract_id; + if (this.elements[current_account]?.length && !this.elements[current_account]?.[0]?.data?.profit) { + const element_id = this.elements[current_account]?.[0].data.contract_id; this.sortOutPositionsBeforeAction(positions, element_id); } } diff --git a/packages/bot-web-ui/src/types/root-stores.types.ts b/packages/bot-web-ui/src/types/root-stores.types.ts deleted file mode 100644 index 4473cd37193d..000000000000 --- a/packages/bot-web-ui/src/types/root-stores.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TCoreStores } from '@deriv/stores/types'; - -/** - * @deprecated - Use `TStores` from `@deriv/stores` instead of this type. - */ -export type TRootStore = TCoreStores & { - gtm?: Record; - portfolio?: Record; -}; diff --git a/packages/bot-web-ui/src/utils/mock/ws-mock.js b/packages/bot-web-ui/src/utils/mock/ws-mock.js index 16dc69b034af..cca6b57a9861 100644 --- a/packages/bot-web-ui/src/utils/mock/ws-mock.js +++ b/packages/bot-web-ui/src/utils/mock/ws-mock.js @@ -2,6 +2,7 @@ export const mock_ws = { authorized: { subscribeProposalOpenContract: jest.fn(), send: jest.fn(), + activeSymbols: jest.fn(() => Promise.resolve({ active_symbols: [] })), }, storage: { send: jest.fn(), @@ -11,4 +12,5 @@ export const mock_ws = { forgetStream: jest.fn(), activeSymbols: jest.fn(), send: jest.fn(), + tradingTimes: jest.fn(() => Promise.resolve({ error: true })), }; diff --git a/packages/bot-web-ui/src/utils/window-size.js b/packages/bot-web-ui/src/utils/window-size.js index 944fb42c972a..f6da91873fd8 100644 --- a/packages/bot-web-ui/src/utils/window-size.js +++ b/packages/bot-web-ui/src/utils/window-size.js @@ -10,3 +10,10 @@ export const setMainContentWidth = is_run_panel_open => { const width = is_run_panel_open ? 'calc(100vw - 366px)' : 'calc(100vw - 16px)'; return document.getElementsByClassName('bot')[0].style.setProperty('--bot-content-width', width); }; + +export const setInnerHeightToVariable = () => { + // Setting the inner height of the document to the --vh variable to fix the issue + // of dynamic view height(vh) on mobile browsers for few scrollable components + const vh = window.innerHeight; + document.body.style.setProperty('--vh', `${vh}px`); +}; diff --git a/packages/cashier/src/components/cashier-container/real/real.tsx b/packages/cashier/src/components/cashier-container/real/real.tsx index 35fde8c48185..7957b165eaa8 100644 --- a/packages/cashier/src/components/cashier-container/real/real.tsx +++ b/packages/cashier/src/components/cashier-container/real/real.tsx @@ -25,7 +25,7 @@ const Real = observer(() => { }, [checkIframeLoaded, is_dark_mode_on, setContainerHeight]); return ( -
+ {should_show_loader && } {iframe_url && (