diff --git a/packages/components/src/components/dialog/dialog.tsx b/packages/components/src/components/dialog/dialog.tsx index df97b7dad99e..85e9a8c5fa0c 100644 --- a/packages/components/src/components/dialog/dialog.tsx +++ b/packages/components/src/components/dialog/dialog.tsx @@ -26,7 +26,7 @@ type TDialog = { onConfirm: () => void; onEscapeButtonCancel?: () => void; portal_element_id?: string; - title?: string; + title?: React.ReactNode; }; const Dialog = ({ diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index 9f43a6e13d9d..6801ccba9ee6 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -306,6 +306,7 @@ const mock = (): TStores & { is_mock: boolean } => { setHasOnlyForwardingContracts: jest.fn(), setIsClosingCreateRealAccountModal: jest.fn(), setRealAccountSignupEnd: jest.fn(), + setPromptHandler: jest.fn(), setPurchaseState: jest.fn(), setAppContentsScrollRef: jest.fn(), shouldNavigateAfterChooseCrypto: jest.fn(), @@ -441,6 +442,7 @@ const mock = (): TStores & { is_mock: boolean } => { onClickCancel: jest.fn(), onClickSell: jest.fn(), onMount: jest.fn(), + onUnmount: jest.fn(), positions: [], removePositionById: jest.fn(), }, diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 193d772e6356..44af228a7718 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -461,6 +461,10 @@ type TUiStore = { setRealAccountSignupEnd: (status: boolean) => void; setPurchaseState: (index: number) => void; sub_section_index: number; + setPromptHandler: ( + condition: boolean, + cb?: (() => void) | ((route_to: RouteComponentProps['location'], action: string) => boolean) + ) => void; setSubSectionIndex: (index: number) => void; shouldNavigateAfterChooseCrypto: (value: Omit | TRoutes) => void; toggleAccountsDialog: () => void; @@ -550,6 +554,7 @@ type TPortfolioStore = { onClickCancel: (contract_id?: number) => void; onClickSell: (contract_id?: number) => void; onMount: () => void; + onUnmount: () => void; positions: TPortfolioPosition[]; removePositionById: (id: number) => void; }; diff --git a/packages/trader/src/App/Components/Elements/Errors/error-component.tsx b/packages/trader/src/App/Components/Elements/Errors/error-component.tsx index b5485b0791c9..9ecb286b1f00 100644 --- a/packages/trader/src/App/Components/Elements/Errors/error-component.tsx +++ b/packages/trader/src/App/Components/Elements/Errors/error-component.tsx @@ -4,11 +4,11 @@ import { routes } from '@deriv/shared'; import { localize } from '@deriv/translations'; type TErrorComponent = { - header: string; + header: React.ReactNode; message: React.ReactNode; is_dialog: boolean; redirect_label: string; - redirectOnClick: () => void; + redirectOnClick: (() => void) | null; should_show_refresh: boolean; }; diff --git a/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js b/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.ts similarity index 91% rename from packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js rename to packages/trader/src/App/Components/Routes/__tests__/helpers.spec.ts index ef0eebbbfef0..d4f6ec74beaa 100644 --- a/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js +++ b/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.ts @@ -1,4 +1,3 @@ -import React from 'react'; import * as Helpers from '../helpers'; import { routes } from '@deriv/shared'; import getRoutesConfig from '../../../Constants/routes-config'; @@ -23,9 +22,9 @@ describe('Helpers', () => { }); it('should return route_info when path is in routes_config and is not nested', () => { const result = Helpers.findRouteByPath(routes.trade, getRoutesConfig()); - expect(result.path).toBe(routes.trade); - expect(result.exact).toBe(true); - expect(result.component).toBe(Trade); + expect(result?.path).toBe(routes.trade); + expect(result?.exact).toBe(true); + expect(result?.component).toBe(Trade); }); }); @@ -46,7 +45,7 @@ describe('Helpers', () => { describe('getPath', () => { it('should return param values in params as a part of path', () => { - expect(Helpers.getPath('/contract/:contract_id', { contract_id: 37511105068 })).toBe( + expect(Helpers.getPath('/contract/:contract_id', { contract_id: '37511105068' })).toBe( '/contract/37511105068' ); expect( @@ -63,7 +62,7 @@ describe('Helpers', () => { describe('getContractPath', () => { it('should return the path of contract with contract_id passed', () => { - expect(Helpers.getContractPath(1234)).toBe('/contract/1234'); + expect(Helpers.getContractPath('1234')).toBe('/contract/1234'); }); }); }); diff --git a/packages/trader/src/App/Components/Routes/binary-link.jsx b/packages/trader/src/App/Components/Routes/binary-link.tsx similarity index 76% rename from packages/trader/src/App/Components/Routes/binary-link.jsx rename to packages/trader/src/App/Components/Routes/binary-link.tsx index 43d66e6c5d4e..b5bfae58f0fa 100644 --- a/packages/trader/src/App/Components/Routes/binary-link.jsx +++ b/packages/trader/src/App/Components/Routes/binary-link.tsx @@ -1,13 +1,17 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { NavLink } from 'react-router-dom'; import { findRouteByPath, normalizePath } from './helpers'; import getRoutesConfig from '../../Constants/routes-config'; +type TBinaryLinkProps = React.PropsWithChildren<{ + active_class?: string; + to?: string; +}>; + // TODO: solve circular dependency problem // when binary link is imported into components present in routes config // or into their descendants -const BinaryLink = ({ active_class, to, children, ...props }) => { +const BinaryLink = ({ active_class, to, children, ...props }: TBinaryLinkProps) => { const path = normalizePath(to); const route = findRouteByPath(path, getRoutesConfig()); @@ -32,10 +36,4 @@ const BinaryLink = ({ active_class, to, children, ...props }) => { ); }; -BinaryLink.propTypes = { - active_class: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]), - to: PropTypes.string, -}; - export default BinaryLink; diff --git a/packages/trader/src/App/Components/Routes/binary-routes.jsx b/packages/trader/src/App/Components/Routes/binary-routes.jsx deleted file mode 100644 index 0bce70a4cc0d..000000000000 --- a/packages/trader/src/App/Components/Routes/binary-routes.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { Switch } from 'react-router-dom'; -import getRoutesConfig from 'App/Constants/routes-config'; -import RouteWithSubRoutes from './route-with-sub-routes.jsx'; - -const BinaryRoutes = props => ( - }> - - {getRoutesConfig().map((route, idx) => ( - - ))} - - -); - -export default BinaryRoutes; diff --git a/packages/trader/src/App/Components/Routes/binary-routes.tsx b/packages/trader/src/App/Components/Routes/binary-routes.tsx new file mode 100644 index 000000000000..eebaeae947ed --- /dev/null +++ b/packages/trader/src/App/Components/Routes/binary-routes.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; +import getRoutesConfig from 'App/Constants/routes-config'; +import { TBinaryRoutesProps, TRouteConfig } from 'Types'; +import RouteWithSubRoutes from './route-with-sub-routes'; + +const BinaryRoutes = (props: TBinaryRoutesProps) => ( + }> + + {getRoutesConfig().map((route: TRouteConfig) => ( + + ))} + + +); + +export default BinaryRoutes; diff --git a/packages/trader/src/App/Components/Routes/helpers.js b/packages/trader/src/App/Components/Routes/helpers.js deleted file mode 100644 index 8d8bcc421c1d..000000000000 --- a/packages/trader/src/App/Components/Routes/helpers.js +++ /dev/null @@ -1,37 +0,0 @@ -import { matchPath } from 'react-router'; -import { routes } from '@deriv/shared'; - -export const normalizePath = path => (/^\//.test(path) ? path : `/${path || ''}`); // Default to '/' - -export const findRouteByPath = (path, routes_config) => { - let result; - - routes_config.some(route_info => { - let match_path; - try { - match_path = matchPath(path, route_info); - } catch (e) { - if (/undefined/.test(e.message)) { - return undefined; - } - } - - if (match_path) { - result = route_info; - return true; - } else if (route_info.routes) { - result = findRouteByPath(path, route_info.routes); - return result; - } - return false; - }); - - return result; -}; - -export const isRouteVisible = (route, is_logged_in) => !(route && route.is_authenticated && !is_logged_in); - -export const getPath = (route_path, params = {}) => - Object.keys(params).reduce((p, name) => p.replace(`:${name}`, params[name]), route_path); - -export const getContractPath = contract_id => getPath(routes.contract, { contract_id }); diff --git a/packages/trader/src/App/Components/Routes/helpers.ts b/packages/trader/src/App/Components/Routes/helpers.ts new file mode 100644 index 000000000000..71b151950378 --- /dev/null +++ b/packages/trader/src/App/Components/Routes/helpers.ts @@ -0,0 +1,39 @@ +import { matchPath, RouteProps } from 'react-router'; +import { routes } from '@deriv/shared'; +import { TRouteConfig } from 'Types'; + +export const normalizePath = (path = '') => (/^\//.test(path) ? path : `/${path || ''}`); // Default to '/' + +export const findRouteByPath = (path: string, routes_config?: TRouteConfig[]): RouteProps | undefined => { + let result: RouteProps | undefined; + + routes_config?.some(route_info => { + let match_path; + try { + match_path = matchPath(path, route_info); + } catch (e: unknown) { + if (/undefined/.test((e as Error).message)) { + return undefined; + } + } + + if (match_path) { + result = route_info; + return true; + } else if (route_info.routes) { + result = findRouteByPath(path, route_info.routes); + return result; + } + return false; + }); + + return result; +}; + +export const isRouteVisible = (route?: TRouteConfig, is_logged_in?: boolean) => + !(route && 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); + +export const getContractPath = (contract_id = '') => getPath(routes.contract, { contract_id }); diff --git a/packages/trader/src/App/Components/Routes/index.js b/packages/trader/src/App/Components/Routes/index.js deleted file mode 100644 index 5d349b8e17ee..000000000000 --- a/packages/trader/src/App/Components/Routes/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import BinaryLink from './binary-link.jsx'; -import RouteWithSubRoutes from './route-with-sub-routes.jsx'; -import BinaryRoutes from './binary-routes.jsx'; - -export * from './helpers'; -export { BinaryLink, RouteWithSubRoutes }; -export default BinaryRoutes; diff --git a/packages/trader/src/App/Components/Routes/index.ts b/packages/trader/src/App/Components/Routes/index.ts new file mode 100644 index 000000000000..ecff56bd3670 --- /dev/null +++ b/packages/trader/src/App/Components/Routes/index.ts @@ -0,0 +1,7 @@ +import BinaryLink from './binary-link'; +import RouteWithSubRoutes from './route-with-sub-routes'; +import BinaryRoutes from './binary-routes'; + +export * from './helpers'; +export { BinaryLink, RouteWithSubRoutes }; +export default BinaryRoutes; diff --git a/packages/trader/src/App/Components/Routes/route-with-sub-routes.jsx b/packages/trader/src/App/Components/Routes/route-with-sub-routes.tsx similarity index 77% rename from packages/trader/src/App/Components/Routes/route-with-sub-routes.jsx rename to packages/trader/src/App/Components/Routes/route-with-sub-routes.tsx index 4c4c4f9213f8..33f9e8f41497 100644 --- a/packages/trader/src/App/Components/Routes/route-with-sub-routes.jsx +++ b/packages/trader/src/App/Components/Routes/route-with-sub-routes.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Redirect, Route } from 'react-router-dom'; +import { Redirect, Route, RouteComponentProps } from 'react-router-dom'; import { alternateLinkTagChange, canonicalLinkTagChange, @@ -11,9 +11,12 @@ import { } from '@deriv/shared'; import { getLanguage } from '@deriv/translations'; import Page404 from 'Modules/Page404'; +import { TBinaryRoutesProps, TRouteConfig } from 'Types'; -const RouteWithSubRoutes = route => { - const validateRoute = pathname => { +type TRouteWithSubRoutesProps = TRouteConfig & TBinaryRoutesProps; + +const RouteWithSubRoutes = (route: TRouteWithSubRoutesProps) => { + const validateRoute = (pathname: string) => { if (pathname === '') return true; if (route.path?.includes(':')) { const static_pathname = pathname.substring(0, pathname.lastIndexOf('/') + 1); @@ -22,7 +25,7 @@ const RouteWithSubRoutes = route => { return route.path === pathname || !!(route.routes && route.routes.find(r => pathname === r.path)); }; - const renderFactory = props => { + const renderFactory = (props: RouteComponentProps) => { let result = null; const pathname = removeBranchName(location.pathname).replace(/\/$/, ''); @@ -42,16 +45,15 @@ const RouteWithSubRoutes = route => { } else { const default_subroute = route.routes ? route.routes.find(r => r.default) : {}; const has_default_subroute = !isEmptyObject(default_subroute); - + const RouteComponent = route.component as React.ElementType; result = ( - {has_default_subroute && pathname === route.path && } - {is_valid_route ? : } + {has_default_subroute && pathname === route.path && } + {is_valid_route ? : } ); } - // eslint-disable-next-line no-nested-ternary const title = route.getTitle?.() || ''; document.title = `${title} | ${default_title}`; diff --git a/packages/trader/src/App/Constants/routes-config.js b/packages/trader/src/App/Constants/routes-config.ts similarity index 64% rename from packages/trader/src/App/Constants/routes-config.js rename to packages/trader/src/App/Constants/routes-config.ts index 5bbbf238210e..52c7930825b2 100644 --- a/packages/trader/src/App/Constants/routes-config.js +++ b/packages/trader/src/App/Constants/routes-config.ts @@ -1,14 +1,24 @@ import React from 'react'; +import { RouteComponentProps } from 'react-router'; import { routes, moduleLoader } from '@deriv/shared'; import { localize } from '@deriv/translations'; import Trade from 'Modules/Trading'; +import { TRouteConfig } from 'Types'; -const ContractDetails = React.lazy(() => - moduleLoader(() => import(/* webpackChunkName: "contract" */ 'Modules/Contract')) +const ContractDetails = React.lazy( + () => + moduleLoader(() => import(/* webpackChunkName: "contract" */ 'Modules/Contract')) as Promise<{ + default: React.ComponentType; + }> ); // Error Routes -const Page404 = React.lazy(() => moduleLoader(() => import(/* webpackChunkName: "404" */ 'Modules/Page404'))); +const Page404 = React.lazy( + () => + moduleLoader(() => import(/* webpackChunkName: "404" */ 'Modules/Page404')) as Promise<{ + default: React.ComponentType>; + }> +); // Order matters const initRoutesConfig = () => { @@ -24,7 +34,7 @@ const initRoutesConfig = () => { ]; }; -let routesConfig; +let routesConfig: TRouteConfig[] | undefined; // For default page route if page/path is not found, must be kept at the end of routes_config array const route_default = { component: Page404, getTitle: () => localize('Error 404') }; diff --git a/packages/trader/src/App/Containers/Routes/routes.jsx b/packages/trader/src/App/Containers/Routes/routes.tsx similarity index 78% rename from packages/trader/src/App/Containers/Routes/routes.jsx rename to packages/trader/src/App/Containers/Routes/routes.tsx index 5a37e794281a..562f4ed101af 100644 --- a/packages/trader/src/App/Containers/Routes/routes.jsx +++ b/packages/trader/src/App/Containers/Routes/routes.tsx @@ -1,6 +1,5 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import { withRouter, matchPath } from 'react-router'; +import { withRouter, matchPath, RouteComponentProps } from 'react-router'; import Loadable from 'react-loadable'; import { UILoader } from '@deriv/components'; import { routes } from '@deriv/shared'; @@ -9,11 +8,29 @@ import getRoutesConfig from 'App/Constants/routes-config'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; -const checkRoutingMatch = (route_list, path) => { +type TMatchPattern = { from: Array; to: Array }; + +type TRoutesProps = RouteComponentProps & { passthrough?: React.ComponentProps['passthrough'] }; + +type TTradePageMountingMiddlewareParams = { + action: string; + callback: (has_match: boolean) => void; + match_patterns: TMatchPattern[]; + path_from: string; + path_to: string; +}; + +const checkRoutingMatch = (route_list: Array, path = '') => { return route_list.some(route => !!matchPath(path, { path: route, exact: true })); }; -const tradePageMountingMiddleware = ({ path_from, path_to, action, match_patterns, callback }) => { +const tradePageMountingMiddleware = ({ + path_from, + path_to, + action, + match_patterns, + callback, +}: TTradePageMountingMiddlewareParams) => { if (action === 'PUSH' || action === 'POP') { // We use matchPath here because on route, there will be extra // parameters which matchPath takes into account. @@ -29,14 +46,14 @@ const tradePageMountingMiddleware = ({ path_from, path_to, action, match_pattern const Error = Loadable({ loader: () => import(/* webpackChunkName: "error-component" */ 'App/Components/Elements/Errors'), - loading: UILoader, + loading: () => , render(loaded, props) { const Component = loaded.default; return ; }, }); -const Routes = observer(({ history, passthrough }) => { +const Routes = observer(({ history, passthrough }: TRoutesProps) => { const { client, common, ui, portfolio } = useStore(); const { setSkipPrePostLifecycle: setTradeMountingPolicy } = useTraderStore(); const { error, has_error } = common; @@ -46,7 +63,7 @@ const Routes = observer(({ history, passthrough }) => { React.useEffect(() => { if (setPromptHandler) { - setPromptHandler(true, (route_to, action) => { + setPromptHandler(true, (route_to: RouteComponentProps['location'], action: string) => { // clears portfolio when we navigate to mt5 dashboard tradePageMountingMiddleware({ path_from: history.location.pathname, @@ -65,7 +82,7 @@ const Routes = observer(({ history, passthrough }) => { }, ], action, - callback: has_match => { + callback: (has_match: boolean) => { if (has_match) { onUnmountPortfolio(); } @@ -100,9 +117,4 @@ const Routes = observer(({ history, passthrough }) => { return ; }); -Routes.propTypes = { - history: PropTypes.object, - passthrough: PropTypes.object, -}; - export default withRouter(Routes); diff --git a/packages/trader/src/App/app.tsx b/packages/trader/src/App/app.tsx index e1c53b91a711..7cdd29ab5142 100644 --- a/packages/trader/src/App/app.tsx +++ b/packages/trader/src/App/app.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Loadable from 'react-loadable'; -import Routes from 'App/Containers/Routes/routes.jsx'; +import Routes from 'App/Containers/Routes/routes'; import TradeHeaderExtensions from 'App/Containers/trade-header-extensions'; import TradeFooterExtensions from 'App/Containers/trade-footer-extensions.jsx'; import TradeSettingsExtensions from 'App/Containers/trade-settings-extensions'; diff --git a/packages/trader/src/Types/common-prop.type.ts b/packages/trader/src/Types/common-prop.type.ts index 2618d7391a99..34b089269291 100644 --- a/packages/trader/src/Types/common-prop.type.ts +++ b/packages/trader/src/Types/common-prop.type.ts @@ -1,4 +1,15 @@ +import { TCoreStores } from '@deriv/stores/types'; import { useTraderStore } from 'Stores/useTraderStores'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +export type TBinaryRoutesProps = { + is_logged_in: boolean; + is_logging_in: boolean; + passthrough?: { + root_store: TCoreStores; + WS: unknown; + }; +}; export type TTextValueStrings = { text: string; @@ -22,4 +33,18 @@ export type TError = { }; }; +type TRoute = { + component?: React.ComponentType | React.ComponentType> | typeof Redirect; + default?: boolean; + exact?: boolean; + getTitle?: () => string; + path?: string; + to?: string; +}; + +export type TRouteConfig = TRoute & { + is_authenticated?: boolean; + routes?: TRoute[]; +}; + export type TTradeStore = ReturnType;