diff --git a/packages/account/src/Containers/Account/__tests__/account.spec.tsx b/packages/account/src/Containers/Account/__tests__/account.spec.tsx
new file mode 100644
index 000000000000..984e4e37c1ca
--- /dev/null
+++ b/packages/account/src/Containers/Account/__tests__/account.spec.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { MemoryRouter, BrowserRouter } from 'react-router-dom';
+import { render, screen } from '@testing-library/react';
+import { StoreProvider, mockStore } from '@deriv/stores';
+import { routes } from '@deriv/shared';
+import { TRoute } from 'Types';
+import Account from '../account';
+
+jest.mock('../../Account/page-overlay-wrapper', () => jest.fn(() =>
MockPageOverlayWrapper
));
+
+jest.mock('@deriv/components', () => ({
+ ...jest.requireActual('@deriv/components'),
+ Loading: () => MockLoading
,
+}));
+
+describe('Account', () => {
+ const store = mockStore({
+ ui: {
+ is_account_settings_visible: true,
+ },
+ });
+
+ const route_list: Array = [
+ {
+ getTitle: () => 'Profile',
+ icon: 'mockIcon',
+ subroutes: [
+ {
+ path: routes.personal_details,
+ component: () => MockPersonalDetails
,
+ getTitle: () => 'Personal details',
+ default: true,
+ },
+ {
+ path: routes.trading_assessment,
+ component: () => MockTradeAssessment
,
+ getTitle: () => 'Trade assessment',
+ },
+ ],
+ },
+ ];
+
+ const mock_props: React.ComponentProps = {
+ routes: route_list,
+ };
+
+ const mock_route = routes.personal_details;
+
+ const renderComponent = ({ store_config = store, route = mock_route, props = mock_props }) =>
+ render(
+
+
+
+
+
+
+
+ );
+
+ it('should render account page', () => {
+ renderComponent({});
+ expect(screen.getByText('MockPageOverlayWrapper')).toBeInTheDocument();
+ });
+
+ it('should render loader while the client is still logging in', () => {
+ const new_store_config = mockStore({
+ client: {
+ is_logging_in: true,
+ },
+ });
+
+ renderComponent({ store_config: new_store_config });
+ expect(screen.getByText('MockLoading')).toBeInTheDocument();
+ });
+});
diff --git a/packages/account/src/Containers/Account/__tests__/tradinghub-logout.spec.tsx b/packages/account/src/Containers/Account/__tests__/tradinghub-logout.spec.tsx
new file mode 100644
index 000000000000..985f7a20ca8b
--- /dev/null
+++ b/packages/account/src/Containers/Account/__tests__/tradinghub-logout.spec.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import TradingHubLogout from '../tradinghub-logout';
+
+describe('TradingHubLogout', () => {
+ const mock_props: React.ComponentProps = {
+ handleOnLogout: jest.fn(),
+ };
+
+ it('should render logout tab', () => {
+ render();
+ expect(screen.getByText('Log out')).toBeInTheDocument();
+ });
+
+ it('should invoke handleOnLogout when logout tab is clicked', () => {
+ render();
+ const el_tab = screen.getByTestId('dt_logout_tab');
+ userEvent.click(el_tab);
+ expect(mock_props.handleOnLogout).toBeCalledTimes(1);
+ });
+});
diff --git a/packages/account/src/Containers/Account/account.tsx b/packages/account/src/Containers/Account/account.tsx
new file mode 100644
index 000000000000..3883eaf8bc11
--- /dev/null
+++ b/packages/account/src/Containers/Account/account.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import { FadeWrapper, Loading } from '@deriv/components';
+import { matchRoute, routes as shared_routes } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
+import PageOverlayWrapper from './page-overlay-wrapper';
+import { TRoute } from '../../Types';
+import 'Styles/account.scss';
+
+type TAccountProps = RouteComponentProps & {
+ routes: Array;
+};
+
+/**
+ * Component that renders the account section
+ * @name Account
+ * @param history - history object passed from react-router-dom
+ * @param location - location object passed from react-router-dom
+ * @param routes - routes object passed from react-router-dom
+ * @returns React component
+ */
+const Account = observer(({ history, location, routes }: TAccountProps) => {
+ const { client, ui } = useStore();
+ const {
+ is_virtual,
+ is_logged_in,
+ is_logging_in,
+ is_pending_proof_of_ownership,
+ landing_company_shortcode,
+ should_allow_authentication,
+ } = client;
+ const { toggleAccountSettings, is_account_settings_visible } = ui;
+ const subroutes = routes.map(i => i.subroutes);
+ let selected_content = subroutes.find(r => matchRoute(r, location.pathname));
+
+ React.useEffect(() => {
+ toggleAccountSettings(true);
+ }, [toggleAccountSettings]);
+
+ routes.forEach(menu_item => {
+ if (menu_item?.subroutes?.length) {
+ menu_item.subroutes.forEach(route => {
+ if (route.path === shared_routes.financial_assessment) {
+ route.is_disabled = is_virtual || landing_company_shortcode === 'maltainvest';
+ }
+
+ if (route.path === shared_routes.trading_assessment) {
+ route.is_disabled = is_virtual || landing_company_shortcode !== 'maltainvest';
+ }
+
+ if (route.path === shared_routes.proof_of_identity || route.path === shared_routes.proof_of_address) {
+ route.is_disabled = !should_allow_authentication;
+ }
+
+ if (route.path === shared_routes.proof_of_ownership) {
+ route.is_disabled = is_virtual || !is_pending_proof_of_ownership;
+ }
+ });
+ }
+ });
+
+ if (!selected_content) {
+ // fallback
+ selected_content = subroutes[0];
+ history.push(shared_routes.personal_details);
+ }
+
+ if (!is_logged_in && is_logging_in) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+});
+
+Account.displayName = 'Account';
+
+export default withRouter(Account);
diff --git a/packages/account/src/Containers/Account/page-overlay-wrapper.tsx b/packages/account/src/Containers/Account/page-overlay-wrapper.tsx
new file mode 100644
index 000000000000..658edbe162bc
--- /dev/null
+++ b/packages/account/src/Containers/Account/page-overlay-wrapper.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { PageOverlay, VerticalTab } from '@deriv/components';
+import { getSelectedRoute, getStaticUrl, routes as shared_routes } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
+import { Localize } from '@deriv/translations';
+import TradingHubLogout from './tradinghub-logout';
+import { TRoute } from '../../Types';
+
+type RouteItems = React.ComponentProps['list'];
+
+type PageOverlayWrapperProps = {
+ routes: Array;
+ subroutes: RouteItems;
+};
+
+/**
+ * @name PageOverlayWrapper
+ * @param routes - routes object pased by react-router-dom
+ * @param subroutes - list of subroutes
+ */
+const PageOverlayWrapper = observer(({ routes, subroutes }: PageOverlayWrapperProps) => {
+ const history = useHistory();
+ const { client, common, ui } = useStore();
+ const { is_mobile } = ui;
+ const { logout } = client;
+ const { is_from_derivgo, routeBackInApp } = common;
+
+ const list_groups = routes.map(route_group => ({
+ icon: route_group.icon,
+ label: route_group?.getTitle(),
+ subitems: route_group?.subroutes?.length ? route_group.subroutes.map(sub => subroutes.indexOf(sub)) : [],
+ }));
+
+ const onClickClose = React.useCallback(() => routeBackInApp(history), [routeBackInApp, history]);
+
+ const selected_route = getSelectedRoute({ routes: subroutes as Array, pathname: location.pathname });
+
+ const onClickLogout = () => {
+ history.push(shared_routes.index);
+ logout().then(() => (window.location.href = getStaticUrl('/')));
+ };
+
+ if (is_mobile && selected_route) {
+ const RouteComponent = selected_route.component as React.ElementType<{ component_icon: string | undefined }>;
+ return (
+
+
+
+ );
+ }
+ return (
+ }
+ onClickClose={onClickClose}
+ is_from_app={is_from_derivgo}
+ >
+ }
+ />
+
+ );
+});
+
+PageOverlayWrapper.displayName = 'PageOverlayWrapper';
+
+export default PageOverlayWrapper;
diff --git a/packages/account/src/Containers/Account/tradinghub-logout.tsx b/packages/account/src/Containers/Account/tradinghub-logout.tsx
new file mode 100644
index 000000000000..208142984b18
--- /dev/null
+++ b/packages/account/src/Containers/Account/tradinghub-logout.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Text, Icon } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+
+/**
+ * Content to be displayed in the side bar
+ * @name TradingHubLogout
+ * @param handleOnLogout - function to handle action when user click on logout
+ * @returns React Component
+ */
+const TradingHubLogout = ({ handleOnLogout }: { handleOnLogout: () => void }) => (
+
+);
+
+export default TradingHubLogout;
diff --git a/packages/account/src/Containers/account.jsx b/packages/account/src/Containers/account.jsx
deleted file mode 100644
index a580baa1d4f9..000000000000
--- a/packages/account/src/Containers/account.jsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import 'Styles/account.scss';
-import {
- PlatformContext,
- getSelectedRoute,
- getStaticUrl,
- isMobile,
- matchRoute,
- routes as shared_routes,
-} from '@deriv/shared';
-import PropTypes from 'prop-types';
-import React from 'react';
-import { withRouter } from 'react-router-dom';
-import { VerticalTab, FadeWrapper, PageOverlay, Loading, Text, Icon } from '@deriv/components';
-import { observer, useStore } from '@deriv/stores';
-import { flatten } from '../Helpers/flatten';
-import { localize } from '@deriv/translations';
-
-const onClickLogout = (logout, history) => {
- history.push(shared_routes.index);
- logout().then(() => (window.location.href = getStaticUrl('/')));
-};
-
-const AccountLogout = ({ logout, history }) => {
- return (
- onClickLogout(logout, history)}>
-
-
- {localize('Log out')}
-
-
-
- );
-};
-
-const TradingHubLogout = ({ logout, history }) => {
- return (
- onClickLogout(logout, history)}>
-
-
-
- {localize('Log out')}
-
-
-
- );
-};
-
-const PageOverlayWrapper = ({
- is_from_derivgo,
- is_appstore,
- list_groups,
- logout,
- onClickClose,
- selected_route,
- subroutes,
- history,
-}) => {
- const routeToPrevious = () => history.push(shared_routes.traders_hub);
-
- if (isMobile() && selected_route) {
- return (
-
-
-
- );
- } else if (is_appstore) {
- return (
- }
- />
- );
- }
-
- return (
-
- }
- />
-
- );
-};
-
-const Account = observer(({ history, location, routes }) => {
- const { client, common, ui } = useStore();
- const {
- is_virtual,
- is_logged_in,
- is_logging_in,
- is_risky_client,
- is_pending_proof_of_ownership,
- landing_company_shortcode,
- should_allow_authentication,
- logout,
- } = client;
- const { is_from_derivgo, routeBackInApp, platform } = common;
- const { toggleAccountSettings, is_account_settings_visible } = ui;
- const { is_appstore } = React.useContext(PlatformContext);
- const subroutes = flatten(routes.map(i => i.subroutes));
- let list_groups = [...routes];
- list_groups = list_groups.map(route_group => ({
- icon: route_group.icon,
- label: route_group.getTitle(),
- subitems: route_group.subroutes.map(sub => subroutes.indexOf(sub)),
- }));
- let selected_content = subroutes.find(r => matchRoute(r, location.pathname));
- const onClickClose = React.useCallback(() => routeBackInApp(history), [routeBackInApp, history]);
-
- React.useEffect(() => {
- toggleAccountSettings(true);
- }, [toggleAccountSettings]);
-
- routes.forEach(menu_item => {
- menu_item.subroutes.forEach(route => {
- if (route.path === shared_routes.financial_assessment) {
- route.is_disabled = is_virtual || (landing_company_shortcode === 'maltainvest' && !is_risky_client);
- }
-
- if (route.path === shared_routes.trading_assessment) {
- route.is_disabled = is_virtual || landing_company_shortcode !== 'maltainvest';
- }
-
- if (route.path === shared_routes.proof_of_identity || route.path === shared_routes.proof_of_address) {
- route.is_disabled = !should_allow_authentication;
- }
-
- if (route.path === shared_routes.proof_of_ownership) {
- route.is_disabled = is_virtual || !is_pending_proof_of_ownership;
- }
- });
- });
-
- if (!selected_content) {
- // fallback
- selected_content = subroutes[0];
- history.push(shared_routes.personal_details);
- }
-
- if (!is_logged_in && is_logging_in) {
- return ;
- }
-
- const selected_route = getSelectedRoute({ routes: subroutes, pathname: location.pathname });
-
- return (
-
-
-
- );
-});
-
-Account.propTypes = {
- active_account_landing_company: PropTypes.string,
- history: PropTypes.object,
- location: PropTypes.object,
- routes: PropTypes.arrayOf(PropTypes.object),
-};
-
-export default withRouter(Account);
diff --git a/packages/account/src/Helpers/flatten.js b/packages/account/src/Helpers/flatten.js
deleted file mode 100644
index d9f0a696b24f..000000000000
--- a/packages/account/src/Helpers/flatten.js
+++ /dev/null
@@ -1 +0,0 @@
-export const flatten = arr => [].concat(...arr);
diff --git a/packages/account/src/Sections/index.js b/packages/account/src/Sections/index.js
index 0326508f030b..e0f1bdd1814a 100644
--- a/packages/account/src/Sections/index.js
+++ b/packages/account/src/Sections/index.js
@@ -9,7 +9,7 @@ import ProofOfOwnership from 'Sections/Verification/ProofOfOwnership';
import TwoFactorAuthentication from 'Sections/Security/TwoFactorAuthentication';
import ApiToken from 'Sections/Security/ApiToken';
import SelfExclusion from 'Sections/Security/SelfExclusion';
-import Account from 'Containers/account.jsx';
+import Account from 'Containers/Account/account';
import ClosingAccount from 'Sections/Security/ClosingAccount';
import ConnectedApps from 'Sections/Security/ConnectedApps';
import LoginHistory from 'Sections/Security/LoginHistory';
diff --git a/packages/account/src/Types/common-prop.type.ts b/packages/account/src/Types/common-prop.type.ts
index 5298105baa8b..2717c3450461 100644
--- a/packages/account/src/Types/common-prop.type.ts
+++ b/packages/account/src/Types/common-prop.type.ts
@@ -65,8 +65,9 @@ export type TRoute = {
icon?: string;
default?: boolean;
to?: string;
- component?: ((cashier_routes?: TRoute[]) => JSX.Element) | typeof Redirect;
+ component?: ((routes?: TRoute[]) => JSX.Element) | typeof Redirect;
getTitle?: () => string;
+ is_disabled?: boolean;
subroutes?: TRoute[];
};
diff --git a/packages/components/src/components/vertical-tab/vertical-tab.tsx b/packages/components/src/components/vertical-tab/vertical-tab.tsx
index 14722e85f493..ba870ca7e1f9 100644
--- a/packages/components/src/components/vertical-tab/vertical-tab.tsx
+++ b/packages/components/src/components/vertical-tab/vertical-tab.tsx
@@ -39,7 +39,7 @@ type TVerticalTab = {
list_groups?: TItem[];
onClickClose?: () => void;
setVerticalTabIndex?: (index: number) => void;
- tab_headers_note: React.ReactNode | React.ReactNode[];
+ tab_headers_note?: React.ReactNode | React.ReactNode[];
title?: string;
vertical_tab_index?: number;
};
diff --git a/packages/shared/src/utils/route/route.ts b/packages/shared/src/utils/route/route.ts
index 905cb381e859..de927db7d182 100644
--- a/packages/shared/src/utils/route/route.ts
+++ b/packages/shared/src/utils/route/route.ts
@@ -1,8 +1,8 @@
// Checks if pathname matches route. (Works even with query string /?)
-import React from 'react';
+
// TODO: Add test cases for this
-type TRoute = {
- component?: JSX.Element | null;
+export type TRoute = {
+ component?: React.ElementType | null;
default?: boolean;
exact?: boolean;
getTitle?: () => string;
@@ -19,7 +19,8 @@ type TGetSelectedRoute = {
pathname: string;
};
-export const matchRoute = (route: TRoute, pathname: string) => new RegExp(`^${route.path}(/.*)?$`).test(pathname);
+// @ts-expect-error as this is a utility function with dynamic types
+export const matchRoute = (route: T, pathname: string) => new RegExp(`^${route?.path}(/.*)?$`).test(pathname);
export const getSelectedRoute = ({ routes, pathname }: TGetSelectedRoute) => {
const matching_route = routes.find(route => matchRoute(route, pathname));
diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts
index da779c6c7307..f2ba74649e5d 100644
--- a/packages/stores/src/mockStore.ts
+++ b/packages/stores/src/mockStore.ts
@@ -279,6 +279,7 @@ const mock = (): TStores & { is_mock: boolean } => {
current: null,
},
current_focus: null,
+ is_account_settings_visible: false,
is_loading: false,
is_cashier_visible: false,
is_app_disabled: false,
@@ -296,6 +297,7 @@ const mock = (): TStores & { is_mock: boolean } => {
enableApp: jest.fn(),
setCurrentFocus: jest.fn(),
toggleAccountsDialog: jest.fn(),
+ toggleAccountSettings: jest.fn(),
toggleCashier: jest.fn(),
togglePositionsDrawer: jest.fn(),
setDarkMode: jest.fn(),
diff --git a/packages/stores/types.ts b/packages/stores/types.ts
index 20687872a115..b4d33c464ad7 100644
--- a/packages/stores/types.ts
+++ b/packages/stores/types.ts
@@ -437,6 +437,7 @@ type TUiStore = {
disableApp: () => void;
enableApp: () => void;
has_real_account_signup_ended: boolean;
+ is_account_settings_visible: boolean;
is_loading: boolean;
is_cashier_visible: boolean;
is_closing_create_real_account_modal: boolean;
@@ -464,6 +465,7 @@ type TUiStore = {
setSubSectionIndex: (index: number) => void;
shouldNavigateAfterChooseCrypto: (value: Omit | TRoutes) => void;
toggleAccountsDialog: () => void;
+ toggleAccountSettings: (props?: boolean) => void;
toggleCashier: () => void;
toggleLanguageSettingsModal: () => void;
toggleLinkExpiredModal: (state_change: boolean) => void;