location.reload())}
+ />
+ );
+};
+
+ErrorComponent.propTypes = {
+ message: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
+ type: PropTypes.string,
+};
+
+export default ErrorComponent;
diff --git a/packages/reports/src/Components/Errors/index.js b/packages/reports/src/Components/Errors/index.js
new file mode 100644
index 000000000000..dc12ba08a1e7
--- /dev/null
+++ b/packages/reports/src/Components/Errors/index.js
@@ -0,0 +1 @@
+export default from './error-component.jsx';
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/calendar-icon.jsx b/packages/reports/src/Components/Form/CompositeCalendar/calendar-icon.jsx
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/calendar-icon.jsx
rename to packages/reports/src/Components/Form/CompositeCalendar/calendar-icon.jsx
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx b/packages/reports/src/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx
rename to packages/reports/src/Components/Form/CompositeCalendar/composite-calendar-mobile.jsx
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/composite-calendar.jsx b/packages/reports/src/Components/Form/CompositeCalendar/composite-calendar.jsx
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/composite-calendar.jsx
rename to packages/reports/src/Components/Form/CompositeCalendar/composite-calendar.jsx
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/index.js b/packages/reports/src/Components/Form/CompositeCalendar/index.js
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/index.js
rename to packages/reports/src/Components/Form/CompositeCalendar/index.js
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/list-item.jsx b/packages/reports/src/Components/Form/CompositeCalendar/list-item.jsx
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/list-item.jsx
rename to packages/reports/src/Components/Form/CompositeCalendar/list-item.jsx
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/side-list.jsx b/packages/reports/src/Components/Form/CompositeCalendar/side-list.jsx
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/side-list.jsx
rename to packages/reports/src/Components/Form/CompositeCalendar/side-list.jsx
diff --git a/packages/trader/src/App/Components/Form/CompositeCalendar/two-month-picker.jsx b/packages/reports/src/Components/Form/CompositeCalendar/two-month-picker.jsx
similarity index 100%
rename from packages/trader/src/App/Components/Form/CompositeCalendar/two-month-picker.jsx
rename to packages/reports/src/Components/Form/CompositeCalendar/two-month-picker.jsx
diff --git a/packages/reports/src/Components/Routes/__tests__/helpers.spec.js b/packages/reports/src/Components/Routes/__tests__/helpers.spec.js
new file mode 100644
index 000000000000..324d5d33967a
--- /dev/null
+++ b/packages/reports/src/Components/Routes/__tests__/helpers.spec.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { expect } from 'chai';
+import * as Helpers from '../helpers';
+import { routes } from '@deriv/shared';
+import getRoutesConfig from 'Constants/routes-config';
+
+describe('Helpers', () => {
+ describe('findRouteByPath', () => {
+ it('should return undefined when path is not in routes_config', () => {
+ expect(Helpers.findRouteByPath('invalidRoute', getRoutesConfig())).to.be.undefined;
+ });
+ it('should return route_info when path is in routes_config and is not nested', () => {
+ const result = Helpers.findRouteByPath(routes.reports, getRoutesConfig());
+ expect(result.path).to.equal(routes.reports);
+ });
+ it('should return route_info of parent route when path is in routes_config child level and is nested', () => {
+ const reports_routes_length = getRoutesConfig().find(r => r.path === routes.reports).routes.length;
+ expect(Helpers.findRouteByPath(routes.profit, getRoutesConfig())).to.have.all.keys(
+ 'path',
+ 'component',
+ 'is_authenticated',
+ 'routes',
+ 'icon_component',
+ 'getTitle'
+ );
+ expect(Helpers.findRouteByPath(routes.profit, getRoutesConfig()).routes).to.be.instanceof(Array);
+ expect(Helpers.findRouteByPath(routes.profit, getRoutesConfig()).routes).to.have.length(
+ reports_routes_length
+ );
+ expect(Helpers.findRouteByPath(routes.profit, getRoutesConfig()).is_authenticated).to.be.equal(true);
+ expect(Helpers.findRouteByPath(routes.profit, getRoutesConfig()).path).to.be.equal(routes.reports);
+ });
+ });
+
+ describe('isRouteVisible', () => {
+ it('should return true if route needs user to be authenticated and user is logged in', () => {
+ expect(Helpers.isRouteVisible({ path: '/contract', is_authenticated: true }, true)).to.equal(true);
+ });
+ it('should return false if route needs user to be authenticated and user is not logged in', () => {
+ expect(Helpers.isRouteVisible({ path: '/contract', is_authenticated: true }, false)).to.equal(false);
+ });
+ it('should return true if route does not need user to be authenticated and user is not logged in', () => {
+ expect(Helpers.isRouteVisible({ path: '/contract', is_authenticated: false }, false)).to.equal(true);
+ });
+ it('should return true if route does not need user to be authenticated and user is logged in', () => {
+ expect(Helpers.isRouteVisible({ path: '/contract', is_authenticated: false }, true)).to.equal(true);
+ });
+ });
+
+ describe('getPath', () => {
+ it('should return param values in params as a part of path', () => {
+ expect(Helpers.getPath('/contract/:contract_id', { contract_id: 37511105068 })).to.equal(
+ '/contract/37511105068'
+ );
+ expect(
+ Helpers.getPath('/something_made_up/:something_made_up_param1/:something_made_up_param2', {
+ something_made_up_param1: '789',
+ something_made_up_param2: '123456',
+ })
+ ).to.equal('/something_made_up/789/123456');
+ });
+ it('should return path as before if there is no params', () => {
+ expect(Helpers.getPath('/contract')).to.equal('/contract');
+ });
+ });
+});
diff --git a/packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js b/packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js
new file mode 100644
index 000000000000..9e9946f2265f
--- /dev/null
+++ b/packages/reports/src/Components/Routes/__tests__/route-with-sub-routes.spec.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { expect } from 'chai';
+import { configure, shallow } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { RouteWithSubRoutesRender } from '../route-with-sub-routes.jsx';
+import { Redirect } from 'react-router-dom';
+import { PlatformContext } from '@deriv/shared';
+
+configure({ adapter: new Adapter() });
+
+describe('', () => {
+ it('should render one component', () => {
+ const comp = (
+
+
+
+ );
+ const wrapper = shallow(comp);
+ expect(wrapper).to.have.length(1);
+ });
+ it('should have props as passed as route', () => {
+ const route = { path: '/', component: Redirect, title: '', exact: true, to: '/root' };
+ const comp = (
+
+
+
+ );
+ const wrapper = shallow(comp);
+ expect(wrapper.prop('exact')).to.equal(true);
+ expect(wrapper.prop('path')).to.equal('/');
+ });
+});
diff --git a/packages/reports/src/Components/Routes/binary-link.jsx b/packages/reports/src/Components/Routes/binary-link.jsx
new file mode 100644
index 000000000000..55f99c6324b8
--- /dev/null
+++ b/packages/reports/src/Components/Routes/binary-link.jsx
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import { PlatformContext } from '@deriv/shared';
+import getRoutesConfig from 'Constants/routes-config';
+import { findRouteByPath, normalizePath } from './helpers';
+
+const BinaryLink = ({ active_class, to, children, ...props }) => {
+ const { is_appstore } = React.useContext(PlatformContext);
+
+ const path = normalizePath(to);
+ const route = findRouteByPath(path, getRoutesConfig({ is_appstore }));
+
+ if (!route) {
+ throw new Error(`Route not found: ${to}`);
+ }
+
+ return to ? (
+
+ {children}
+
+ ) : (
+ {children}
+ );
+};
+
+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/reports/src/Components/Routes/binary-routes.jsx b/packages/reports/src/Components/Routes/binary-routes.jsx
new file mode 100644
index 000000000000..5a1f542af729
--- /dev/null
+++ b/packages/reports/src/Components/Routes/binary-routes.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Switch } from 'react-router-dom';
+import { PlatformContext } from '@deriv/shared';
+import { Localize } from '@deriv/translations';
+import getRoutesConfig from '../../Constants/routes-config';
+import RouteWithSubRoutes from './route-with-sub-routes.jsx';
+
+const BinaryRoutes = props => {
+ const { is_appstore } = React.useContext(PlatformContext);
+
+ return (
+ {
+ return (
+
+
+
+ );
+ }}
+ >
+
+ {getRoutesConfig({ is_appstore }).map(route => (
+
+ ))}
+
+
+ );
+};
+
+export default BinaryRoutes;
diff --git a/packages/reports/src/Components/Routes/helpers.js b/packages/reports/src/Components/Routes/helpers.js
new file mode 100644
index 000000000000..6a16bfb56965
--- /dev/null
+++ b/packages/reports/src/Components/Routes/helpers.js
@@ -0,0 +1,34 @@
+import { matchPath } from 'react-router';
+
+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);
diff --git a/packages/reports/src/Components/Routes/index.js b/packages/reports/src/Components/Routes/index.js
new file mode 100644
index 000000000000..061bdf961719
--- /dev/null
+++ b/packages/reports/src/Components/Routes/index.js
@@ -0,0 +1,4 @@
+export BinaryLink from './binary-link.jsx';
+export default from './binary-routes.jsx';
+export * from './helpers';
+export RouteWithSubRoutes from './route-with-sub-routes.jsx';
diff --git a/packages/reports/src/Components/Routes/route-with-sub-routes.jsx b/packages/reports/src/Components/Routes/route-with-sub-routes.jsx
new file mode 100644
index 000000000000..ce6f23915d0a
--- /dev/null
+++ b/packages/reports/src/Components/Routes/route-with-sub-routes.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Redirect, Route } from 'react-router-dom';
+import { redirectToLogin, isEmptyObject, routes, removeBranchName, default_title } from '@deriv/shared';
+import { getLanguage } from '@deriv/translations';
+
+const RouteWithSubRoutes = route => {
+ const renderFactory = props => {
+ let result = null;
+ if (route.component === Redirect) {
+ let to = route.to;
+
+ // This if clause has been added just to remove '/index' from url in localhost env.
+ if (route.path === routes.index) {
+ const { location } = props;
+ to = location.pathname.toLowerCase().replace(route.path, '');
+ }
+ result = ;
+ } else if (route.is_authenticated && !route.is_logging_in && !route.is_logged_in) {
+ redirectToLogin(route.is_logged_in, getLanguage());
+ } else {
+ const default_subroute = route.routes ? route.routes.find(r => r.default) : {};
+ const has_default_subroute = !isEmptyObject(default_subroute);
+ const pathname = removeBranchName(location.pathname);
+ result = (
+
+ {has_default_subroute && pathname === route.path && }
+
+
+ );
+ }
+
+ const title = route.getTitle?.() || '';
+ document.title = `${title} | ${default_title}`;
+ return result;
+ };
+
+ return ;
+};
+
+export { RouteWithSubRoutes as RouteWithSubRoutesRender }; // For tests
+
+export default RouteWithSubRoutes;
diff --git a/packages/trader/src/Modules/Reports/Components/account-statistics.jsx b/packages/reports/src/Components/account-statistics.jsx
similarity index 100%
rename from packages/trader/src/Modules/Reports/Components/account-statistics.jsx
rename to packages/reports/src/Components/account-statistics.jsx
diff --git a/packages/trader/src/Modules/Reports/Components/amount-cell.jsx b/packages/reports/src/Components/amount-cell.jsx
similarity index 80%
rename from packages/trader/src/Modules/Reports/Components/amount-cell.jsx
rename to packages/reports/src/Components/amount-cell.jsx
index 74342892adb0..b2d3d7a74068 100644
--- a/packages/trader/src/Modules/Reports/Components/amount-cell.jsx
+++ b/packages/reports/src/Components/amount-cell.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { getProfitOrLoss } from 'Modules/Reports/Helpers/profit-loss';
+import { getProfitOrLoss } from '../Helpers/profit-loss';
const AmountCell = ({ value }) => {
const status = getProfitOrLoss(value);
diff --git a/packages/trader/src/Modules/Reports/Components/currency-wrapper.jsx b/packages/reports/src/Components/currency-wrapper.jsx
similarity index 100%
rename from packages/trader/src/Modules/Reports/Components/currency-wrapper.jsx
rename to packages/reports/src/Components/currency-wrapper.jsx
diff --git a/packages/trader/src/Modules/Reports/Components/empty-portfolio-message.jsx b/packages/reports/src/Components/empty-portfolio-message.jsx
similarity index 100%
rename from packages/trader/src/Modules/Reports/Components/empty-portfolio-message.jsx
rename to packages/reports/src/Components/empty-portfolio-message.jsx
diff --git a/packages/trader/src/Modules/Reports/Components/empty-trade-history-message.jsx b/packages/reports/src/Components/empty-trade-history-message.jsx
similarity index 100%
rename from packages/trader/src/Modules/Reports/Components/empty-trade-history-message.jsx
rename to packages/reports/src/Components/empty-trade-history-message.jsx
diff --git a/packages/trader/src/Modules/Reports/Components/filter-component.jsx b/packages/reports/src/Components/filter-component.jsx
similarity index 95%
rename from packages/trader/src/Modules/Reports/Components/filter-component.jsx
rename to packages/reports/src/Components/filter-component.jsx
index 9f9588b6a130..316995b171ff 100644
--- a/packages/trader/src/Modules/Reports/Components/filter-component.jsx
+++ b/packages/reports/src/Components/filter-component.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { FilterDropdown } from '@deriv/components';
import { localize } from '@deriv/translations';
import { connect } from 'Stores/connect';
-import CompositeCalendar from 'App/Components/Form/CompositeCalendar/composite-calendar.jsx';
+import CompositeCalendar from './Form/CompositeCalendar';
const FilterComponent = ({
action_type,
diff --git a/packages/reports/src/Components/index.js b/packages/reports/src/Components/index.js
new file mode 100644
index 000000000000..d498bb957778
--- /dev/null
+++ b/packages/reports/src/Components/index.js
@@ -0,0 +1,3 @@
+import EmptyPortfolioMessage from './empty-portfolio-message.jsx';
+
+export default EmptyPortfolioMessage;
diff --git a/packages/trader/src/Modules/Reports/Components/indicative-cell.jsx b/packages/reports/src/Components/indicative-cell.jsx
similarity index 92%
rename from packages/trader/src/Modules/Reports/Components/indicative-cell.jsx
rename to packages/reports/src/Components/indicative-cell.jsx
index af1b3521a4f6..0bf9029340e1 100644
--- a/packages/trader/src/Modules/Reports/Components/indicative-cell.jsx
+++ b/packages/reports/src/Components/indicative-cell.jsx
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Icon, Money, DesktopWrapper, ContractCard } from '@deriv/components';
-import { getCardLabels } from 'Constants/contract';
+import { getCardLabels } from '_common/contract';
import { connect } from 'Stores/connect';
const IndicativeCell = ({ amount, currency, contract_info, is_footer, onClickSell, is_sell_requested }) => {
@@ -48,6 +48,6 @@ IndicativeCell.propTypes = {
onClickSell: PropTypes.func,
};
-export default connect(({ modules }) => ({
- onClickSell: modules.portfolio.onClickSell,
+export default connect(({ portfolio }) => ({
+ onClickSell: portfolio.onClickSell,
}))(IndicativeCell);
diff --git a/packages/trader/src/Modules/Reports/Components/market-symbol-icon-row.jsx b/packages/reports/src/Components/market-symbol-icon-row.jsx
similarity index 100%
rename from packages/trader/src/Modules/Reports/Components/market-symbol-icon-row.jsx
rename to packages/reports/src/Components/market-symbol-icon-row.jsx
diff --git a/packages/trader/src/Modules/Reports/Components/placeholder-component.jsx b/packages/reports/src/Components/placeholder-component.jsx
similarity index 92%
rename from packages/trader/src/Modules/Reports/Components/placeholder-component.jsx
rename to packages/reports/src/Components/placeholder-component.jsx
index 54d43f05c528..b8d9717abc41 100644
--- a/packages/trader/src/Modules/Reports/Components/placeholder-component.jsx
+++ b/packages/reports/src/Components/placeholder-component.jsx
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
-import Loading from '../../../templates/_common/components/loading';
+import Loading from '_common/components/loading.jsx';
const PlaceholderComponent = props => {
const EmptyMessageComponent = props.empty_message_component;
diff --git a/packages/trader/src/Modules/Reports/Components/profit_loss_cell.jsx b/packages/reports/src/Components/profit_loss_cell.jsx
similarity index 81%
rename from packages/trader/src/Modules/Reports/Components/profit_loss_cell.jsx
rename to packages/reports/src/Components/profit_loss_cell.jsx
index b968cb81d03f..bc454d179b2b 100644
--- a/packages/trader/src/Modules/Reports/Components/profit_loss_cell.jsx
+++ b/packages/reports/src/Components/profit_loss_cell.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { getProfitOrLoss } from 'Modules/Reports/Helpers/profit-loss';
+import { getProfitOrLoss } from '../Helpers/profit-loss';
const ProfitLossCell = ({ value, children }) => {
const status = getProfitOrLoss(value);
diff --git a/packages/trader/src/Modules/Reports/Components/reports-meta.jsx b/packages/reports/src/Components/reports-meta.jsx
similarity index 100%
rename from packages/trader/src/Modules/Reports/Components/reports-meta.jsx
rename to packages/reports/src/Components/reports-meta.jsx
diff --git a/packages/trader/src/Modules/Reports/Constants/data-table-constants.js b/packages/reports/src/Constants/data-table-constants.js
similarity index 98%
rename from packages/trader/src/Modules/Reports/Constants/data-table-constants.js
rename to packages/reports/src/Constants/data-table-constants.js
index 75d0bb5dc178..a6cf33b3daff 100644
--- a/packages/trader/src/Modules/Reports/Constants/data-table-constants.js
+++ b/packages/reports/src/Constants/data-table-constants.js
@@ -3,9 +3,10 @@ import React from 'react';
import { Icon, Label, Money, ContractCard } from '@deriv/components';
import { isMobile, getCurrencyDisplayCode, getTotalProfit, shouldShowCancellation } from '@deriv/shared';
import { localize, Localize } from '@deriv/translations';
-import ProgressSliderStream from 'App/Containers/ProgressSliderStream';
-import { getCardLabels } from 'Constants/contract';
-import { getProfitOrLoss } from 'Modules/Reports/Helpers/profit-loss';
+import ProgressSliderStream from '../Containers/progress-slider-stream.jsx';
+
+import { getCardLabels } from '_common/contract';
+import { getProfitOrLoss } from '../Helpers/profit-loss';
import IndicativeCell from '../Components/indicative-cell.jsx';
import MarketSymbolIconRow from '../Components/market-symbol-icon-row.jsx';
import ProfitLossCell from '../Components/profit_loss_cell.jsx';
diff --git a/packages/reports/src/Constants/routes-config.js b/packages/reports/src/Constants/routes-config.js
new file mode 100644
index 000000000000..d767b7b05c2d
--- /dev/null
+++ b/packages/reports/src/Constants/routes-config.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import { routes, makeLazyLoader, moduleLoader } from '@deriv/shared';
+import { Loading } from '@deriv/components';
+import { localize } from '@deriv/translations';
+
+const Page404 = React.lazy(() => import(/* webpackChunkName: "404" */ 'Modules/Page404'));
+
+const lazyLoadReportComponent = makeLazyLoader(
+ () => moduleLoader(() => import(/* webpackChunkName: "reports-routes" */ 'Containers')),
+ () =>
+);
+
+// Order matters
+const initRoutesConfig = () => {
+ return [
+ {
+ path: routes.reports,
+ component: lazyLoadReportComponent('Reports'),
+ is_authenticated: true,
+ getTitle: () => localize('Reports'),
+ icon_component: 'IcReports',
+ routes: [
+ {
+ path: routes.positions,
+ component: lazyLoadReportComponent('OpenPositions'),
+ getTitle: () => localize('Open positions'),
+ icon_component: 'IcOpenPositions',
+ default: true,
+ },
+ {
+ path: routes.profit,
+ component: lazyLoadReportComponent('ProfitTable'),
+ getTitle: () => localize('Profit table'),
+ icon_component: 'IcProfitTable',
+ },
+ {
+ path: routes.statement,
+ component: lazyLoadReportComponent('Statement'),
+ getTitle: () => localize('Statement'),
+ icon_component: 'IcStatement',
+ },
+ ],
+ },
+ ];
+};
+
+let routesConfig;
+
+// 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') };
+
+const getRoutesConfig = () => {
+ if (!routesConfig) {
+ routesConfig = initRoutesConfig();
+ routesConfig.push(route_default);
+ }
+ return routesConfig;
+};
+
+export default getRoutesConfig;
diff --git a/packages/trader/src/Modules/Reports/Containers/__tests__/open-positions.spec.js b/packages/reports/src/Containers/__tests__/open-positions.spec.js
similarity index 91%
rename from packages/trader/src/Modules/Reports/Containers/__tests__/open-positions.spec.js
rename to packages/reports/src/Containers/__tests__/open-positions.spec.js
index cae4c669d486..a1c2eed51a47 100644
--- a/packages/trader/src/Modules/Reports/Containers/__tests__/open-positions.spec.js
+++ b/packages/reports/src/Containers/__tests__/open-positions.spec.js
@@ -19,7 +19,7 @@ describe('OpenPositions', () => {
);
it('should render one component', async () => {
- const OpenPositions = (await import('../../index')).default.OpenPositions;
+ const OpenPositions = (await import('../index')).default.OpenPositions;
const wrapper = shallow();
expect(wrapper).to.have.length(1);
});
diff --git a/packages/reports/src/Containers/index.js b/packages/reports/src/Containers/index.js
new file mode 100644
index 000000000000..0523dc131e09
--- /dev/null
+++ b/packages/reports/src/Containers/index.js
@@ -0,0 +1,11 @@
+import OpenPositions from './open-positions.jsx';
+import ProfitTable from './profit-table.jsx';
+import Statement from './statement.jsx';
+import Reports from './reports.jsx';
+
+export default {
+ OpenPositions,
+ ProfitTable,
+ Statement,
+ Reports,
+};
diff --git a/packages/trader/src/Modules/Reports/Containers/open-positions.jsx b/packages/reports/src/Containers/open-positions.jsx
similarity index 86%
rename from packages/trader/src/Modules/Reports/Containers/open-positions.jsx
rename to packages/reports/src/Containers/open-positions.jsx
index 7368f5318571..1d109e55e0a7 100644
--- a/packages/trader/src/Modules/Reports/Containers/open-positions.jsx
+++ b/packages/reports/src/Containers/open-positions.jsx
@@ -12,19 +12,27 @@ import {
ContractCard,
usePrevious,
} from '@deriv/components';
-import { urlFor, isMobile, isMultiplierContract, getTimePercentage, website_name, getTotalProfit } from '@deriv/shared';
+import {
+ urlFor,
+ isMobile,
+ isMultiplierContract,
+ getTimePercentage,
+ website_name,
+ getTotalProfit,
+ getContractPath,
+} from '@deriv/shared';
import { localize, Localize } from '@deriv/translations';
-import { ReportsTableRowLoader } from 'App/Components/Elements/ContentLoader';
-import { getContractPath } from 'App/Components/Routes/helpers';
-import { getContractDurationType } from 'Modules/Reports/Helpers/market-underlying';
-import EmptyTradeHistoryMessage from 'Modules/Reports/Components/empty-trade-history-message.jsx';
+import { ReportsTableRowLoader } from '../Components/Elements/ContentLoader';
+import { getContractDurationType } from '../Helpers/market-underlying';
+
+import EmptyTradeHistoryMessage from '../Components/empty-trade-history-message.jsx';
import {
getOpenPositionsColumnsTemplate,
getMultiplierOpenPositionsColumnsTemplate,
-} from 'Modules/Reports/Constants/data-table-constants';
-import PositionsCard from 'App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx';
-import PlaceholderComponent from 'Modules/Reports/Components/placeholder-component.jsx';
-import { getCardLabels } from 'Constants/contract';
+} from 'Constants/data-table-constants';
+import PositionsDrawerCard from '../Components/Elements/PositionsDrawerCard';
+import PlaceholderComponent from '../Components/placeholder-component.jsx';
+import { getCardLabels } from '_common/contract';
import { connect } from 'Stores/connect';
const EmptyPlaceholderWrapper = props => (
@@ -42,7 +50,16 @@ const EmptyPlaceholderWrapper = props => (
);
-const MobileRowRenderer = ({ row, is_footer, columns_map, server_time, onClickCancel, onClickSell, measure }) => {
+const MobileRowRenderer = ({
+ row,
+ is_footer,
+ columns_map,
+ server_time,
+ onClickCancel,
+ onClickSell,
+ measure,
+ ...props
+}) => {
React.useEffect(() => {
if (!is_footer) {
measure();
@@ -77,7 +94,7 @@ const MobileRowRenderer = ({ row, is_footer, columns_map, server_time, onClickCa
if (isMultiplierContract(type)) {
return (
-
);
}
@@ -289,6 +307,7 @@ const OpenPositions = ({
onClickSell,
onMount,
server_time,
+ ...props
}) => {
const [active_index, setActiveIndex] = React.useState(is_multiplier ? 1 : 0);
// Tabs should be visible only when there is at least one active multiplier contract
@@ -300,8 +319,8 @@ const OpenPositions = ({
* For mobile, we show portfolio stepper in header even for reports pages.
* `onMount` in portfolio store will be invoked from portfolio stepper component in `trade-header-extensions.jsx`
*/
- if (!isMobile()) onMount();
+ onMount();
checkForMultiplierContract();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -349,13 +368,14 @@ const OpenPositions = ({
return map;
}, {});
- const mobileRowRenderer = props => (
+ const mobileRowRenderer = args => (
);
@@ -424,18 +444,38 @@ OpenPositions.propTypes = {
onClickSell: PropTypes.func,
onMount: PropTypes.func,
server_time: PropTypes.object,
+ addToast: PropTypes.func,
+ current_focus: PropTypes.string,
+ onClickRemove: PropTypes.func,
+ getContractById: PropTypes.func,
+ is_mobile: PropTypes.bool,
+ removeToast: PropTypes.func,
+ setCurrentFocus: PropTypes.func,
+ should_show_cancellation_warning: PropTypes.bool,
+ toggleCancellationWarning: PropTypes.func,
+ toggleUnsupportedContractModal: PropTypes.func,
};
-export default connect(({ modules, client, common, ui }) => ({
- active_positions: modules.portfolio.active_positions,
+export default connect(({ client, common, ui, portfolio, contract_trade }) => ({
+ active_positions: portfolio.active_positions,
currency: client.currency,
- error: modules.portfolio.error,
- getPositionById: modules.portfolio.getPositionById,
- is_loading: modules.portfolio.is_loading,
- is_multiplier: modules.trade.is_multiplier,
+ error: portfolio.error,
+ getPositionById: portfolio.getPositionById,
+ is_loading: portfolio.is_loading,
+ is_multiplier: portfolio.is_multiplier,
NotificationMessages: ui.notification_messages_ui,
- onClickCancel: modules.portfolio.onClickCancel,
- onClickSell: modules.portfolio.onClickSell,
- onMount: modules.portfolio.onMount,
+ onClickCancel: portfolio.onClickCancel,
+ onClickSell: portfolio.onClickSell,
+ onMount: portfolio.onMount,
server_time: common.server_time,
+ addToast: ui.addToast,
+ current_focus: ui.current_focus,
+ onClickRemove: portfolio.removePositionById,
+ getContractById: contract_trade.getContractById,
+ is_mobile: ui.is_mobile,
+ removeToast: ui.removeToast,
+ setCurrentFocus: ui.setCurrentFocus,
+ should_show_cancellation_warning: ui.should_show_cancellation_warning,
+ toggleCancellationWarning: ui.toggleCancellationWarning,
+ toggleUnsupportedContractModal: ui.toggleUnsupportedContractModal,
}))(withRouter(OpenPositions));
diff --git a/packages/trader/src/Modules/Reports/Containers/profit-table.jsx b/packages/reports/src/Containers/profit-table.jsx
similarity index 95%
rename from packages/trader/src/Modules/Reports/Containers/profit-table.jsx
rename to packages/reports/src/Containers/profit-table.jsx
index 9469288002ca..e5368fbf6054 100644
--- a/packages/trader/src/Modules/Reports/Containers/profit-table.jsx
+++ b/packages/reports/src/Containers/profit-table.jsx
@@ -4,17 +4,17 @@ import { PropTypes as MobxPropTypes } from 'mobx-react';
import React from 'react';
import { withRouter } from 'react-router';
import { DesktopWrapper, MobileWrapper, DataList, DataTable } from '@deriv/components';
-import { extractInfoFromShortcode, isForwardStarting, urlFor, website_name } from '@deriv/shared';
+import { extractInfoFromShortcode, isForwardStarting, urlFor, website_name, getContractPath } from '@deriv/shared';
import { localize, Localize } from '@deriv/translations';
-import { ReportsTableRowLoader } from 'App/Components/Elements/ContentLoader';
-import CompositeCalendar from 'App/Components/Form/CompositeCalendar';
-import { getContractPath } from 'App/Components/Routes/helpers';
-import { getSupportedContracts } from 'Constants';
+import { ReportsTableRowLoader } from '../Components/Elements/ContentLoader';
+import CompositeCalendar from '../Components/Form/CompositeCalendar';
+import { getSupportedContracts } from '_common/contract';
+
import { connect } from 'Stores/connect';
import EmptyTradeHistoryMessage from '../Components/empty-trade-history-message.jsx';
import PlaceholderComponent from '../Components/placeholder-component.jsx';
import { ReportsMeta } from '../Components/reports-meta.jsx';
-import { getProfitTableColumnsTemplate } from '../Constants/data-table-constants';
+import { getProfitTableColumnsTemplate } from 'Constants/data-table-constants';
const getRowAction = row_obj =>
getSupportedContracts()[extractInfoFromShortcode(row_obj.shortcode).category.toUpperCase()] &&
diff --git a/packages/reports/src/Containers/progress-slider-stream.jsx b/packages/reports/src/Containers/progress-slider-stream.jsx
new file mode 100644
index 000000000000..024822c3d65c
--- /dev/null
+++ b/packages/reports/src/Containers/progress-slider-stream.jsx
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { ProgressSlider } from '@deriv/components';
+import { getCurrentTick } from '@deriv/shared';
+import { connect } from 'Stores/connect';
+import { getCardLabels } from '_common/contract';
+
+const ProgressSliderStream = ({ contract_info, is_loading, server_time }) => {
+ if (!contract_info) {
+ return ;
+ }
+ const current_tick = contract_info.tick_count && getCurrentTick(contract_info);
+
+ return (
+
+ );
+};
+
+ProgressSliderStream.propTypes = {
+ contract_info: PropTypes.object,
+ is_loading: PropTypes.bool,
+ server_time: PropTypes.object,
+};
+
+export default connect(({ common, portfolio }) => ({
+ is_loading: portfolio.is_loading,
+ server_time: common.server_time,
+}))(ProgressSliderStream);
diff --git a/packages/trader/src/Modules/Reports/Containers/reports.jsx b/packages/reports/src/Containers/reports.jsx
similarity index 99%
rename from packages/trader/src/Modules/Reports/Containers/reports.jsx
rename to packages/reports/src/Containers/reports.jsx
index 33b0e8a509a2..6d55e1a02d8a 100644
--- a/packages/trader/src/Modules/Reports/Containers/reports.jsx
+++ b/packages/reports/src/Containers/reports.jsx
@@ -32,7 +32,6 @@ const Reports = ({
}) => {
React.useEffect(() => {
toggleReports(true);
-
return () => {
setVisibilityRealityCheck(1);
toggleReports(false);
diff --git a/packages/reports/src/Containers/routes.jsx b/packages/reports/src/Containers/routes.jsx
new file mode 100644
index 000000000000..585ad7a9d3f8
--- /dev/null
+++ b/packages/reports/src/Containers/routes.jsx
@@ -0,0 +1,39 @@
+import { PropTypes as MobxPropTypes } from 'mobx-react';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { withRouter } from 'react-router';
+import BinaryRoutes from 'Components/Routes';
+import { connect } from 'Stores/connect';
+import ErrorComponent from 'Components/Errors';
+
+const Routes = props => {
+ if (props.has_error) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+Routes.propTypes = {
+ error: MobxPropTypes.objectOrObservableObject,
+ has_error: PropTypes.bool,
+ is_logged_in: PropTypes.bool,
+ is_virtual: PropTypes.bool,
+};
+
+// need to wrap withRouter around connect
+// to prevent updates on from being blocked
+export default withRouter(
+ connect(({ client, common }) => ({
+ is_logged_in: client.is_logged_in,
+ is_logging_in: client.is_logging_in,
+ error: common.error,
+ has_error: common.has_error,
+ }))(Routes)
+);
diff --git a/packages/trader/src/Modules/Reports/Containers/statement.jsx b/packages/reports/src/Containers/statement.jsx
similarity index 98%
rename from packages/trader/src/Modules/Reports/Containers/statement.jsx
rename to packages/reports/src/Containers/statement.jsx
index 1d5b36935b9d..71ab00441f6e 100644
--- a/packages/trader/src/Modules/Reports/Containers/statement.jsx
+++ b/packages/reports/src/Containers/statement.jsx
@@ -3,11 +3,10 @@ import PropTypes from 'prop-types';
import React from 'react';
import { withRouter } from 'react-router-dom';
import { DesktopWrapper, MobileWrapper, DataList, DataTable, Text, Clipboard } from '@deriv/components';
-import { extractInfoFromShortcode, isForwardStarting, urlFor, website_name } from '@deriv/shared';
+import { extractInfoFromShortcode, isForwardStarting, urlFor, website_name, getContractPath } from '@deriv/shared';
import { localize, Localize } from '@deriv/translations';
-import { ReportsTableRowLoader } from 'App/Components/Elements/ContentLoader';
-import { getContractPath } from 'App/Components/Routes/helpers';
-import { getSupportedContracts } from 'Constants';
+import { ReportsTableRowLoader } from '../Components/Elements/ContentLoader';
+import { getSupportedContracts } from '_common/contract';
import { connect } from 'Stores/connect';
import { getStatementTableColumnsTemplate } from '../Constants/data-table-constants';
import PlaceholderComponent from '../Components/placeholder-component.jsx';
diff --git a/packages/reports/src/Helpers/digits.js b/packages/reports/src/Helpers/digits.js
new file mode 100644
index 000000000000..6ad31ce7e17c
--- /dev/null
+++ b/packages/reports/src/Helpers/digits.js
@@ -0,0 +1,5 @@
+const digitCategoriesMap = ['even_odd', 'match_diff', 'over_under'];
+const digitTypesMap = ['DIGITDIFF', 'DIGITMATCH', 'DIGITOVER', 'DIGITUNDER', 'DIGITEVEN', 'DIGITODD'];
+
+export const isDigitTradeType = trade_type => digitCategoriesMap.includes(trade_type);
+export const isDigitContractType = contract_type => digitTypesMap.includes(contract_type);
diff --git a/packages/trader/src/Modules/Reports/Helpers/market-underlying.js b/packages/reports/src/Helpers/market-underlying.js
similarity index 95%
rename from packages/trader/src/Modules/Reports/Helpers/market-underlying.js
rename to packages/reports/src/Helpers/market-underlying.js
index 82363678b752..e40f15c4dc0f 100644
--- a/packages/trader/src/Modules/Reports/Helpers/market-underlying.js
+++ b/packages/reports/src/Helpers/market-underlying.js
@@ -1,4 +1,4 @@
-import { getMarketNamesMap, getContractConfig } from 'Constants';
+import { getMarketNamesMap, getContractConfig } from '_common/contract';
import { localize } from '@deriv/translations';
/**
diff --git a/packages/trader/src/Modules/Reports/Helpers/profit-loss.js b/packages/reports/src/Helpers/profit-loss.js
similarity index 100%
rename from packages/trader/src/Modules/Reports/Helpers/profit-loss.js
rename to packages/reports/src/Helpers/profit-loss.js
diff --git a/packages/reports/src/Modules/Page404/Components/Page404.jsx b/packages/reports/src/Modules/Page404/Components/Page404.jsx
new file mode 100644
index 000000000000..981431ce85c8
--- /dev/null
+++ b/packages/reports/src/Modules/Page404/Components/Page404.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { PageError } from '@deriv/components';
+import { routes, getUrlBase } from '@deriv/shared';
+
+import { localize } from '@deriv/translations';
+
+const Page404 = () => (
+
+);
+
+export default Page404;
diff --git a/packages/reports/src/Modules/Page404/index.js b/packages/reports/src/Modules/Page404/index.js
new file mode 100644
index 000000000000..ccb77a533cf4
--- /dev/null
+++ b/packages/reports/src/Modules/Page404/index.js
@@ -0,0 +1 @@
+export default from './Components/Page404.jsx';
diff --git a/packages/reports/src/Modules/SmartChart/Components/Markers/__tests__/marker-spot-label.spec.js b/packages/reports/src/Modules/SmartChart/Components/Markers/__tests__/marker-spot-label.spec.js
new file mode 100644
index 000000000000..26403c0342da
--- /dev/null
+++ b/packages/reports/src/Modules/SmartChart/Components/Markers/__tests__/marker-spot-label.spec.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { expect } from 'chai';
+import { configure, shallow, mount } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import MarkerSpotLabel from '../marker-spot-label.jsx';
+
+configure({ adapter: new Adapter() });
+
+describe('MarkerSpotLabel', () => {
+ it('should render one component', () => {
+ const wrapper = shallow();
+ expect(wrapper).to.have.length(1);
+ });
+ it('should have class .chart-spot-label__time-value-container--top if align_label top is passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot-label__time-value-container--top').exists()).to.be.true;
+ });
+ it('should have class .chart-spot-label__time-value-container--bottom if align_label bottom is passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot-label__time-value-container--bottom').exists()).to.be.true;
+ });
+ it('should have class .chart-spot-label__time-value-container--top if no align_label is passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot-label__time-value-container--top').exists()).to.be.true;
+ expect(wrapper.find('.chart-spot-label__time-value-container--bottom').exists()).to.be.false;
+ });
+ it('should toggle label on hover if has_hover_toggle is passed in props', async () => {
+ const wrapper = mount();
+ expect(wrapper.find('.chart-spot-label__info-container').exists()).to.be.false;
+
+ wrapper.find('.marker-hover-container').simulate('mouseenter');
+ wrapper.update();
+ expect(wrapper.find('.chart-spot-label__info-container').exists()).to.be.true;
+
+ wrapper.find('.marker-hover-container').simulate('mouseleave');
+ wrapper.update();
+ expect(wrapper.find('.chart-spot-label__info-container').exists()).to.be.false;
+ });
+ it('should not toggle label on hover if has_label_toggle is not passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot-label__info-container').exists()).to.be.true;
+ expect(wrapper.find('.marker-hover-container').exists()).to.equal(false);
+ });
+ it('should have class .chart-spot-label__value-container--won if status won is passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot-label__value-container--won').exists()).to.be.true;
+ });
+ it('should have class .chart-spot-label__value-container--lost if status lost is passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot-label__value-container--lost').exists()).to.be.true;
+ });
+});
diff --git a/packages/reports/src/Modules/SmartChart/Components/Markers/__tests__/marker-spot.spec.js b/packages/reports/src/Modules/SmartChart/Components/Markers/__tests__/marker-spot.spec.js
new file mode 100644
index 000000000000..4327cb046a54
--- /dev/null
+++ b/packages/reports/src/Modules/SmartChart/Components/Markers/__tests__/marker-spot.spec.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { expect } from 'chai';
+import { configure, shallow } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import MarkerSpot from '../marker-spot.jsx';
+
+configure({ adapter: new Adapter() });
+
+describe('MarkerSpot', () => {
+ it('should render one component', () => {
+ const wrapper = shallow();
+ expect(wrapper).to.have.length(1);
+ });
+ it('should not have class .chart-spot__spot--lost or .chart-spot__spot--won if no status is passed in props', () => {
+ const wrapper = shallow();
+ expect(wrapper.find('.chart-spot__spot--lost').exists()).to.be.false;
+ expect(wrapper.find('.chart-spot__spot--lost').exists()).to.be.false;
+ expect(wrapper.find('.chart-spot').exists()).to.be.true;
+ });
+});
diff --git a/packages/reports/src/Modules/SmartChart/Components/Markers/marker-line.jsx b/packages/reports/src/Modules/SmartChart/Components/Markers/marker-line.jsx
new file mode 100644
index 000000000000..c4a5c7083a7e
--- /dev/null
+++ b/packages/reports/src/Modules/SmartChart/Components/Markers/marker-line.jsx
@@ -0,0 +1,38 @@
+import classNames from 'classnames';
+import { observer } from 'mobx-react';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Icon } from '@deriv/components';
+
+const MarkerLine = ({ label, line_style, marker_config, status }) => {
+ // TODO: Find a more elegant solution
+ if (!marker_config) return ;
+ return (
+
+ {label === marker_config.LINE_END.content_config.label && (
+
+ )}
+ {label === marker_config.LINE_START.content_config.label && (
+
+ )}
+
+ );
+};
+
+MarkerLine.propTypes = {
+ label: PropTypes.string,
+ line_style: PropTypes.string,
+ marker_config: PropTypes.object,
+ status: PropTypes.oneOf(['won', 'lost']),
+};
+export default observer(MarkerLine);
diff --git a/packages/reports/src/Modules/SmartChart/Components/Markers/marker-spot-label.jsx b/packages/reports/src/Modules/SmartChart/Components/Markers/marker-spot-label.jsx
new file mode 100644
index 000000000000..bfcb77a258f9
--- /dev/null
+++ b/packages/reports/src/Modules/SmartChart/Components/Markers/marker-spot-label.jsx
@@ -0,0 +1,82 @@
+import classNames from 'classnames';
+import { observer } from 'mobx-react';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Icon, Text } from '@deriv/components';
+import { addComma, toMoment } from '@deriv/shared';
+
+import MarkerSpot from './marker-spot.jsx';
+
+const MarkerSpotLabel = ({
+ align_label,
+ has_hover_toggle,
+ spot_className,
+ spot_count,
+ spot_epoch,
+ spot_value,
+ status,
+}) => {
+ const [show_label, setShowLabel] = React.useState(!has_hover_toggle);
+
+ const handleHoverToggle = () => {
+ setShowLabel(!show_label);
+ };
+
+ let marker_spot = ;
+
+ if (has_hover_toggle) {
+ marker_spot = (
+
+ {marker_spot}
+
+ );
+ }
+
+ return (
+
+ {show_label && (
+
+
+
+
+
+ {toMoment(+spot_epoch).format('HH:mm:ss')}
+
+
+
+
{addComma(spot_value)}
+
+
+
+ )}
+ {marker_spot}
+
+ );
+};
+
+MarkerSpotLabel.defaultProps = {
+ align_label: 'top',
+};
+
+MarkerSpotLabel.propTypes = {
+ align_label: PropTypes.oneOf(['top', 'bottom']),
+ has_hover_toggle: PropTypes.bool,
+ spot_className: PropTypes.string,
+ spot_count: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ spot_epoch: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ spot_value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ status: PropTypes.oneOf(['won', 'lost']),
+};
+export default observer(MarkerSpotLabel);
diff --git a/packages/reports/src/Modules/SmartChart/Components/Markers/marker-spot.jsx b/packages/reports/src/Modules/SmartChart/Components/Markers/marker-spot.jsx
new file mode 100644
index 000000000000..a242334713b6
--- /dev/null
+++ b/packages/reports/src/Modules/SmartChart/Components/Markers/marker-spot.jsx
@@ -0,0 +1,15 @@
+import classNames from 'classnames';
+import { observer } from 'mobx-react';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const MarkerSpot = ({ className, spot_count }) => (
+ {spot_count}
+);
+
+MarkerSpot.propTypes = {
+ className: PropTypes.string,
+ spot_count: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+};
+
+export default observer(MarkerSpot);
diff --git a/packages/reports/src/Modules/SmartChart/Helpers/symbol.js b/packages/reports/src/Modules/SmartChart/Helpers/symbol.js
new file mode 100644
index 000000000000..00d2fadba0c6
--- /dev/null
+++ b/packages/reports/src/Modules/SmartChart/Helpers/symbol.js
@@ -0,0 +1,10 @@
+export const symbolChange = onSymbolChange =>
+ onSymbolChange &&
+ (symbol => {
+ onSymbolChange({
+ target: {
+ name: 'symbol',
+ value: symbol,
+ },
+ });
+ });
diff --git a/packages/trader/src/Stores/Modules/Profit/Helpers/format-request.js b/packages/reports/src/Stores/Modules/Profit/Helpers/format-request.js
similarity index 100%
rename from packages/trader/src/Stores/Modules/Profit/Helpers/format-request.js
rename to packages/reports/src/Stores/Modules/Profit/Helpers/format-request.js
diff --git a/packages/trader/src/Stores/Modules/Profit/Helpers/format-response.js b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js
similarity index 81%
rename from packages/trader/src/Stores/Modules/Profit/Helpers/format-response.js
rename to packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js
index db7e776c2249..f691a0a7b596 100644
--- a/packages/trader/src/Stores/Modules/Profit/Helpers/format-response.js
+++ b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js
@@ -1,7 +1,4 @@
-import { formatMoney, toMoment } from '@deriv/shared';
-
-import { getMarketInformation } from '../../../../Modules/Reports/Helpers/market-underlying';
-import { getSymbolDisplayName } from '../../Trading/Helpers/active-symbols';
+import { formatMoney, toMoment, getSymbolDisplayName, getMarketInformation } from '@deriv/shared';
export const formatProfitTableTransactions = (transaction, currency, active_symbols = []) => {
const format_string = 'DD MMM YYYY HH:mm:ss';
diff --git a/packages/trader/src/Stores/Modules/Profit/profit-store.js b/packages/reports/src/Stores/Modules/Profit/profit-store.js
similarity index 98%
rename from packages/trader/src/Stores/Modules/Profit/profit-store.js
rename to packages/reports/src/Stores/Modules/Profit/profit-store.js
index 00c4c07e137c..adc04536f095 100644
--- a/packages/trader/src/Stores/Modules/Profit/profit-store.js
+++ b/packages/reports/src/Stores/Modules/Profit/profit-store.js
@@ -72,7 +72,7 @@ export default class ProfitTableStore extends BaseStore {
formatProfitTableTransactions(
transaction,
this.root_store.client.currency,
- this.root_store.modules.trade.active_symbols
+ this.root_store.active_symbols.active_symbols
)
);
diff --git a/packages/trader/src/Stores/Modules/Statement/Helpers/__tests__/format-response.spec.js b/packages/reports/src/Stores/Modules/Statement/Helpers/__tests__/format-response.spec.js
similarity index 100%
rename from packages/trader/src/Stores/Modules/Statement/Helpers/__tests__/format-response.spec.js
rename to packages/reports/src/Stores/Modules/Statement/Helpers/__tests__/format-response.spec.js
diff --git a/packages/trader/src/Stores/Modules/Statement/Helpers/format-response.js b/packages/reports/src/Stores/Modules/Statement/Helpers/format-response.js
similarity index 88%
rename from packages/trader/src/Stores/Modules/Statement/Helpers/format-response.js
rename to packages/reports/src/Stores/Modules/Statement/Helpers/format-response.js
index 39992f536a7b..3111f2e37825 100644
--- a/packages/trader/src/Stores/Modules/Statement/Helpers/format-response.js
+++ b/packages/reports/src/Stores/Modules/Statement/Helpers/format-response.js
@@ -1,9 +1,5 @@
-import { formatMoney, toTitleCase, toMoment } from '@deriv/shared';
-
+import { formatMoney, toTitleCase, toMoment, getMarketInformation, getSymbolDisplayName } from '@deriv/shared';
import { localize } from '@deriv/translations';
-import { getMarketInformation } from 'Modules/Reports/Helpers/market-underlying';
-
-import { getSymbolDisplayName } from '../../Trading/Helpers/active-symbols';
export const formatStatementTransaction = (transaction, currency, active_symbols = []) => {
const format_string = 'DD MMM YYYY HH:mm:ss';
diff --git a/packages/trader/src/Stores/Modules/Statement/statement-store.js b/packages/reports/src/Stores/Modules/Statement/statement-store.js
similarity index 99%
rename from packages/trader/src/Stores/Modules/Statement/statement-store.js
rename to packages/reports/src/Stores/Modules/Statement/statement-store.js
index c7b70a83c6ed..fe03f89b329b 100644
--- a/packages/trader/src/Stores/Modules/Statement/statement-store.js
+++ b/packages/reports/src/Stores/Modules/Statement/statement-store.js
@@ -91,7 +91,7 @@ export default class StatementStore extends BaseStore {
formatStatementTransaction(
transaction,
this.root_store.client.currency,
- this.root_store.modules.trade.active_symbols
+ this.root_store.active_symbols.active_symbols
)
);
diff --git a/packages/reports/src/Stores/Modules/index.js b/packages/reports/src/Stores/Modules/index.js
new file mode 100644
index 000000000000..4f8e6853040f
--- /dev/null
+++ b/packages/reports/src/Stores/Modules/index.js
@@ -0,0 +1,9 @@
+import ProfitTableStore from './Profit/profit-store';
+import StatementStore from './Statement/statement-store';
+
+export default class ModulesStore {
+ constructor(root_store) {
+ this.profit_table = new ProfitTableStore({ root_store });
+ this.statement = new StatementStore({ root_store });
+ }
+}
diff --git a/packages/reports/src/Stores/base-store.js b/packages/reports/src/Stores/base-store.js
new file mode 100644
index 000000000000..3a803f0c2a40
--- /dev/null
+++ b/packages/reports/src/Stores/base-store.js
@@ -0,0 +1,537 @@
+import { action, intercept, observable, reaction, toJS, when } from 'mobx';
+import { isProduction, isEmptyObject } from '@deriv/shared';
+
+import Validator from '../Utils/Validator';
+
+/**
+ * BaseStore class is the base class for all defined stores in the application. It handles some stuff such as:
+ * 1. Creating snapshot object from the store.
+ * 2. Saving the store's snapshot in local/session storage and keeping them in sync.
+ */
+export default class BaseStore {
+ /**
+ * An enum object to define LOCAL_STORAGE and SESSION_STORAGE
+ */
+ static STORAGES = Object.freeze({
+ LOCAL_STORAGE: Symbol('LOCAL_STORAGE'),
+ SESSION_STORAGE: Symbol('SESSION_STORAGE'),
+ });
+
+ @observable
+ validation_errors = {};
+
+ @observable
+ validation_rules = {};
+
+ preSwitchAccountDisposer = null;
+ pre_switch_account_listener = null;
+
+ switchAccountDisposer = null;
+ switch_account_listener = null;
+
+ logoutDisposer = null;
+ logout_listener = null;
+
+ clientInitDisposer = null;
+ client_init_listener = null;
+
+ networkStatusChangeDisposer = null;
+ network_status_change_listener = null;
+
+ themeChangeDisposer = null;
+ theme_change_listener = null;
+
+ realAccountSignupEndedDisposer = null;
+ real_account_signup_ended_listener = null;
+
+ @observable partial_fetch_time = 0;
+
+ /**
+ * Constructor of the base class that gets properties' name of child which should be saved in storages
+ *
+ * @param {Object} options - An object that contains the following properties:
+ * @property {Object} root_store - An object that contains the root store of the app.
+ * @property {String[]} local_storage_properties - A list of properties' names that should be kept in localStorage.
+ * @property {String[]} session_storage_properties - A list of properties' names that should be kept in sessionStorage.
+ * @property {Object} validation_rules - An object that contains the validation rules for each property of the store.
+ * @property {String} store_name - Explicit store name for browser application storage (to bypass minification)
+ */
+ constructor(options = {}) {
+ const { root_store, local_storage_properties, session_storage_properties, validation_rules, store_name } =
+ options;
+
+ Object.defineProperty(this, 'root_store', {
+ enumerable: false,
+ writable: true,
+ });
+ Object.defineProperty(this, 'local_storage_properties', {
+ enumerable: false,
+ writable: true,
+ });
+ Object.defineProperty(this, 'session_storage_properties', {
+ enumerable: false,
+ writable: true,
+ });
+
+ const has_local_or_session_storage =
+ (local_storage_properties && local_storage_properties.length) ||
+ (session_storage_properties && session_storage_properties.length);
+
+ if (has_local_or_session_storage) {
+ if (!store_name) {
+ throw new Error('store_name is required for local/session storage');
+ }
+
+ Object.defineProperty(this, 'store_name', {
+ value: store_name,
+ enumerable: false,
+ writable: false,
+ });
+ }
+
+ this.root_store = root_store;
+ this.local_storage_properties = local_storage_properties || [];
+ this.session_storage_properties = session_storage_properties || [];
+ this.setValidationRules(validation_rules);
+
+ this.setupReactionForLocalStorage();
+ this.setupReactionForSessionStorage();
+ this.retrieveFromStorage();
+ }
+
+ /**
+ * Returns an snapshot of the current store
+ *
+ * @param {String[]} properties - A list of properties' names that should be in the snapshot.
+ *
+ * @return {Object} Returns a cloned object of the store.
+ */
+ getSnapshot(properties) {
+ let snapshot = toJS(this);
+
+ if (!isEmptyObject(this.root_store)) {
+ snapshot.root_store = this.root_store;
+ }
+
+ if (properties && properties.length) {
+ snapshot = properties.reduce((result, p) => Object.assign(result, { [p]: snapshot[p] }), {});
+ }
+
+ return snapshot;
+ }
+
+ /**
+ * Sets up a reaction on properties which are mentioned in `local_storage_properties`
+ * and invokes `saveToStorage` when there are any changes on them.
+ *
+ */
+ setupReactionForLocalStorage() {
+ if (this.local_storage_properties.length) {
+ reaction(
+ () => this.local_storage_properties.map(i => this[i]),
+ () => this.saveToStorage(this.local_storage_properties, BaseStore.STORAGES.LOCAL_STORAGE)
+ );
+ }
+ }
+
+ /**
+ * Sets up a reaction on properties which are mentioned in `session_storage_properties`
+ * and invokes `saveToStorage` when there are any changes on them.
+ *
+ */
+ setupReactionForSessionStorage() {
+ if (this.session_storage_properties.length) {
+ reaction(
+ () => this.session_storage_properties.map(i => this[i]),
+ () => this.saveToStorage(this.session_storage_properties, BaseStore.STORAGES.SESSION_STORAGE)
+ );
+ }
+ }
+
+ /**
+ * Removes properties that are not passed from the snapshot of the store and saves it to the passed storage
+ *
+ * @param {String[]} properties - A list of the store's properties' names which should be saved in the storage.
+ * @param {Symbol} storage - A symbol object that defines the storage which the snapshot should be stored in it.
+ *
+ */
+ saveToStorage(properties, storage) {
+ const snapshot = JSON.stringify(this.getSnapshot(properties), (key, value) => {
+ if (value !== null) return value;
+ return undefined;
+ });
+
+ if (storage === BaseStore.STORAGES.LOCAL_STORAGE) {
+ localStorage.setItem(this.store_name, snapshot);
+ } else if (storage === BaseStore.STORAGES.SESSION_STORAGE) {
+ sessionStorage.setItem(this.store_name, snapshot);
+ }
+ }
+
+ /**
+ * Retrieves saved snapshot of the store and assigns to the current instance.
+ *
+ */
+ @action
+ retrieveFromStorage() {
+ const local_storage_snapshot = JSON.parse(localStorage.getItem(this.store_name, {}));
+ const session_storage_snapshot = JSON.parse(sessionStorage.getItem(this.store_name, {}));
+
+ const snapshot = { ...local_storage_snapshot, ...session_storage_snapshot };
+
+ Object.keys(snapshot).forEach(k => (this[k] = snapshot[k]));
+ }
+
+ /**
+ * Sets validation error messages for an observable property of the store
+ *
+ * @param {String} propertyName - The observable property's name
+ * @param [{String}] messages - An array of strings that contains validation error messages for the particular property.
+ *
+ */
+ @action
+ setValidationErrorMessages(propertyName, messages) {
+ const is_different = () =>
+ !!this.validation_errors[propertyName]
+ .filter(x => !messages.includes(x))
+ .concat(messages.filter(x => !this.validation_errors[propertyName].includes(x))).length;
+ if (!this.validation_errors[propertyName] || is_different()) {
+ this.validation_errors[propertyName] = messages;
+ }
+ }
+
+ /**
+ * Sets validation rules
+ *
+ * @param {object} rules
+ *
+ */
+ @action
+ setValidationRules(rules = {}) {
+ Object.keys(rules).forEach(key => {
+ this.addRule(key, rules[key]);
+ });
+ }
+
+ /**
+ * Adds rules to the particular property
+ *
+ * @param {String} property
+ * @param {String} rules
+ *
+ */
+ @action
+ addRule(property, rules) {
+ this.validation_rules[property] = rules;
+
+ intercept(this, property, change => {
+ this.validateProperty(property, change.newValue);
+ return change;
+ });
+ }
+
+ /**
+ * Validates a particular property of the store
+ *
+ * @param {String} property - The name of the property in the store
+ * @param {object} value - The value of the property, it can be undefined.
+ *
+ */
+ @action
+ validateProperty(property, value) {
+ const trigger = this.validation_rules[property].trigger;
+ const inputs = { [property]: value !== undefined ? value : this[property] };
+ const validation_rules = { [property]: this.validation_rules[property].rules || [] };
+
+ if (!!trigger && Object.hasOwnProperty.call(this, trigger)) {
+ inputs[trigger] = this[trigger];
+ validation_rules[trigger] = this.validation_rules[trigger].rules || [];
+ }
+
+ const validator = new Validator(inputs, validation_rules, this);
+
+ validator.isPassed();
+
+ Object.keys(inputs).forEach(key => {
+ this.setValidationErrorMessages(key, validator.errors.get(key));
+ });
+ }
+
+ /**
+ * Validates all properties which validation rule has been set for.
+ *
+ */
+ @action
+ validateAllProperties() {
+ const validation_rules = Object.keys(this.validation_rules);
+ const validation_errors = Object.keys(this.validation_errors);
+
+ validation_rules.forEach(p => {
+ this.validateProperty(p, this[p]);
+ });
+
+ // Remove keys that are present in error, but not in rules:
+ validation_errors.forEach(error => {
+ if (!validation_rules.includes(error)) {
+ delete this.validation_errors[error];
+ }
+ });
+ }
+
+ @action.bound
+ onSwitchAccount(listener) {
+ if (listener) {
+ this.switch_account_listener = listener;
+
+ this.switchAccountDisposer = when(
+ () => this.root_store.client.switch_broadcast,
+ () => {
+ try {
+ const result = this.switch_account_listener();
+ if (result && result.then && typeof result.then === 'function') {
+ result.then(() => {
+ this.root_store.client.switchEndSignal();
+ this.onSwitchAccount(this.switch_account_listener);
+ });
+ } else {
+ throw new Error('Switching account listeners are required to return a promise.');
+ }
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+ }
+ }
+
+ @action.bound
+ onPreSwitchAccount(listener) {
+ if (listener) {
+ this.pre_switch_account_listener = listener;
+ this.preSwitchAccountDisposer = when(
+ () => this.root_store.client.pre_switch_broadcast,
+ () => {
+ try {
+ const result = this.pre_switch_account_listener();
+ if (result && result.then && typeof result.then === 'function') {
+ result.then(() => {
+ this.root_store.client.setPreSwitchAccount(false);
+ this.onPreSwitchAccount(this.pre_switch_account_listener);
+ });
+ } else {
+ throw new Error('Pre-switch account listeners are required to return a promise.');
+ }
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+ }
+ }
+
+ @action.bound
+ onLogout(listener) {
+ this.logoutDisposer = when(
+ () => this.root_store.client.has_logged_out,
+ async () => {
+ try {
+ const result = this.logout_listener();
+ if (result && result.then && typeof result.then === 'function') {
+ result.then(() => {
+ this.root_store.client.setLogout(false);
+ this.onLogout(this.logout_listener);
+ });
+ } else {
+ throw new Error('Logout listeners are required to return a promise.');
+ }
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+ this.logout_listener = listener;
+ }
+
+ @action.bound
+ onClientInit(listener) {
+ this.clientInitDisposer = when(
+ () => this.root_store.client.initialized_broadcast,
+ async () => {
+ try {
+ const result = this.client_init_listener();
+ if (result && result.then && typeof result.then === 'function') {
+ result.then(() => {
+ this.root_store.client.setInitialized(false);
+ this.onClientInit(this.client_init_listener);
+ });
+ } else {
+ throw new Error('Client init listeners are required to return a promise.');
+ }
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+ this.client_init_listener = listener;
+ }
+
+ @action.bound
+ onNetworkStatusChange(listener) {
+ this.networkStatusChangeDisposer = reaction(
+ () => this.root_store.common.is_network_online,
+ is_online => {
+ try {
+ this.network_status_change_listener(is_online);
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+
+ this.network_status_change_listener = listener;
+ }
+
+ @action.bound
+ onThemeChange(listener) {
+ this.themeChangeDisposer = reaction(
+ () => this.root_store.ui.is_dark_mode_on,
+ is_dark_mode_on => {
+ try {
+ this.theme_change_listener(is_dark_mode_on);
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+
+ this.theme_change_listener = listener;
+ }
+
+ @action.bound
+ onRealAccountSignupEnd(listener) {
+ this.realAccountSignupEndedDisposer = when(
+ () => this.root_store.ui.has_real_account_signup_ended,
+ () => {
+ try {
+ const result = this.real_account_signup_ended_listener();
+ if (result && result.then && typeof result.then === 'function') {
+ result.then(() => {
+ this.root_store.ui.setRealAccountSignupEnd(false);
+ this.onRealAccountSignupEnd(this.real_account_signup_ended_listener);
+ });
+ } else {
+ throw new Error('Real account signup listeners are required to return a promise.');
+ }
+ } catch (error) {
+ // there is no listener currently active. so we can just ignore the error raised from treating
+ // a null object as a function. Although, in development mode, we throw a console error.
+ if (!isProduction()) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+ }
+ );
+
+ this.real_account_signup_ended_listener = listener;
+ }
+
+ @action.bound
+ disposePreSwitchAccount() {
+ if (typeof this.preSwitchAccountDisposer === 'function') {
+ this.preSwitchAccountDisposer();
+ }
+ this.pre_switch_account_listener = null;
+ }
+
+ @action.bound
+ disposeSwitchAccount() {
+ if (typeof this.switchAccountDisposer === 'function') {
+ this.switchAccountDisposer();
+ }
+ this.switch_account_listener = null;
+ }
+
+ @action.bound
+ disposeLogout() {
+ if (typeof this.logoutDisposer === 'function') {
+ this.logoutDisposer();
+ }
+ this.logout_listener = null;
+ }
+
+ @action.bound
+ disposeClientInit() {
+ if (typeof this.clientInitDisposer === 'function') {
+ this.clientInitDisposer();
+ }
+ this.client_init_listener = null;
+ }
+
+ @action.bound
+ disposeNetworkStatusChange() {
+ if (typeof this.networkStatusChangeDisposer === 'function') {
+ this.networkStatusChangeDisposer();
+ }
+ this.network_status_change_listener = null;
+ }
+
+ @action.bound
+ disposeThemeChange() {
+ if (typeof this.themeChangeDisposer === 'function') {
+ this.themeChangeDisposer();
+ }
+ this.theme_change_listener = null;
+ }
+
+ @action.bound
+ disposeRealAccountSignupEnd() {
+ if (typeof this.realAccountSignupEndedDisposer === 'function') {
+ this.realAccountSignupEndedDisposer();
+ }
+ this.real_account_signup_ended_listener = null;
+ }
+
+ @action.bound
+ onUnmount() {
+ this.disposePreSwitchAccount();
+ this.disposeSwitchAccount();
+ this.disposeLogout();
+ this.disposeClientInit();
+ this.disposeNetworkStatusChange();
+ this.disposeThemeChange();
+ this.disposeRealAccountSignupEnd();
+ }
+
+ @action.bound
+ assertHasValidCache(loginid, ...reactions) {
+ // account was changed when this was unmounted.
+ if (this.root_store.client.loginid !== loginid) {
+ reactions.forEach(act => act());
+ this.partial_fetch_time = false;
+ }
+ }
+}
diff --git a/packages/reports/src/Stores/connect.js b/packages/reports/src/Stores/connect.js
new file mode 100644
index 000000000000..4ef42c8d18b6
--- /dev/null
+++ b/packages/reports/src/Stores/connect.js
@@ -0,0 +1,31 @@
+import { useObserver } from 'mobx-react';
+import React from 'react';
+
+const isClassComponent = Component =>
+ !!(typeof Component === 'function' && Component.prototype && Component.prototype.isReactComponent);
+
+export const MobxContent = React.createContext(null);
+
+function injectStorePropsToComponent(propsToSelectFn, BaseComponent) {
+ const Component = own_props => {
+ const store = React.useContext(MobxContent);
+
+ let ObservedComponent = BaseComponent;
+
+ if (isClassComponent(BaseComponent)) {
+ const FunctionalWrapperComponent = props => ;
+ ObservedComponent = FunctionalWrapperComponent;
+ }
+
+ return useObserver(() => ObservedComponent({ ...own_props, ...propsToSelectFn(store, own_props) }));
+ };
+
+ Component.displayName = BaseComponent.name;
+ return Component;
+}
+
+export const MobxContentProvider = ({ store, children }) => {
+ return {children};
+};
+
+export const connect = propsToSelectFn => Component => injectStorePropsToComponent(propsToSelectFn, Component);
diff --git a/packages/reports/src/Stores/index.js b/packages/reports/src/Stores/index.js
new file mode 100644
index 000000000000..f0495d2fb985
--- /dev/null
+++ b/packages/reports/src/Stores/index.js
@@ -0,0 +1,19 @@
+import ModulesStore from './Modules';
+
+export default class RootStore {
+ constructor(core_store) {
+ this.client = core_store.client;
+ this.common = core_store.common;
+ this.modules = new ModulesStore(this, core_store);
+ this.ui = core_store.ui;
+ this.gtm = core_store.gtm;
+ this.rudderstack = core_store.rudderstack;
+ this.pushwoosh = core_store.pushwoosh;
+ this.notifications = core_store.notifications;
+ this.contract_replay = core_store.contract_replay;
+ this.contract_trade = core_store.contract_trade;
+ this.portfolio = core_store.portfolio;
+ this.chart_barrier_store = core_store.chart_barrier_store;
+ this.active_symbols = core_store.active_symbols;
+ }
+}
diff --git a/packages/reports/src/Utils/Validator/__tests__/error.spec.js b/packages/reports/src/Utils/Validator/__tests__/error.spec.js
new file mode 100644
index 000000000000..d99897f955be
--- /dev/null
+++ b/packages/reports/src/Utils/Validator/__tests__/error.spec.js
@@ -0,0 +1,53 @@
+import { expect } from 'chai';
+import Errors from '../errors';
+
+describe('Error', () => {
+ let errors;
+ beforeEach(() => {
+ errors = new Errors();
+ errors.add('Error', 100);
+ });
+
+ describe('.add', () => {
+ it('should add error to errors', () => {
+ errors.add('Error', 101);
+ expect(errors.errors).to.have.property('Error').with.length(2);
+ });
+ it('should not add error if already existed', () => {
+ errors.add('Error', 100);
+ expect(errors.errors).to.have.property('Error').with.length(1);
+ });
+ });
+
+ describe('.all', () => {
+ it('should return all errors', () => {
+ expect(errors.all()).to.be.eql({
+ Error: [100],
+ });
+ });
+ });
+
+ describe('.first', () => {
+ it('should return first error if attribute exists', () => {
+ expect(errors.first('Error')).to.eql(100);
+ });
+ });
+
+ describe('.get', () => {
+ it('should return data if attribute exists', () => {
+ expect(errors.get('Error')).to.eql([100]);
+ });
+ it('should return [] if attribute does not exist', () => {
+ expect(errors.get('')).to.eql([]);
+ });
+ });
+
+ describe('.has', () => {
+ it('should return true if attribute exists', () => {
+ expect(errors.has('Error')).to.be.true;
+ });
+ it('should return false if attribute does not exists', () => {
+ expect(errors.has('')).to.be.false;
+ });
+ });
+});
diff --git a/packages/reports/src/Utils/Validator/errors.js b/packages/reports/src/Utils/Validator/errors.js
new file mode 100644
index 000000000000..8c3307765b64
--- /dev/null
+++ b/packages/reports/src/Utils/Validator/errors.js
@@ -0,0 +1,40 @@
+class Errors {
+ constructor() {
+ this.errors = {};
+ }
+
+ add(attribute, message) {
+ if (!this.has(attribute)) {
+ this.errors[attribute] = [];
+ }
+
+ if (this.errors[attribute].indexOf(message) === -1) {
+ this.errors[attribute].push(message);
+ }
+ }
+
+ all() {
+ return this.errors;
+ }
+
+ first(attribute) {
+ if (this.has(attribute)) {
+ return this.errors[attribute][0];
+ }
+ return null;
+ }
+
+ get(attribute) {
+ if (this.has(attribute)) {
+ return this.errors[attribute];
+ }
+
+ return [];
+ }
+
+ has(attribute) {
+ return Object.prototype.hasOwnProperty.call(this.errors, attribute);
+ }
+}
+
+export default Errors;
diff --git a/packages/reports/src/Utils/Validator/index.js b/packages/reports/src/Utils/Validator/index.js
new file mode 100644
index 000000000000..1b7f5cfa8611
--- /dev/null
+++ b/packages/reports/src/Utils/Validator/index.js
@@ -0,0 +1 @@
+export default from './validator';
diff --git a/packages/reports/src/Utils/Validator/validator.js b/packages/reports/src/Utils/Validator/validator.js
new file mode 100644
index 000000000000..c8aa2ce59dff
--- /dev/null
+++ b/packages/reports/src/Utils/Validator/validator.js
@@ -0,0 +1,112 @@
+import { template } from '_common/utility';
+import { getPreBuildDVRs } from '@deriv/shared';
+import Error from './errors';
+
+class Validator {
+ constructor(input, rules, store = null) {
+ this.input = input;
+ this.rules = rules;
+ this.store = store;
+ this.errors = new Error();
+
+ this.error_count = 0;
+ }
+
+ /**
+ * Add failure and error message for given rule
+ *
+ * @param {string} attribute
+ * @param {object} rule
+ */
+ addFailure(attribute, rule, error_message) {
+ let message = error_message || rule.options.message || getPreBuildDVRs()[rule.name].message();
+ if (rule.name === 'length') {
+ message = template(message, [
+ rule.options.min === rule.options.max ? rule.options.min : `${rule.options.min}-${rule.options.max}`,
+ ]);
+ } else if (rule.name === 'min') {
+ message = template(message, [rule.options.min]);
+ } else if (rule.name === 'not_equal') {
+ message = template(message, [rule.options.name1, rule.options.name2]);
+ }
+ this.errors.add(attribute, message);
+ this.error_count++;
+ }
+
+ /**
+ * Runs validator
+ *
+ * @return {boolean} Whether it passes; true = passes, false = fails
+ */
+ check() {
+ Object.keys(this.input).forEach(attribute => {
+ if (!Object.prototype.hasOwnProperty.call(this.rules, attribute)) {
+ return;
+ }
+
+ this.rules[attribute].forEach(rule => {
+ const ruleObject = Validator.getRuleObject(rule);
+
+ if (!ruleObject.validator && typeof ruleObject.validator !== 'function') {
+ return;
+ }
+
+ if (ruleObject.options.condition && !ruleObject.options.condition(this.store)) {
+ return;
+ }
+
+ if (this.input[attribute] === '' && ruleObject.name !== 'req') {
+ return;
+ }
+
+ let is_valid, error_message;
+ if (ruleObject.name === 'number') {
+ const { is_ok, message } = ruleObject.validator(
+ this.input[attribute],
+ ruleObject.options,
+ this.store,
+ this.input
+ );
+ is_valid = is_ok;
+ error_message = message;
+ } else {
+ is_valid = ruleObject.validator(this.input[attribute], ruleObject.options, this.store, this.input);
+ }
+
+ if (!is_valid) {
+ this.addFailure(attribute, ruleObject, error_message);
+ }
+ });
+ });
+ return !this.error_count;
+ }
+
+ /**
+ * Determine if validation passes
+ *
+ * @return {boolean}
+ */
+ isPassed() {
+ return this.check();
+ }
+
+ /**
+ * Converts the rule array to an object
+ *
+ * @param {array} rule
+ * @return {object}
+ */
+ static getRuleObject(rule) {
+ const is_rule_string = typeof rule === 'string';
+ const rule_object = {
+ name: is_rule_string ? rule : rule[0],
+ options: is_rule_string ? {} : rule[1] || {},
+ };
+
+ rule_object.validator = rule_object.name === 'custom' ? rule[1].func : getPreBuildDVRs()[rule_object.name].func;
+
+ return rule_object;
+ }
+}
+
+export default Validator;
diff --git a/packages/reports/src/_common/__tests__/utility.js b/packages/reports/src/_common/__tests__/utility.js
new file mode 100644
index 000000000000..f138acbc03e1
--- /dev/null
+++ b/packages/reports/src/_common/__tests__/utility.js
@@ -0,0 +1,16 @@
+const expect = require('chai').expect;
+const Utility = require('../utility');
+
+describe('Utility', () => {
+ describe('.template()', () => {
+ it('works as expected', () => {
+ expect(Utility.template('abc [_1] abc', ['2'])).to.eq('abc 2 abc');
+ expect(Utility.template('[_1] [_2]', ['1', '2'])).to.eq('1 2');
+ expect(Utility.template('[_1] [_1]', ['1'])).to.eq('1 1');
+ });
+
+ it('does not replace twice', () => {
+ expect(Utility.template('[_1] [_2]', ['[_2]', 'abc'])).to.eq('[_2] abc');
+ });
+ });
+});
diff --git a/packages/reports/src/_common/base/server_time.js b/packages/reports/src/_common/base/server_time.js
new file mode 100644
index 000000000000..b639380b27a5
--- /dev/null
+++ b/packages/reports/src/_common/base/server_time.js
@@ -0,0 +1,25 @@
+const PromiseClass = require('../utility').PromiseClass;
+
+const ServerTime = (() => {
+ let clock_started = false;
+ const pending = new PromiseClass();
+ let common_store;
+
+ const init = store => {
+ if (!clock_started) {
+ common_store = store;
+ pending.resolve(common_store.server_time);
+ clock_started = true;
+ }
+ };
+
+ const get = () => (clock_started && common_store.server_time ? common_store.server_time.clone() : undefined);
+
+ return {
+ init,
+ get,
+ timePromise: () => (clock_started ? Promise.resolve(common_store.server_time) : pending.promise),
+ };
+})();
+
+module.exports = ServerTime;
diff --git a/packages/reports/src/_common/components/loading.jsx b/packages/reports/src/_common/components/loading.jsx
new file mode 100644
index 000000000000..1e1379209e36
--- /dev/null
+++ b/packages/reports/src/_common/components/loading.jsx
@@ -0,0 +1,15 @@
+import classNames from 'classnames';
+import React from 'react';
+
+const Loading = ({ className, is_invisible, theme, id }) => (
+
+ {Array.from(new Array(5)).map((x, inx) => (
+
+ ))}
+
+);
+
+export default Loading;
diff --git a/packages/reports/src/_common/contract.js b/packages/reports/src/_common/contract.js
new file mode 100644
index 000000000000..b1797962c68d
--- /dev/null
+++ b/packages/reports/src/_common/contract.js
@@ -0,0 +1,272 @@
+import React from 'react';
+import { localize, Localize } from '@deriv/translations';
+
+export const getCardLabels = () => ({
+ APPLY: localize('Apply'),
+ STAKE: localize('Stake:'),
+ CLOSE: localize('Close'),
+ CANCEL: localize('Cancel'),
+ CURRENT_STAKE: localize('Current stake:'),
+ DEAL_CANCEL_FEE: localize('Deal cancel. fee:'),
+ TAKE_PROFIT: localize('Take profit:'),
+ BUY_PRICE: localize('Buy price:'),
+ STOP_LOSS: localize('Stop loss:'),
+ TOTAL_PROFIT_LOSS: localize('Total profit/loss:'),
+ PROFIT_LOSS: localize('Profit/Loss:'),
+ POTENTIAL_PROFIT_LOSS: localize('Potential profit/loss:'),
+ INDICATIVE_PRICE: localize('Indicative price:'),
+ PAYOUT: localize('Sell price:'),
+ PURCHASE_PRICE: localize('Buy price:'),
+ POTENTIAL_PAYOUT: localize('Payout limit:'),
+ TICK: localize('Tick '),
+ WON: localize('Won'),
+ LOST: localize('Lost'),
+ DAYS: localize('days'),
+ DAY: localize('day'),
+ SELL: localize('Sell'),
+ INCREMENT_VALUE: localize('Increment value'),
+ DECREMENT_VALUE: localize('Decrement value'),
+ TAKE_PROFIT_LOSS_NOT_AVAILABLE: localize(
+ 'Take profit and/or stop loss are not available while deal cancellation is active.'
+ ),
+ DONT_SHOW_THIS_AGAIN: localize("Don't show this again"),
+ RESALE_NOT_OFFERED: localize('Resale not offered'),
+ NOT_AVAILABLE: localize('N/A'),
+});
+
+export const getMarketNamesMap = () => ({
+ FRXAUDCAD: localize('AUD/CAD'),
+ FRXAUDCHF: localize('AUD/CHF'),
+ FRXAUDJPY: localize('AUD/JPY'),
+ FRXAUDNZD: localize('AUD/NZD'),
+ FRXAUDPLN: localize('AUD/PLN'),
+ FRXAUDUSD: localize('AUD/USD'),
+ FRXBROUSD: localize('Oil/USD'),
+ FRXEURAUD: localize('EUR/AUD'),
+ FRXEURCAD: localize('EUR/CAD'),
+ FRXEURCHF: localize('EUR/CHF'),
+ FRXEURGBP: localize('EUR/GBP'),
+ FRXEURJPY: localize('EUR/JPY'),
+ FRXEURNZD: localize('EUR/NZD'),
+ FRXEURUSD: localize('EUR/USD'),
+ FRXGBPAUD: localize('GBP/AUD'),
+ FRXGBPCAD: localize('GBP/CAD'),
+ FRXGBPCHF: localize('GBP/CHF'),
+ FRXGBPJPY: localize('GBP/JPY'),
+ FRXGBPNOK: localize('GBP/NOK'),
+ FRXGBPUSD: localize('GBP/USD'),
+ FRXNZDJPY: localize('NZD/JPY'),
+ FRXNZDUSD: localize('NZD/USD'),
+ FRXUSDCAD: localize('USD/CAD'),
+ FRXUSDCHF: localize('USD/CHF'),
+ FRXUSDJPY: localize('USD/JPY'),
+ FRXUSDNOK: localize('USD/NOK'),
+ FRXUSDPLN: localize('USD/PLN'),
+ FRXUSDSEK: localize('USD/SEK'),
+ FRXXAGUSD: localize('Silver/USD'),
+ FRXXAUUSD: localize('Gold/USD'),
+ FRXXPDUSD: localize('Palladium/USD'),
+ FRXXPTUSD: localize('Platinum/USD'),
+ OTC_AEX: localize('Dutch Index'),
+ OTC_AS51: localize('Australian Index'),
+ OTC_DJI: localize('Wall Street Index'),
+ OTC_FCHI: localize('French Index'),
+ OTC_FTSE: localize('UK Index'),
+ OTC_GDAXI: localize('German Index'),
+ OTC_HSI: localize('Hong Kong Index'),
+ OTC_IBEX35: localize('Spanish Index'),
+ OTC_N225: localize('Japanese Index'),
+ OTC_NDX: localize('US Tech Index'),
+ OTC_SPC: localize('US Index'),
+ OTC_SSMI: localize('Swiss Index'),
+ OTC_SX5E: localize('Euro 50 Index'),
+ R_10: localize('Volatility 10 Index'),
+ R_25: localize('Volatility 25 Index'),
+ R_50: localize('Volatility 50 Index'),
+ R_75: localize('Volatility 75 Index'),
+ R_100: localize('Volatility 100 Index'),
+ BOOM300N: localize('Boom 300 Index'),
+ BOOM500: localize('Boom 500 Index'),
+ BOOM1000: localize('Boom 1000 Index'),
+ CRASH300N: localize('Crash 300 Index'),
+ CRASH500: localize('Crash 500 Index'),
+ CRASH1000: localize('Crash 1000 Index'),
+ RDBEAR: localize('Bear Market Index'),
+ RDBULL: localize('Bull Market Index'),
+ STPRNG: localize('Step Index'),
+ WLDAUD: localize('AUD Index'),
+ WLDEUR: localize('EUR Index'),
+ WLDGBP: localize('GBP Index'),
+ WLDXAU: localize('Gold Index'),
+ WLDUSD: localize('USD Index'),
+ '1HZ10V': localize('Volatility 10 (1s) Index'),
+ '1HZ100V': localize('Volatility 100 (1s) Index'),
+ '1HZ200V': localize('Volatility 200 (1s) Index'),
+ '1HZ300V': localize('Volatility 300 (1s) Index'),
+ JD10: localize('Jump 10 Index'),
+ JD25: localize('Jump 25 Index'),
+ JD50: localize('Jump 50 Index'),
+ JD75: localize('Jump 75 Index'),
+ JD100: localize('Jump 100 Index'),
+ JD150: localize('Jump 150 Index'),
+ JD200: localize('Jump 200 Index'),
+ CRYBCHUSD: localize('BCH/USD'),
+ CRYBNBUSD: localize('BNB/USD'),
+ CRYBTCLTC: localize('BTC/LTC'),
+ CRYIOTUSD: localize('IOT/USD'),
+ CRYNEOUSD: localize('NEO/USD'),
+ CRYOMGUSD: localize('OMG/USD'),
+ CRYTRXUSD: localize('TRX/USD'),
+ CRYBTCETH: localize('BTC/ETH'),
+ CRYZECUSD: localize('ZEC/USD'),
+ CRYXMRUSD: localize('ZMR/USD'),
+ CRYXMLUSD: localize('XLM/USD'),
+ CRYXRPUSD: localize('XRP/USD'),
+ CRYBTCUSD: localize('BTC/USD'),
+ CRYDSHUSD: localize('DSH/USD'),
+ CRYETHUSD: localize('ETH/USD'),
+ CRYEOSUSD: localize('EOS/USD'),
+ CRYLTCUSD: localize('LTC/USD'),
+});
+
+export const getUnsupportedContracts = () => ({
+ EXPIRYMISS: {
+ name: ,
+ position: 'top',
+ },
+ EXPIRYRANGE: {
+ name: ,
+ position: 'bottom',
+ },
+ RANGE: {
+ name: ,
+ position: 'top',
+ },
+ UPORDOWN: {
+ name: ,
+ position: 'bottom',
+ },
+ RESETCALL: {
+ name: ,
+ position: 'top',
+ },
+ RESETPUT: {
+ name: ,
+ position: 'bottom',
+ },
+ TICKHIGH: {
+ name: ,
+ position: 'top',
+ },
+ TICKLOW: {
+ name: ,
+ position: 'bottom',
+ },
+ ASIANU: {
+ name: ,
+ position: 'top',
+ },
+ ASIAND: {
+ name: ,
+ position: 'bottom',
+ },
+ LBFLOATCALL: {
+ name: ,
+ position: 'top',
+ },
+ LBFLOATPUT: {
+ name: ,
+ position: 'top',
+ },
+ LBHIGHLOW: {
+ name: ,
+ position: 'top',
+ },
+ CALLSPREAD: {
+ name: ,
+ position: 'top',
+ },
+ PUTSPREAD: {
+ name: ,
+ position: 'bottom',
+ },
+ RUNHIGH: {
+ name: ,
+ position: 'top',
+ },
+ RUNLOW: {
+ name: ,
+ position: 'bottom',
+ },
+});
+
+export const getSupportedContracts = is_high_low => ({
+ CALL: {
+ name: is_high_low ? : ,
+ position: 'top',
+ },
+ PUT: {
+ name: is_high_low ? : ,
+ position: 'bottom',
+ },
+ CALLE: {
+ name: ,
+ position: 'top',
+ },
+ PUTE: {
+ name: ,
+ position: 'bottom',
+ },
+ DIGITMATCH: {
+ name: ,
+ position: 'top',
+ },
+ DIGITDIFF: {
+ name: ,
+ position: 'bottom',
+ },
+ DIGITEVEN: {
+ name: ,
+ position: 'top',
+ },
+ DIGITODD: {
+ name: ,
+ position: 'bottom',
+ },
+ DIGITOVER: {
+ name: ,
+ position: 'top',
+ },
+ DIGITUNDER: {
+ name: ,
+ position: 'bottom',
+ },
+ ONETOUCH: {
+ name: ,
+ position: 'top',
+ },
+ NOTOUCH: {
+ name: ,
+ position: 'bottom',
+ },
+ MULTUP: {
+ name: ,
+ position: 'top',
+ },
+ MULTDOWN: {
+ name: ,
+ position: 'bottom',
+ },
+});
+
+export const getContractConfig = is_high_low => ({
+ ...getSupportedContracts(is_high_low),
+ ...getUnsupportedContracts(),
+});
+
+export const getContractTypeDisplay = (type, is_high_low = false) => {
+ return getContractConfig(is_high_low)[type] ? getContractConfig(is_high_low)[type.toUpperCase()].name : '';
+};
+
+export const getContractTypePosition = (type, is_high_low = false) =>
+ getContractConfig(is_high_low)[type] ? getContractConfig(is_high_low)[type.toUpperCase()].position : 'top';
diff --git a/packages/reports/src/_common/utility.js b/packages/reports/src/_common/utility.js
new file mode 100644
index 000000000000..5c20dc5c33e0
--- /dev/null
+++ b/packages/reports/src/_common/utility.js
@@ -0,0 +1,52 @@
+const template = (string, content) => {
+ let to_replace = content;
+ if (content && !Array.isArray(content)) {
+ to_replace = [content];
+ }
+ return string.replace(/\[_(\d+)]/g, (s, index) => to_replace[+index - 1]);
+};
+
+/**
+ * Creates a DOM element and adds any attributes to it.
+ *
+ * @param {String} tag_name: the tag to create, e.g. 'div', 'a', etc
+ * @param {Object} attributes: all the attributes to assign, e.g. { id: '...', class: '...', html: '...', ... }
+ * @return the created DOM element
+ */
+const createElement = (tag_name, attributes = {}) => {
+ const el = document.createElement(tag_name);
+ Object.keys(attributes).forEach(attr => {
+ const value = attributes[attr];
+ if (attr === 'text') {
+ el.textContent = value;
+ } else if (attr === 'html') {
+ el.html(value);
+ } else {
+ el.setAttribute(attr, value);
+ }
+ });
+ return el;
+};
+
+let static_hash;
+const getStaticHash = () => {
+ static_hash =
+ static_hash || (document.querySelector('script[src*="main"]').getAttribute('src') || '').split('.')[1];
+ return static_hash;
+};
+
+class PromiseClass {
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.reject = reject;
+ this.resolve = resolve;
+ });
+ }
+}
+
+module.exports = {
+ template,
+ createElement,
+ getStaticHash,
+ PromiseClass,
+};
diff --git a/packages/reports/src/app.jsx b/packages/reports/src/app.jsx
new file mode 100644
index 000000000000..52db330e336e
--- /dev/null
+++ b/packages/reports/src/app.jsx
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Routes from 'Containers/routes.jsx';
+import { MobxContentProvider } from 'Stores/connect';
+
+import initStore from './init-store'; // eslint-disable-line import/extensions
+
+const App = ({ passthrough }) => {
+ const [root_store] = React.useState(initStore(passthrough.root_store, passthrough.WS));
+
+ return (
+
+
+
+
+
+ );
+};
+
+App.propTypes = {
+ passthrough: PropTypes.shape({
+ root_store: PropTypes.object,
+ WS: PropTypes.object,
+ }),
+};
+
+export default App;
diff --git a/packages/reports/src/index.jsx b/packages/reports/src/index.jsx
new file mode 100644
index 000000000000..c5c2db91b385
--- /dev/null
+++ b/packages/reports/src/index.jsx
@@ -0,0 +1,14 @@
+import 'promise-polyfill';
+
+import 'event-source-polyfill';
+
+import React from 'react';
+import { makeLazyLoader } from '@deriv/shared';
+import { Loading } from '@deriv/components';
+
+const App = makeLazyLoader(
+ () => import(/* webpackChunkName: "reports-app", webpackPreload: true */ './app.jsx'),
+ () =>
+)();
+
+export default App;
diff --git a/packages/reports/src/init-store.js b/packages/reports/src/init-store.js
new file mode 100644
index 000000000000..f6ad9f411595
--- /dev/null
+++ b/packages/reports/src/init-store.js
@@ -0,0 +1,20 @@
+import { configure } from 'mobx';
+import RootStore from 'Stores';
+import { setWebsocket } from '@deriv/shared';
+import ServerTime from '_common/base/server_time';
+
+configure({ enforceActions: 'observed' });
+
+let root_store;
+
+const initStore = (core_store, websocket) => {
+ if (root_store) return root_store;
+
+ ServerTime.init(core_store.common);
+ setWebsocket(websocket);
+ root_store = new RootStore(core_store);
+
+ return root_store;
+};
+
+export default initStore;
diff --git a/packages/reports/src/sass/app.scss b/packages/reports/src/sass/app.scss
new file mode 100644
index 000000000000..f32a3ebdf759
--- /dev/null
+++ b/packages/reports/src/sass/app.scss
@@ -0,0 +1,17 @@
+// Layout
+@import 'app/_common/layout/trader-layouts';
+
+// Drawers
+@import 'app/_common/drawer/positions-drawer';
+
+// Components
+@import 'app/_common/components/amount';
+@import 'app/_common/components/allow-equals';
+//@import 'app/_common/components/calendar'; // TODO: [move-to-components] Calendar component should be moved
+@import 'app/_common/components/card-list';
+//@import 'app/_common/components/date-picker'; // TODO: [move-to-components] Datepicker component should be moved
+@import 'app/_common/components/market-symbol-icon';
+
+// Modules
+@import 'app/modules/portfolio';
+@import 'app/modules/smart-chart';
diff --git a/packages/reports/src/sass/app/_common/components/allow-equals.scss b/packages/reports/src/sass/app/_common/components/allow-equals.scss
new file mode 100644
index 000000000000..ea3d2b81384c
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/components/allow-equals.scss
@@ -0,0 +1,19 @@
+/** @define allow-equals */
+.allow-equals {
+ display: flex;
+ align-items: center;
+ position: relative;
+ margin-top: 0.6em;
+
+ &__label {
+ @include typeface(--paragraph-left-normal-black, none);
+ @include toEm(padding, 0 8px, 1.2em);
+ color: var(--text-general);
+ cursor: pointer;
+ }
+ @include mobile {
+ &__subtitle {
+ color: var(--text-less-prominent);
+ }
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/components/amount.scss b/packages/reports/src/sass/app/_common/components/amount.scss
new file mode 100644
index 000000000000..ab4916398f02
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/components/amount.scss
@@ -0,0 +1,8 @@
+.amount {
+ &--profit {
+ color: var(--text-profit-success);
+ }
+ &--loss {
+ color: var(--text-loss-danger);
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/components/card-list.scss b/packages/reports/src/sass/app/_common/components/card-list.scss
new file mode 100644
index 000000000000..9d4017837c29
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/components/card-list.scss
@@ -0,0 +1,20 @@
+/** @define card-list */
+.card-list {
+ overflow: auto; // fixes margin collapse
+
+ & &__card {
+ // keep & for higher specificity
+ display: block;
+ text-decoration: none;
+ max-width: 450px;
+ margin: 0.6em auto;
+ border-radius: 4px;
+ background-color: var(--general-main-2);
+ border: 1px solid var(--general-main-2);
+ color: var(--text-prominent);
+
+ &-link {
+ cursor: pointer;
+ }
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/components/composite-calendar.scss b/packages/reports/src/sass/app/_common/components/composite-calendar.scss
new file mode 100644
index 000000000000..e05ee710e3a6
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/components/composite-calendar.scss
@@ -0,0 +1,183 @@
+/* @define .composite-calendar; weak; */
+.composite-calendar {
+ display: grid;
+ grid-template-columns: 128px minmax(min-content, 280px) minmax(min-content, 280px);
+ position: absolute;
+ top: 36px;
+ right: 0;
+ z-index: 99;
+ border-radius: $BORDER_RADIUS;
+ background-color: var(--general-main-2);
+ box-shadow: 0 2px 16px 8px var(--shadow-menu);
+
+ .composite-wrapper {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: var(--general-main-1);
+ z-index: 98;
+ }
+ &__input-fields {
+ display: flex;
+ border-radius: $BORDER_RADIUS;
+ width: 100%;
+
+ &--fill {
+ width: 100%;
+
+ & > .dc-input-field {
+ width: 100%;
+ }
+ }
+ & > .dc-input-field {
+ margin: 0;
+ width: 100%;
+
+ @include desktop {
+ max-width: 17.6rem;
+ }
+
+ @include mobile {
+ .inline-icon {
+ top: 1.2rem;
+ }
+ }
+
+ @include colorIcon(var(--text-prominent));
+
+ & .input {
+ height: 3.2rem;
+ background-color: var(--fill-normal);
+ border: 1px solid var(--border-normal);
+ appearance: none;
+
+ @include mobile {
+ height: 4rem;
+ text-align: left;
+ padding-left: 3rem;
+ }
+
+ &:hover {
+ border-color: var(--border-hover);
+ }
+ &:focus,
+ &:active {
+ border-color: var(--border-active);
+ }
+ &::placeholder {
+ color: var(--text-general);
+ }
+ }
+ }
+ & > .dc-input-field:not(:first-child) {
+ margin-left: 8px;
+ }
+ }
+ & > .first-month,
+ & > .second-month {
+ .dc-calendar__body {
+ border-bottom: none;
+ }
+ }
+ &__prepopulated-list {
+ padding-top: 50px;
+ @include typeface(--paragraph-center-normal-black);
+ color: var(--text-prominent);
+ background: var(--state-normal);
+
+ &--is-active {
+ color: var(--text-prominent);
+ background-color: var(--state-active);
+ font-weight: bold;
+ }
+ & li {
+ cursor: pointer;
+ padding: 6px 6px 6px 16px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+
+ &:hover:not(.composite-calendar__prepopulated-list--is-active) {
+ background: var(--state-hover);
+ }
+ }
+ }
+}
+
+/* @define composite-calendar-modal; weak; */
+.composite-calendar-modal {
+ @include mobile {
+ &__actions {
+ display: flex;
+ padding: 16px;
+ border-top: 2px solid var(--border-disabled);
+
+ > * {
+ flex: 1;
+ margin: 8px;
+ }
+ &-today {
+ width: 100%;
+ }
+ }
+ &__radio-group {
+ padding: 16px 16px 24px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ border-bottom: 2px solid var(--border-disabled);
+ }
+ &__radio {
+ display: flex;
+ align-items: center;
+ padding: 7px 8px;
+ border: 1px solid;
+ border-color: var(--border-normal);
+ border-radius: 4px;
+ margin: 8px;
+ font-size: 1.4rem;
+
+ &-input {
+ display: none;
+ }
+ &-circle {
+ border: 2px solid var(--text-general);
+ border-radius: 50%;
+ box-shadow: 0 0 1px 0 var(--shadow-menu);
+ width: 16px;
+ height: 16px;
+ transition: all 0.3s ease-in-out;
+ margin-right: 8px;
+ align-self: center;
+
+ &--selected {
+ border-width: 4px;
+ border-color: var(--brand-red-coral);
+ background: $color-white;
+ }
+ }
+ &--selected {
+ border-color: var(--brand-secondary);
+ font-weight: bold;
+ }
+ }
+ &__custom {
+ padding: 16px;
+
+ &-radio {
+ display: inline-flex;
+ }
+ &-date-range {
+ margin: 8px;
+ display: flex;
+ flex-direction: column;
+
+ &-start-date {
+ margin: 16px 0px;
+ }
+ &-end-date {
+ margin-bottom: 8px;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss b/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss
new file mode 100644
index 000000000000..c1b39d42b87b
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss
@@ -0,0 +1,38 @@
+/** @define .market-symbol-icon; weak */
+.market-symbol-icon {
+ display: flex;
+ justify-content: flex-start;
+ width: 100%;
+
+ .color1-fill {
+ fill: var(--brand-red-coral);
+ }
+ .color2-fill {
+ fill: var(--brand-secondary);
+ }
+ &-name,
+ &-category {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ @include typeface(--paragraph-left-bold-active);
+ color: var(--text-prominent);
+ }
+ &-name {
+ width: 3.2rem;
+ margin-right: 0.8rem;
+ }
+ &-category {
+ svg {
+ width: 2.4rem;
+ height: 2.4rem;
+ }
+ }
+ &__multiplier {
+ color: var(--text-less-prominent);
+ font-size: 1rem;
+ display: flex;
+ align-items: flex-end;
+ margin: 0 0 0.4rem 0.4rem;
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/components/message-box.scss b/packages/reports/src/sass/app/_common/components/message-box.scss
new file mode 100644
index 000000000000..fc66f8215952
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/components/message-box.scss
@@ -0,0 +1,103 @@
+/** @define message-box */
+.message-box {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ z-index: 2;
+ border-radius: $BORDER_RADIUS;
+ display: flex;
+ align-items: center;
+ background-color: var(--general-main-2);
+ color: var(--text-prominent);
+
+ &__close-btn {
+ position: absolute;
+ cursor: pointer;
+ right: 2px;
+ top: 2px;
+
+ &-ic {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ &__result {
+ padding: 16px;
+ line-height: 1.5;
+ font-size: 0.8em;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ max-height: 187px;
+ height: 100%;
+ width: 100%;
+ color: var(--text-prominent);
+
+ &-header {
+ margin-bottom: 0.5rem;
+ font-size: 12px;
+ }
+ &-label {
+ margin-right: 4px;
+ font-size: 10px;
+ color: var(--text-prominent);
+ }
+ &-currency {
+ position: relative;
+ display: inline-flex;
+ }
+ }
+ &__login {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ color: var(--text-prominent);
+
+ &-btn {
+ margin: 0 auto;
+
+ &-span {
+ font-size: 0.8em;
+ }
+ }
+ &-info {
+ font-weight: 300;
+ font-size: 1.2em;
+ }
+ &-prompt {
+ line-height: 100%;
+ margin-bottom: 0;
+ font-size: 0.8em;
+ font-weight: 300;
+ }
+ &-link {
+ text-decoration: none;
+ color: var(--text-prominent);
+
+ &-info {
+ padding: 5px 10px 10px;
+ font-weight: 500;
+ color: var(--status-info);
+ }
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: underline;
+ color: var(--text-prominent);
+ }
+ }
+ }
+ &__info {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.5rem 1.5rem;
+ text-align: center;
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/drawer/positions-drawer.scss b/packages/reports/src/sass/app/_common/drawer/positions-drawer.scss
new file mode 100644
index 000000000000..eedc16f387e0
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/drawer/positions-drawer.scss
@@ -0,0 +1,124 @@
+$header-height: 3.6em;
+
+// Trade page animation performance fix #perfmatters
+.trade-container + .positions-drawer {
+ transition: opacity 0.4s ease;
+}
+
+.positions-drawer {
+ $MARGIN_TOP: #{$POSITIONS_DRAWER_MARGIN * 2};
+ $MARGIN_BOTTOM: #{$POSITIONS_DRAWER_MARGIN * 2};
+
+ width: $POSITIONS_DRAWER_WIDTH;
+ height: calc(100vh - #{$HEADER_HEIGHT} - #{$FOOTER_HEIGHT} - #{$MARGIN_TOP} - #{$MARGIN_BOTTOM});
+ margin-top: #{$MARGIN_TOP};
+ position: fixed;
+ z-index: 2;
+ top: #{$HEADER_HEIGHT};
+ left: 4px;
+ box-sizing: border-box;
+ opacity: 0;
+ transform: translateX(-100%);
+ will-change: transform, opacity;
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ border-radius: $BORDER_RADIUS;
+ border: 1px solid var(--general-section-1);
+ background-color: var(--general-section-1);
+ color: var(--text-prominent);
+
+ &__bg {
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ left: 0;
+ width: 260px;
+ height: 100%;
+ background: var(--general-main-1);
+ // box-shadow: 10px 0 5px -2px var(--general-main-1);
+ transition: opacity 0.25s linear;
+ opacity: 0;
+ pointer-events: none;
+
+ &--open {
+ opacity: 1;
+ }
+ }
+ &--open {
+ transform: translateX(#{$POSITIONS_DRAWER_MARGIN});
+ opacity: 1;
+ }
+ &__header {
+ height: $header-height;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 0 1em;
+
+ &:after {
+ content: '';
+ position: absolute;
+ height: 8px;
+ width: calc(100% - 18px);
+ left: 9px;
+ top: 39px;
+ z-index: 1;
+ box-shadow: 0 8px 2px -2px var(--general-section-1) inset;
+ }
+ }
+ &__body {
+ height: calc(100% - #{$header-height * 2.7});
+ padding: 0.8em 0 0;
+ box-sizing: border-box;
+ overflow: hidden;
+ align-self: center;
+ color: var(--text-general);
+
+ & .dc-themed-scrollbars {
+ height: 100%;
+ }
+ & .dc-contract-card {
+ &__stop-loss {
+ & .dc-contract-card-item {
+ &__header {
+ padding-top: 0.8rem;
+ }
+ }
+ }
+ }
+ }
+ &__footer {
+ position: relative;
+ height: 6em;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:before {
+ content: '';
+ position: absolute;
+ height: 8px;
+ width: calc(100% - 18px);
+ left: 9px;
+ top: -6px;
+ box-shadow: 0 8px 2px -2px var(--general-section-1) inset;
+ }
+ .dc-btn {
+ width: 100%;
+ margin: 8px;
+ height: 40px;
+ }
+ }
+ &__icon-main {
+ margin-right: 0.8em;
+ }
+ &__icon-close {
+ display: inline-block;
+ margin-left: auto;
+ cursor: pointer;
+ svg {
+ @extend %inline-icon;
+ height: 1.6em;
+ width: 1.6em;
+ }
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/layout/trader-layouts.scss b/packages/reports/src/sass/app/_common/layout/trader-layouts.scss
new file mode 100644
index 000000000000..4e86770dac1c
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/layout/trader-layouts.scss
@@ -0,0 +1,797 @@
+/** @define app-contents; weak */
+.app-contents {
+ &--show-positions-drawer:not(&--is-mobile) {
+ .trade-container {
+ .chart-container {
+ width: 100%;
+
+ .sc-navigation-widget,
+ .cq-top-ui-widgets,
+ .sc-toolbar-widget,
+ .stx-panel-control {
+ transform: translate3d(248px, 0, 0);
+ }
+ .cq-chart-controls {
+ transform: translate3d(130px, 0, 0) !important;
+ }
+ .cq-bottom-ui-widgets {
+ .digits__container {
+ transform: translate3d(130px, 0, 0) !important;
+ }
+ }
+ .cq-chart-control-left {
+ .cq-chart-controls {
+ transform: translate3d(248px, 0, 0) !important;
+ }
+ .cq-bottom-ui-widgets {
+ .digits__container {
+ transform: translate3d(170px, 40px, 0) !important;
+ }
+ }
+ }
+ &__loader {
+ .barspinner {
+ transform: translate3d(130px, 0, 0) !important;
+ }
+ }
+ }
+ }
+ }
+ &--is-mobile {
+ .top-widgets-portal {
+ position: absolute;
+ top: 0px;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ z-index: 1;
+
+ .recent-trade-info {
+ min-width: 8rem;
+ line-height: 2.4rem;
+ margin-left: 0.8rem;
+ }
+ }
+ .cq-chart-title {
+ > .cq-menu-btn {
+ padding: 0.4rem;
+ user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+ }
+ & .contract-details-wrapper + .smartcharts-undefined {
+ & .cq-symbols-display {
+ display: none;
+ }
+ }
+ }
+}
+
+$FLOATING_HEADER_HEIGHT: 41px;
+/** @define trade-container; weak */
+.trade-container {
+ position: relative;
+ padding: 0.8em 1.2em;
+ display: flex;
+ min-height: calc(100vh - 84px);
+ overflow: hidden;
+
+ &__replay {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ height: calc(100vh - 108px - #{$FLOATING_HEADER_HEIGHT + 12px});
+ padding-bottom: 2.4rem;
+
+ .contract-drawer {
+ /* prettier-ignore */
+ height: calc(100% + 2.4rem);
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ z-index: 1;
+ overflow: hidden;
+ min-width: 240px;
+
+ &-wrapper {
+ z-index: 4;
+ }
+
+ .dc-contract-card {
+ margin: 0.8rem 0;
+ &__sell-button {
+ &--exit {
+ display: none;
+ }
+ }
+ }
+
+ @include mobile {
+ z-index: 4;
+ height: auto;
+ border-bottom-right-radius: $BORDER_RADIUS;
+ border-top-right-radius: $BORDER_RADIUS;
+ width: calc(100% - 1.6rem);
+ margin-left: 0.8rem;
+ transition: none;
+
+ &__mobile-wrapper {
+ position: relative;
+ }
+ &--with-collapsible-btn {
+ overflow: visible;
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ & .dc-contract-card {
+ margin-top: 0;
+ }
+ }
+ &__transition {
+ &-enter,
+ &-exit {
+ transition: transform 0.25s linear;
+ }
+ }
+ & .dc-contract-card {
+ &__grid-underlying-trade {
+ grid-template-columns: 2fr 1fr !important;
+ }
+ &__underlying-name {
+ max-width: none;
+ }
+ }
+ &--is-multiplier {
+ & .dc-contract-card {
+ &__body-wrapper {
+ flex-direction: column;
+ padding-top: 0.4rem;
+ }
+ &-items-wrapper {
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-gap: 0.4rem;
+ padding-bottom: 0.4rem;
+ border-bottom: 1px solid var(--general-section-1);
+ min-height: 80px;
+
+ @media (max-width: 320px) and (max-height: 480px) {
+ min-height: unset;
+ }
+ }
+ &-item {
+ &__total-profit-loss {
+ flex-direction: row;
+ margin: 0 auto;
+
+ & .contract-card-item__header {
+ font-size: 1.2rem !important;
+
+ @media (max-width: 320px) and (max-height: 480px) {
+ font-size: 1rem !important;
+ }
+ }
+ }
+ &__header,
+ &__body {
+ font-size: 1.2rem;
+ }
+ &__body--loss {
+ padding-left: 0.4rem;
+ }
+ &:nth-child(1) {
+ order: 0;
+ }
+ &:nth-child(3),
+ &:nth-child(5) {
+ order: 2;
+ }
+ &:nth-child(6) {
+ order: 6;
+ }
+ @media only screen and (max-width: 320px) {
+ &__header {
+ font-size: 1rem;
+ }
+ }
+ @media (max-width: 320px) and (max-height: 480px) {
+ &__body {
+ font-size: 1rem;
+ }
+ }
+ }
+ @media (max-width: 320px) and (max-height: 480px) {
+ &__symbol,
+ &__type {
+ font-size: 1rem;
+ }
+ &__sell-button {
+ padding-top: 0.4rem;
+
+ & .dc-btn {
+ height: 2.6rem;
+
+ &__text {
+ font-size: 1rem;
+ }
+ }
+ }
+ &__footer {
+ margin-bottom: 0;
+ }
+ }
+
+ &__sell-button {
+ &--has-cancel-btn {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+ @media (max-width: 320px) and (max-height: 480px) {
+ & .dc-btn--cancel {
+ & .dc-btn__text {
+ align-items: center;
+ }
+ & .dc-remaining-time {
+ font-size: 1rem;
+ padding-top: 0;
+ }
+ }
+ }
+ }
+ }
+ &.contract-drawer--with-collapsible-btn {
+ & .dc-contract-card {
+ &__indicative--movement {
+ margin-top: 2px;
+ }
+ }
+ }
+ & .dc-contract-card__grid-underlying-trade,
+ & .dc-contract-card__footer-wrapper {
+ grid-template-columns: 1fr 1fr 0.7fr;
+
+ @media only screen and (max-height: 480px) {
+ grid-template-columns: 1fr 1fr;
+ }
+ }
+ @media only screen and (max-height: 480px) {
+ & .dc-contract-card__body-wrapper {
+ padding-top: 0.2rem;
+ }
+ }
+
+ &-sold {
+ &.contract-drawer--with-collapsible-btn {
+ & .dc-contract-card-item {
+ &__total-profit-loss {
+ flex-direction: column;
+ }
+ &__body--profit {
+ font-size: 1.6rem;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ .replay-chart__container {
+ width: 100%;
+ position: relative;
+ margin-left: 24px;
+
+ .smartcharts {
+ left: 0;
+ border-radius: $BORDER_RADIUS;
+
+ .ciq-chart {
+ .cq-top-ui-widgets,
+ & .info-box {
+ transition: transform 0.25s ease;
+
+ .cq-symbols-display {
+ z-index: 1;
+
+ &.ciq-disabled {
+ display: none;
+ }
+ }
+ .info-box-container {
+ transform: none;
+ opacity: 1;
+ left: 1px;
+
+ .chart-close-btn {
+ display: none;
+ }
+ }
+ }
+ .sc-toolbar-widget,
+ .stx-panel-control,
+ .sc-navigation-widget {
+ transition: transform 0.25s ease;
+ }
+ .ciq-asset-information {
+ top: 75px;
+ }
+ .stx_jump_today.home > svg {
+ top: 10px;
+ left: 8px;
+ padding: 0;
+ position: absolute;
+ }
+ .cq-bottom-ui-widgets {
+ bottom: 30px !important;
+
+ .digits {
+ margin-right: 0;
+
+ &__container {
+ transition: transform 0.25s ease;
+ }
+ }
+ }
+ }
+
+ /* postcss-bem-linter: ignore */
+ &-mobile {
+ /* TODO: Remove this override once the issue is fixed in smartcharts */
+ .stx-holder.stx-panel-chart {
+ z-index: 14;
+
+ .cq-inchart-holder {
+ z-index: 107;
+ position: relative;
+ }
+ }
+
+ .cq-context {
+ height: 100%;
+ }
+ }
+ }
+
+ &-swipeable-wrapper {
+ .dc-swipeable__item {
+ margin-left: 0.8rem;
+ width: calc(100vw - 1.6rem);
+ }
+ }
+
+ @include mobile {
+ height: 100%;
+ width: calc(100% - 1.6rem);
+ margin-left: 0.8rem;
+ }
+ }
+ @include mobile {
+ display: flex;
+ flex-direction: column-reverse;
+ height: 100%;
+ padding-bottom: 0;
+ position: relative;
+
+ #dt_contract_drawer_audit {
+ flex: 1;
+ overflow: auto;
+ }
+
+ & .contract-audit-card {
+ height: calc(100% - 1rem);
+ &__container {
+ height: 100%;
+ }
+ }
+ }
+ }
+ @include mobile {
+ flex-direction: column;
+ min-height: calc(100vh - 48px);
+ padding: 0;
+ }
+}
+
+/** @define mobile-wrapper; weak */
+.mobile-wrapper {
+ padding: 0 0.8rem;
+ display: flex;
+ flex-direction: column;
+ height: 212px;
+ position: relative;
+
+ &__content-loader {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ bottom: -0.8rem;
+
+ svg {
+ height: 100%;
+ width: 100%;
+ }
+ }
+}
+
+/** @define chart-container; weak */
+.chart-container {
+ width: 100%;
+ position: relative;
+
+ &__wrapper {
+ position: fixed;
+ top: calc(#{$HEADER_HEIGHT} + 2px);
+ // charts width is 100% - sidebar width - sidebar margin - .trade-container padding
+ width: calc(100% - 240px - 1.6rem - 2.4rem);
+ height: calc(100vh - #{$HEADER_HEIGHT} - #{$FOOTER_HEIGHT});
+ }
+ &__loader {
+ position: absolute;
+ height: calc(100% - 68px);
+ width: calc(100% - 12px);
+ top: 54px;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: $BORDER_RADIUS;
+ background-color: var(--general-main-1);
+
+ .initial-loader {
+ pointer-events: none;
+ }
+ .barspinner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 0;
+ width: 100%;
+ height: 18px;
+ }
+ & + .smartcharts {
+ visibility: hidden;
+ pointer-events: none;
+
+ .chart-marker-line__wrapper,
+ .cq-chart-controls,
+ .cq-symbols-display,
+ .cq-bottom-ui-widgets,
+ .cq-inchart-subholder {
+ display: none;
+ }
+ }
+ }
+ // smartchart library style fixes
+ .smartcharts-mobile {
+ .sc-categorical-display {
+ height: calc(100% - 8px) !important;
+ }
+ .ciq-chart {
+ padding: 0 0.8rem;
+ }
+ }
+ .cq-context {
+ top: 0;
+ left: 0;
+ z-index: 0;
+
+ div.ciq-chart {
+ height: 100%;
+ box-shadow: none;
+
+ div.cq-last-digits {
+ bottom: 15px;
+ left: calc(45% - 150px);
+ }
+ .info-box div.cq-chart-controls {
+ box-shadow: none;
+ }
+ // TODO: enable asset information
+ // div.ciq-asset-information {
+ // z-index: 0;
+ // top: 0;
+ // left: 0;
+ // }
+ div.stx_jump_today.home > svg {
+ top: 10px;
+ left: 8px;
+ padding: 0;
+ position: absolute;
+ }
+ div.stx-marker {
+ z-index: 2;
+
+ &:not(.chart-marker-line) {
+ animation: fadeIn 0.2s;
+ }
+ }
+ }
+ div.cq-chart-control-left {
+ .cq-top-ui-widgets {
+ width: calc(100% - 9em);
+ }
+ }
+ }
+ div.debug-text {
+ display: none;
+ }
+ .cq-chart-control-left {
+ .cq-chart-controls,
+ .sc-toolbar-widget,
+ .stx-panel-control,
+ .sc-navigation-widget {
+ transform: translate3d(0, 0, 0);
+ transition: transform 0.25s ease;
+ }
+ .cq-top-ui-widgets {
+ left: 9em;
+
+ .info-box {
+ transform: translate3d(0, 0, 0);
+ }
+ }
+ }
+ .ciq-chart {
+ .cq-top-ui-widgets,
+ & .info-box {
+ transition: transform 0.25s ease;
+
+ .cq-symbols-display {
+ z-index: 1;
+
+ &.ciq-disabled {
+ display: none;
+ }
+ @include mobile {
+ top: 0.8rem;
+ left: 0.8rem;
+ min-width: 170px;
+ max-width: 260px;
+ width: auto;
+
+ .cq-menu-btn {
+ padding: 0.2rem;
+ }
+ .cq-symbol-select-btn {
+ padding: 0.3rem 0.9rem;
+
+ .cq-symbol-dropdown {
+ transform: scale(1);
+ margin-left: auto;
+ }
+ .cq-symbol {
+ font-size: 1.2rem;
+ }
+ .cq-chart-price {
+ display: none;
+ }
+ }
+ }
+ }
+ }
+ .cq-chart-controls {
+ transition: max-width 0.25s ease, transform 0.25s ease;
+ }
+ .sc-navigation-widget,
+ .stx-panel-control {
+ transition: transform 0.25s ease;
+ }
+ .sc-toolbar-widget {
+ transition: transform 0.25s ease;
+
+ @include mobile {
+ background: transparent;
+ border-width: 0;
+ bottom: 2.8rem;
+
+ /* postcss-bem-linter: ignore */
+ .sc-chart-mode,
+ .sc-studies {
+ background: var(--general-section-1);
+ padding: 0.4rem 0.2rem;
+ width: 4rem;
+ height: 4rem;
+ display: flex;
+ border-radius: 50%;
+ justify-content: center;
+ align-items: center;
+ margin: 0.8rem;
+ opacity: 0.75;
+
+ &__menu {
+ &__timeperiod {
+ top: 0.8rem;
+ left: 0.8rem;
+ }
+ & > .ic-icon {
+ top: 0.6rem;
+ }
+ }
+ }
+ }
+ }
+ &--screenshot {
+ .sc-toolbar-widget,
+ .stx-panel-control,
+ .sc-navigation-widget,
+ .cq-top-ui-widgets {
+ transform: translate3d(0, 0, 0) !important;
+ }
+ }
+ }
+ .chartContainer {
+ background: transparent;
+ min-height: 100%;
+ }
+}
+
+/** @define sidebar; weak; */
+.sidebar {
+ &__container {
+ position: relative;
+ margin: 0.8rem 0 0.8rem 1.6rem;
+ width: $SIDEBAR_WIDTH;
+ z-index: 5;
+ }
+ &__items {
+ opacity: 1;
+ transform: none;
+ position: relative;
+ min-height: 460px;
+ width: $SIDEBAR_WIDTH;
+
+ &:after {
+ transition: opacity 0.25s cubic-bezier(0.25, 0.1, 0.1, 0.25);
+ opacity: 0;
+ position: absolute;
+ pointer-events: none;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 999;
+ content: '';
+ background-color: var(--overlay-outside-dialog);
+ }
+ &--market-closed {
+ & .dc-tooltip--with-label {
+ display: none;
+ }
+ & .dc-tooltip--with-label:before,
+ .dc-tooltip--with-label:after {
+ display: none;
+ }
+ }
+ }
+}
+
+// TODO: improve handling of rendering markets dropdown via portals from smartcharts library
+/** @define smartcharts-portal; weak */
+.smartcharts-portal {
+ @include mobile {
+ &--open {
+ .smartcharts {
+ z-index: 9999;
+ }
+ }
+ }
+}
+
+/** @define contract; weak */
+.contract {
+ // TODO: Remove below if redundant
+ &-update {
+ /* postcss-bem-linter: ignore */
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+
+ /* postcss-bem-linter: ignore */
+ & .dc-tooltip:before,
+ & .dc-tooltip:after {
+ display: none;
+ }
+ /* postcss-bem-linter: ignore */
+ & .dc-contract-card-dialog__button {
+ display: flex;
+ align-items: flex-end;
+ }
+ }
+ }
+ &--enter {
+ transform: translate3d(calc(100% + 1.6em), 0, 0);
+ opacity: 0;
+ }
+ &--exit {
+ transform: translate3d(calc(100% + 1.6em), 0, 0);
+ opacity: 0;
+ pointer-events: none;
+ }
+}
+
+/** @define smartcharts; weak */
+/* postcss-bem-linter: ignore */
+.smartcharts {
+ &-dark,
+ &-light {
+ @include mobile {
+ /* postcss-bem-linter: ignore */
+ .cq-menu-dropdown-enter-done {
+ margin-top: 0;
+
+ /* postcss-bem-linter: ignore */
+ .icon-close-menu {
+ opacity: 1;
+ pointer-events: auto;
+ top: 8px;
+ }
+ }
+ .cq-dialog-portal {
+ /* postcss-bem-linter: ignore */
+ .cq-dialog {
+ max-width: calc(100% - 36px);
+ }
+ }
+ /** @define ciq-chart-type; weak */
+ .sc-chart-type {
+ &__item {
+ /* postcss-bem-linter: ignore */
+ .sc-tooltip,
+ .dc-tooltip {
+ display: none;
+ }
+ }
+ }
+ /** @define ciq-chart-mode; weak */
+ .sc-chart-mode {
+ /* postcss-bem-linter: ignore */
+ &__section__item {
+ /* postcss-bem-linter: ignore */
+ .sc-interval {
+ display: grid;
+ padding: 1.6rem;
+ grid-template-columns: 1fr;
+
+ /* postcss-bem-linter: ignore */
+ &__content {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ padding-top: 16px;
+ }
+ /* postcss-bem-linter: ignore */
+ &__item {
+ width: 100% !important;
+ margin: 0;
+
+ /* postcss-bem-linter: ignore */
+ .sc-tooltip,
+ .dc-tooltip {
+ display: none;
+ }
+ }
+ /* postcss-bem-linter: ignore */
+ &__info {
+ margin-top: 0.4rem;
+ padding-left: 0.2rem;
+ }
+ }
+ }
+ }
+ /** @define cq-top-ui-widgets; weak */
+ .cq-top-ui-widgets {
+ z-index: 15 !important;
+ }
+ }
+ }
+ /* TODO: Remove this override once the issue is fixed in smartcharts */
+ /* postcss-bem-linter: ignore */
+ @at-root body.theme--light & .chart-line.horizontal .title-wrapper {
+ background-image: linear-gradient(
+ rgba(255, 255, 255, 0.001) 30%,
+ var(--general-main-1) 50%,
+ rgba(255, 255, 255, 0.001) 75%
+ );
+ }
+}
diff --git a/packages/reports/src/sass/app/_common/mobile-widget.scss b/packages/reports/src/sass/app/_common/mobile-widget.scss
new file mode 100644
index 000000000000..b9896a97cd8a
--- /dev/null
+++ b/packages/reports/src/sass/app/_common/mobile-widget.scss
@@ -0,0 +1,159 @@
+/** @define mobile-widget */
+.mobile-widget {
+ border-radius: 4px;
+ padding: 1rem 0.8rem;
+ height: 4rem;
+ background-color: var(--general-main-1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 0 0 0.8rem;
+ flex: 1;
+
+ &__amount {
+ @include typeface(--paragraph-center-bold-black);
+ color: var(--text-prominent);
+ }
+ &__duration {
+ @include typeface(--paragraph-center-normal-black);
+ color: var(--text-prominent);
+ }
+ &__type {
+ @include typeface(--paragraph-center-normal-black);
+ color: var(--text-less-prominent);
+ }
+ &__item {
+ color: var(--text-prominent);
+ line-height: 1.4rem;
+
+ &-value {
+ font-weight: bold;
+ font-size: 1.2rem;
+ }
+ }
+ &__multiplier {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 0;
+
+ &-amount {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0.4rem 0.8rem 0;
+
+ .mobile-widget__item-label {
+ margin-right: 0.4rem;
+ }
+ }
+ &-expiration {
+ min-width: 7.2rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ &-options {
+ flex: none;
+ flex-direction: column;
+ padding: 0.6rem 0.8rem;
+ margin-bottom: 0.6rem;
+ margin-left: 0.8rem;
+ justify-content: center;
+ min-width: 8.8rem;
+
+ .mobile-widget__item-label {
+ color: var(--text-general);
+ }
+ }
+ &-risk-management {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: stretch;
+ margin-bottom: 0.6rem;
+ color: var(--text-general);
+ padding: 0.4rem 0.8rem;
+
+ .mobile-widget__item {
+ flex: 1;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .mobile-widget__item-label {
+ color: var(--text-general);
+ }
+ .mobile-widget__item-label,
+ .mobile-widget__item-value {
+ font-size: 1.4rem;
+ line-height: 1.8rem;
+ }
+ }
+ &-trade-info {
+ display: flex;
+ justify-content: space-evenly;
+ background: var(--general-main-1);
+ flex: 2;
+ margin-left: 0.8rem;
+
+ &--no-stop-out {
+ justify-content: flex-end;
+ margin-right: 1.6rem;
+ }
+ &-tooltip-text {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+ span:before {
+ content: ': ';
+ }
+ &:first-child {
+ margin-right: 0.8rem;
+ }
+ @media only screen and (max-width: 340px) {
+ font-size: 0.8rem;
+ }
+ }
+ }
+ }
+ &__wrapper {
+ display: flex;
+
+ .mobile-widget:last-child:not(:only-child) {
+ margin-left: 0.8rem;
+ flex: unset;
+ }
+ }
+}
+
+/** @define fieldset-minimized */
+.fieldset-minimized {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 0.5em;
+ white-space: pre-line;
+
+ &__amount {
+ grid-area: c;
+ }
+ &__barrier1 {
+ grid-area: b;
+ }
+ &__barrier2 {
+ grid-area: d;
+ }
+ &__currency:before {
+ margin-right: 0.1em;
+ position: static;
+ display: inline;
+ font-size: 1em;
+ }
+ &__basis {
+ font-weight: bold;
+ color: var(--text-prominent);
+ }
+}
diff --git a/packages/reports/src/sass/app/modules/contract.scss b/packages/reports/src/sass/app/modules/contract.scss
new file mode 100644
index 000000000000..c67dfeb6a995
--- /dev/null
+++ b/packages/reports/src/sass/app/modules/contract.scss
@@ -0,0 +1,165 @@
+$CONTRACT_INFO_BOX_PADDING: 1.6em;
+
+.info-box-container {
+ position: absolute;
+ z-index: 3;
+ top: 1em;
+ right: 1em;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+
+ &-button:hover {
+ cursor: pointer;
+ }
+ &-icon {
+ width: 32px;
+ height: 32px;
+ }
+ .info-box {
+ position: relative;
+ border-radius: $BORDER_RADIUS;
+ padding: $CONTRACT_INFO_BOX_PADDING;
+ background: var(--general-section-1);
+ font-weight: 300;
+
+ &-longcode {
+ display: flex;
+
+ &-icon {
+ @extend %inline-icon;
+ margin-right: 1.6rem;
+ padding: 0.4rem;
+
+ @include mobile {
+ margin-right: 0.8rem;
+ }
+ }
+ &-text {
+ max-width: 360px;
+
+ @include mobile {
+ max-width: 175px;
+ line-height: 1.4;
+ letter-spacing: normal;
+ font-size: 1rem;
+ /* iPhone SE screen height fixes due to UI space restrictions */
+ @media only screen and (max-height: 480px) {
+ font-size: 0.8rem;
+ }
+ }
+ }
+ }
+ .expired {
+ display: flex;
+ align-items: center;
+
+ svg {
+ width: 2.4em;
+ height: 2.4em;
+ margin-right: 1em;
+
+ .color1-fill {
+ fill: var(--status-success);
+ }
+ }
+ .pl-value {
+ color: var(--text-profit-success);
+ font-weight: bold;
+ font-size: 1.6em;
+ line-height: 1.5em;
+
+ .percentage {
+ display: inline-block;
+ margin-left: 0.7em;
+ }
+ }
+ &.lost {
+ .pl-value {
+ color: var(--text-loss-danger);
+ }
+ svg .color1-fill {
+ fill: var(--status-danger);
+ }
+ }
+ .sell-info {
+ margin-right: 2em;
+ text-align: center;
+ line-height: 1.2em;
+ }
+ }
+ .general {
+ display: flex;
+ align-items: center;
+ line-height: 1.4em;
+
+ .values {
+ margin-left: 1em;
+ margin-right: 2em;
+ text-align: right;
+ font-weight: bold;
+
+ .profit {
+ color: var(--text-profit-success);
+ }
+ .loss {
+ color: var(--text-loss-danger);
+ }
+ }
+ .sell {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ margin: -$CONTRACT_INFO_BOX_PADDING;
+ margin-left: $CONTRACT_INFO_BOX_PADDING;
+ padding: $CONTRACT_INFO_BOX_PADDING;
+
+ .dc-tooltip {
+ position: absolute;
+ bottom: 0.5em;
+ right: 0.5em;
+ line-height: 0;
+
+ &:before {
+ width: 350px;
+ white-space: normal;
+ }
+ }
+ }
+ }
+ @include mobile {
+ padding: 0.8rem;
+ margin-left: 0.8rem;
+ }
+ }
+ .message {
+ margin-top: 0.5em;
+ border: 1px solid var(--brand-red-coral);
+ border-radius: $BORDER_RADIUS;
+ padding: 1em;
+ background-color: transparentize($color-black, 0.84);
+ display: flex;
+ align-items: center;
+
+ .message-icon {
+ margin-right: 1em;
+ }
+ .message-text {
+ flex-grow: 1;
+ }
+ .message-close {
+ cursor: pointer;
+ }
+ }
+ .chart-close-btn {
+ position: absolute;
+ cursor: pointer;
+ z-index: 11;
+ right: 0;
+ top: 0;
+ }
+ @include mobile {
+ left: 0;
+ }
+}
diff --git a/packages/reports/src/sass/app/modules/contract/digits.scss b/packages/reports/src/sass/app/modules/contract/digits.scss
new file mode 100644
index 000000000000..76dc2ae097d4
--- /dev/null
+++ b/packages/reports/src/sass/app/modules/contract/digits.scss
@@ -0,0 +1,464 @@
+/** @define cq-bottom-ui-widgets; weak */
+.cq-bottom-ui-widgets {
+ z-index: 4;
+ overflow: visible;
+ height: 0;
+ top: unset !important;
+ bottom: 80px;
+
+ .bottom-widgets {
+ left: -3.5em;
+ }
+}
+
+/** @define digits; weak */
+.digits {
+ $self: &;
+ display: flex;
+ align-items: center;
+ position: relative;
+ margin: 0 2.5em 0 1em;
+
+ &__container {
+ position: relative;
+
+ /* Screen height fixes due to UI space restrictions */
+ @media only screen and (max-height: 520px) {
+ transform: scale(0.85);
+ transform-origin: bottom;
+ padding: 0 0.8rem !important;
+ }
+ @media only screen and (max-height: 480px) {
+ transform: scale(0.75);
+ }
+ }
+ &__tooltip-container {
+ position: absolute;
+ z-index: 2;
+ top: -10px;
+ right: 10px;
+ }
+ &__digit {
+ pointer-events: none;
+ margin: 0 0.6em;
+ text-align: center;
+ position: relative;
+ transition: transform 0.25s linear;
+
+ &--latest {
+ z-index: 1;
+ transform: scale(1.2);
+
+ .digits__digit-spot {
+ font-size: 0.9em;
+ }
+ }
+ &--win {
+ z-index: 1;
+ transform: scale(1.25);
+
+ .digits__pie-container:before {
+ box-shadow: 0px 1px 18px var(--text-profit-success);
+ }
+ }
+ &--loss {
+ z-index: 1;
+ transform: scale(1.25);
+
+ .digits__pie-container:before {
+ top: -2px;
+ left: -2px;
+ box-shadow: 0px 1px 18px var(--text-loss-danger);
+ border: 3px solid var(--text-loss-danger);
+
+ @include mobile {
+ top: -1px;
+ left: -3px;
+ }
+ }
+ }
+ &--is-selected {
+ .digits__digit-display {
+ &-value,
+ &-percentage {
+ color: $color-white;
+ }
+ }
+ .progress__bg {
+ fill: $color-blue-2;
+ }
+ }
+ &--is-selectable {
+ pointer-events: auto;
+
+ &:focus,
+ &:active,
+ &:hover {
+ .digits__digit-display {
+ &-value,
+ &-percentage {
+ color: $color-white;
+ }
+ }
+ .progress__bg {
+ fill: $color-blue-2;
+ }
+ }
+ }
+ &-display-value {
+ transition: transform 0.25s linear;
+ position: absolute;
+ transform: scale(0.9);
+ top: 4px;
+ color: var(--text-prominent);
+
+ &--no-stats {
+ transform: scale(1) translateY(5px);
+ }
+
+ @include mobile {
+ top: 10px;
+ transform: none;
+ font-size: 1.4rem;
+ line-height: 1.43;
+
+ &--no-stats {
+ top: 15px;
+ }
+ }
+ }
+ &-display-percentage {
+ top: 20px;
+ position: absolute;
+ font-size: 0.65em;
+ font-weight: 400;
+ color: var(--text-general);
+
+ @include mobile {
+ top: 25px;
+ transform: none;
+ font-size: 1rem;
+ line-height: 1.4;
+ }
+ }
+ &-value {
+ @include typeface(--paragraph-center-bold-black);
+ width: 40px;
+ height: 40px;
+ background-color: var(--general-main-1);
+ color: var(--text-prominent);
+ margin-bottom: 0.5em;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: background-color 0.2s ease-out, transform 0.1s ease-out;
+
+ &--selected {
+ background-color: var(--general-main-2);
+ }
+ &--win {
+ background-color: var(--text-profit-success);
+ color: var(--text-prominent);
+
+ .digits__digit-display-value,
+ .digits__digit-display-percentage {
+ color: $color-white;
+ }
+ }
+ &--blink {
+ animation-duration: 500ms;
+ animation-iteration-count: 4;
+ animation-timing-function: step-end;
+ animation-name: blinking;
+ }
+ &--loss {
+ border: none !important;
+ background-color: var(--text-loss-danger);
+ color: var(--text-prominent);
+
+ .digits__digit-display-value,
+ .digits__digit-display-percentage {
+ color: $color-white;
+ }
+ }
+ @include mobile {
+ height: 48px;
+ width: 48px;
+ margin: 0;
+ }
+ }
+ &-spot {
+ position: absolute;
+ top: -25px;
+ left: -50%;
+ right: -50%;
+ width: auto;
+ white-space: nowrap;
+
+ &-value {
+ transform: scale(0.95);
+ display: inline-block;
+ }
+ &-last {
+ @include typeface(--paragraph-center-bold-black);
+ padding: 0 4px;
+ margin-left: 1px;
+ border-radius: 50%;
+ border: 1px solid $color-blue;
+ color: var(--text-prominent);
+ background: var(--general-main-2);
+
+ &--selected-win {
+ color: var(--text-profit-success);
+ }
+ &--win {
+ color: var(--text-colored-background);
+ border-color: var(--text-profit-success);
+ background: var(--text-profit-success);
+ }
+ &--loss {
+ color: var(--text-colored-background);
+ border-color: var(--text-loss-danger);
+ background: var(--text-loss-danger);
+ }
+ }
+ @include mobile {
+ display: flex;
+ justify-content: center;
+ top: 1.6rem;
+ left: 0;
+ right: 0;
+ margin-top: 4.8rem;
+ pointer-events: none;
+
+ &-value,
+ &-last {
+ font-size: 2rem;
+ }
+ &-last {
+ padding: 0 8px;
+ }
+ &:not(&--is-trading) {
+ position: relative;
+ top: 0.8rem;
+ }
+ /* iPhone 8 screen height fixes due to UI space restrictions */
+ @media only screen and (max-height: 580px) {
+ &--is-trading {
+ &-value,
+ &-last {
+ font-size: 1.4rem;
+ }
+ &-last {
+ padding: 0 7px;
+ }
+ }
+ }
+ @media only screen and (max-height: 580px) {
+ position: relative;
+
+ &:not(&--is-trading) {
+ top: 1.4rem;
+ }
+ }
+ @media only screen and (max-height: 520px) {
+ position: relative;
+ top: 0;
+ margin-top: 1.6rem;
+
+ &--is-trading {
+ margin-top: auto;
+ }
+ }
+ }
+ }
+ }
+ &__pie-container {
+ position: absolute;
+ top: -1px;
+ left: -1px;
+
+ &:before {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ top: 1px;
+ left: 1px;
+ content: '';
+
+ @include mobile {
+ top: -4px;
+ left: -2px;
+ width: 48px;
+ height: 48px;
+ }
+ }
+ &--selected:before {
+ top: -2px;
+ left: -2px;
+ width: 42px;
+ height: 42px;
+ border: 2px solid var(--text-profit-success);
+
+ @include mobile {
+ top: 0;
+ left: -2px;
+ width: 48px;
+ height: 48px;
+ }
+ }
+ }
+ &__pie-progress {
+ transform: rotate(-90deg);
+ width: 42px;
+ height: 42px;
+
+ /* postcss-bem-linter: ignore */
+ .progress__bg {
+ stroke: var(--general-disabled);
+ }
+ /* postcss-bem-linter: ignore */
+ .progress__value {
+ stroke: var(--text-less-prominent);
+
+ /* postcss-bem-linter: ignore */
+ &--is-max {
+ stroke: var(--text-profit-success);
+ }
+ /* postcss-bem-linter: ignore */
+ &--is-min {
+ stroke: var(--text-loss-danger);
+ }
+ }
+ @include mobile {
+ width: 48px;
+ height: 48px;
+ margin-top: 0.2rem;
+ }
+ }
+ &__pointer {
+ position: absolute;
+ bottom: -12px;
+ padding: 0 12px;
+ transition: transform 0.25s ease;
+
+ @include mobile {
+ left: -2px;
+ }
+ }
+ &__particles {
+ position: absolute;
+ padding: 0 20px;
+ top: 8px;
+ transform: rotate(45deg);
+ opacity: 0;
+
+ &-particle {
+ background: var(--brand-secondary);
+ opacity: 0.7;
+ border-radius: 50%;
+ display: block;
+ width: 5px;
+ height: 5px;
+ position: absolute;
+ transition: transform 0.5s ease, opacity 0.5s linear 0.5s;
+ }
+ &--explode {
+ opacity: 1;
+
+ .digits__particles-particle {
+ opacity: 0;
+
+ &:nth-child(1) {
+ transform: translate(45px, 45px);
+ }
+ &:nth-child(2) {
+ transform: translate(45px, 0px);
+ }
+ &:nth-child(3) {
+ transform: translate(0px, 45px);
+ }
+ &:nth-child(4) {
+ transform: translate(-45px, 45px);
+ }
+ &:nth-child(5) {
+ transform: translate(-45px, -45px);
+ }
+ &:nth-child(6) {
+ transform: translate(-45px, 0px);
+ }
+ &:nth-child(7) {
+ transform: translate(0px, -45px);
+ }
+ &:nth-child(8) {
+ transform: translate(45px, -45px);
+ }
+ }
+ }
+ }
+ &__icon {
+ &-color {
+ fill: var(--brand-orange);
+ }
+ &--win .digits__icon-color {
+ fill: var(--text-profit-success);
+ }
+ &--loss .digits__icon-color {
+ fill: var(--text-loss-danger);
+ }
+ }
+ @include mobile {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ grid-gap: 2.4rem 1.6rem;
+ align-items: center;
+ margin: 1.6rem 0;
+
+ &--trade {
+ grid-gap: 2rem 1.6rem;
+ margin: auto 0;
+ transform: scale(1.1);
+ transform-origin: bottom;
+
+ @media only screen and (max-height: 580px) {
+ transform: unset;
+ transform-origin: unset;
+ }
+ @media only screen and (max-height: 480px) {
+ margin: auto 0 4.8rem;
+ }
+ }
+ &__container {
+ width: 100%;
+ margin: 0;
+ padding: 0.8rem;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+ &__tooltip-text {
+ font-size: 1.2rem;
+ color: var(--text-general);
+ line-height: 18px;
+ text-align: center;
+ }
+ &__digit {
+ margin: auto;
+ }
+ }
+ @include mobile {
+ @at-root .popup-root #{$self}__toast-info {
+ top: 10.5rem;
+ }
+ }
+}
+
+@keyframes blinking {
+ 50% {
+ background-color: var(--general-main-1);
+ color: var(--text-general);
+ }
+}
diff --git a/packages/reports/src/sass/app/modules/portfolio.scss b/packages/reports/src/sass/app/modules/portfolio.scss
new file mode 100644
index 000000000000..e95e64667de9
--- /dev/null
+++ b/packages/reports/src/sass/app/modules/portfolio.scss
@@ -0,0 +1,87 @@
+// TODO: Combine whatever selector / rule that's still needed from this and merge into positions_drawer
+/** @define portfolio; weak */
+.portfolio {
+ padding: 1.5em 1.2em;
+ height: 100%;
+
+ &--card-view {
+ background: var(--general-main-2);
+ }
+ &__table {
+ height: 100%;
+ }
+ &__row {
+ grid-template-columns: 6.5em 7em 1fr 6em 9em 6em 5em;
+ }
+ /* postcss-bem-linter: ignore */
+ .payout,
+ .indicative,
+ .purchase,
+ .remaining_time {
+ justify-content: flex-end;
+ }
+ .container {
+ background: var(--general-section-1);
+ }
+ .contract-type {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+
+ .type-wrapper {
+ width: 2em;
+ height: 2em;
+ padding: 0.5em;
+ margin-bottom: 0.3em;
+ border-radius: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: var(--state-normal);
+ }
+ }
+ .reference a {
+ color: $COLOR_SKY_BLUE;
+ text-decoration: none;
+ }
+ .table__body .indicative {
+ font-weight: bold;
+ }
+ .indicative {
+ text-align: right;
+
+ &--price-moved-up {
+ color: var(--text-profit-success);
+ }
+ &--price-moved-down {
+ color: var(--text-loss-danger);
+ }
+ &--no-resale {
+ color: var(--text-general);
+ }
+ }
+}
+
+// empty portfolio message
+/** @define portfolio-empty */
+.portfolio-empty {
+ height: calc(100vh - 240px);
+ display: flex;
+
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ }
+ &__icon {
+ @extend %inline-icon.disabled;
+ height: 4.8em;
+ width: 4.8em;
+ margin-bottom: 1.6em;
+ }
+}
diff --git a/packages/reports/src/sass/app/modules/reports.scss b/packages/reports/src/sass/app/modules/reports.scss
new file mode 100644
index 000000000000..2745e324883a
--- /dev/null
+++ b/packages/reports/src/sass/app/modules/reports.scss
@@ -0,0 +1,983 @@
+@import '../_common/components/composite-calendar';
+
+$side-padding: 1.2em;
+
+/** @define reports; weak */
+.reports {
+ height: 100%;
+
+ &__meta {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ padding: 0 2.4rem 1.6rem 0;
+
+ @include mobile {
+ padding: 0 1.6rem 1.6rem;
+ }
+
+ flex-direction: column-reverse;
+ padding-bottom: 0;
+
+ &-filter {
+ position: relative;
+ display: flex;
+ width: 100%;
+ @include desktop {
+ max-width: 36rem;
+ margin-left: auto;
+ }
+ &--statement {
+ @include desktop {
+ max-width: 50rem;
+ }
+ }
+ }
+
+ @include desktop {
+ align-items: center;
+ }
+
+ @include mobile {
+ flex-direction: column;
+ padding-bottom: 0;
+
+ &-filter {
+ padding: 0 0 1.6rem;
+ }
+ #dt_calendar_input {
+ text-align: left;
+ padding-left: 3rem;
+ }
+ }
+ }
+ &__mobile-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ &__route-selection {
+ padding: 1.6rem;
+ }
+ &__content {
+ width: 100%;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ @media only screen and (min-width: 1280px) {
+ overflow: visible;
+ }
+ }
+ .unknown-icon {
+ margin-left: 8px;
+ fill: var(--text-general);
+ border-radius: $BORDER_RADIUS;
+ }
+ /* postcss-bem-linter: ignore */
+ .dc-tabs--open-positions {
+ flex: 1;
+ grid-template-rows: 36px calc(100% - 36px);
+ grid-template-columns: 100%;
+
+ .dc-tabs__content {
+ display: flex;
+ height: 100%;
+ }
+ }
+ /* postcss-bem-linter: ignore */
+ .statement__row--detail {
+ overflow: hidden;
+ min-height: 63px;
+ display: flex;
+ align-items: center;
+ padding: 0;
+ justify-content: center;
+ background-color: var(--general-section-1);
+
+ /* postcss-bem-linter: ignore */
+ &-text {
+ padding: 1.12em;
+ word-break: break-all;
+
+ .dc-popover__wrapper {
+ display: inline-block;
+ margin-left: 1rem;
+ }
+ }
+ }
+ .dc-vertical-tab__content--floating {
+ margin-right: 0;
+ }
+ .table__head {
+ height: auto;
+ .table__cell {
+ @include tablet-up {
+ white-space: break-spaces;
+ }
+ }
+ }
+}
+
+/** @define reports-page-wrapper; weak */
+.reports-page-wrapper {
+ height: 100%;
+}
+
+/** @define statement; weak */
+.statement {
+ .table__head {
+ font-weight: bold;
+ align-items: flex-start;
+ height: auto;
+
+ .table__cell {
+ height: auto;
+ }
+ @include desktop {
+ white-space: normal;
+ }
+ }
+
+ @include desktop {
+ height: 100%;
+ }
+ @include mobile {
+ flex: 1;
+
+ &__data-list-body {
+ height: 100%;
+
+ .action_type {
+ display: flex;
+ flex: none;
+ align-items: center;
+
+ &__row-title {
+ display: none;
+ }
+ }
+ .balance {
+ display: flex;
+
+ &__row-title {
+ flex: 50%;
+ }
+ }
+ }
+ }
+
+ &__content {
+ width: 100%;
+ max-height: 100%;
+ }
+ /*
+ TABLE STYLES
+ */
+ &__table {
+ height: calc(100% - 42px);
+ flex: 1;
+ min-width: 85rem;
+ }
+ &__row {
+ /* icon refId currency tr_time transaction cred/debt balance */
+ /* stylelint-disable-next-line declaration-colon-space-after */
+ grid-template-columns:
+ minmax(120px, 0.8fr) minmax(85px, 1.4fr) minmax(110px, 1.2fr) minmax(85px, 1.2fr) minmax(85px, 1fr)
+ minmax(85px, 1.2fr) minmax(85px, 1fr);
+
+ .date {
+ text-align: left;
+ }
+ }
+ .amount,
+ .balance {
+ justify-content: flex-end;
+ }
+ .amount {
+ font-weight: bold;
+
+ &--profit {
+ color: var(--text-profit-success);
+ }
+ &--loss {
+ color: var(--text-loss-danger);
+ }
+ }
+ .market-symbol-icon {
+ @include mobile {
+ width: 80px;
+ }
+ }
+ /*
+ MOBILE CARDS
+ */
+ &--card-view {
+ background: var(--general-main-2);
+ overflow: hidden;
+
+ .statement__filter {
+ padding: 0 $side-padding;
+ border-bottom: 1px solid var(--general-section-1);
+
+ &-content {
+ padding: 0;
+ margin: 0 auto;
+ max-width: 450px;
+ display: grid;
+ grid-template-columns: 1fr 3em 1fr;
+ text-align: center;
+
+ .datepicker__input-field {
+ width: 100%;
+ }
+ }
+ &-label {
+ display: none;
+ }
+ }
+ .statement__content {
+ padding: 0;
+ }
+ .statement__card-list {
+ padding: 0 $side-padding;
+ height: 100%;
+ }
+ }
+ &__statement-header {
+ justify-content: flex-end;
+ }
+ &__account-statistics {
+ background-color: var(--general-section-1);
+ @include desktop {
+ margin: 1.6rem 0;
+ width: 100%;
+ }
+ @include mobile {
+ margin: 0.8rem 0 1.6rem;
+ order: 1;
+ }
+ height: 4.8rem;
+ display: flex;
+ flex-direction: row;
+ text-align: left;
+
+ &-item {
+ flex: 1;
+ display: flex;
+
+ &:last-child {
+ border-right: unset;
+ }
+ &:first-child {
+ .statement__account-statistics--is-rectangle {
+ padding-left: 0;
+ }
+ }
+ &:not(:first-child) {
+ justify-content: center;
+ }
+ }
+ &-total-withdrawal {
+ @include desktop {
+ min-width: 19rem;
+ }
+ @include mobile {
+ min-width: 12.3rem;
+ }
+ }
+ &--is-rectangle {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ margin: auto;
+
+ @include desktop {
+ padding: 0.4rem 1.6rem;
+ }
+ @include mobile {
+ flex-direction: column;
+ }
+ }
+ &-title {
+ margin: auto;
+
+ @include mobile {
+ font-size: 1rem;
+ margin-bottom: 0;
+ }
+ }
+ &-amount {
+ margin: auto;
+
+ @include desktop {
+ margin-left: 1rem;
+ }
+ @include mobile {
+ font-size: 1.4rem;
+ margin-top: 1rem;
+ }
+ }
+ }
+}
+
+/** @define statement-card */
+.statement-card {
+ &__header {
+ font-size: 1em;
+ padding: 0.5em;
+ border-bottom: 1px solid var(--general-section-1);
+ display: flex;
+ justify-content: space-between;
+ }
+ /* postcss-bem-linter: ignore */
+ &__refid a {
+ color: $COLOR_SKY_BLUE;
+ text-decoration: none;
+ }
+ &__body {
+ padding: 0.5em;
+ font-size: 1.2em;
+ }
+ &__desc {
+ margin-bottom: 0.7em;
+ }
+ &__row {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ font-weight: bold;
+ }
+ &__cell-text {
+ vertical-align: middle;
+ }
+ &__amount {
+ &--sell,
+ &--deposit {
+ color: var(--text-profit-success);
+ }
+ &--buy,
+ &--withdrawal {
+ color: var(--text-loss-danger);
+ }
+ }
+ &__icon {
+ display: inline-block;
+ height: 1.6em;
+ width: 1.6em;
+ background-size: 1.6em 1.6em;
+ vertical-align: middle;
+ margin-right: 0.5em;
+ }
+}
+
+/** @define statement-empty */
+.statement-empty {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ &__icon {
+ height: 6.4em;
+ width: 6.4em;
+ margin-bottom: 1.4em;
+ }
+ &__text {
+ font-size: 1.4em;
+ }
+}
+
+/** @define profit-table; weak */
+.profit-table {
+ .table__head,
+ .table__foot {
+ font-weight: bold;
+ }
+ .table__head {
+ white-space: normal;
+ .table__cell {
+ align-items: flex-start;
+ }
+ }
+
+ @include desktop {
+ height: 100%;
+ }
+
+ @include mobile {
+ flex: 1;
+
+ &__data-list-body {
+ height: calc(100% - 50px);
+
+ .sell_time__row-title {
+ display: flex;
+ align-items: center;
+
+ .dc-icon {
+ margin-left: 4px;
+ }
+ }
+ }
+ &__data-list-footer {
+ height: 50px;
+ min-height: 50px;
+ font-weight: bold;
+
+ .data-list__row__content {
+ font-size: 1.2rem;
+ color: var(--text-prominent);
+ }
+ .data-list__row {
+ padding: 0;
+ }
+ }
+ }
+
+ &__content {
+ width: 100%;
+ max-height: 100%;
+ }
+ /*
+ TABLE STYLES
+ */
+ &__table {
+ height: calc(100% - 42px);
+ flex: 1;
+ min-width: 90rem;
+ }
+ &__row {
+ /* icon refId currency buy_time buy_price sell_time sell_price profit/loss */
+ /* stylelint-disable-next-line declaration-colon-space-after */
+ grid-template-columns:
+ minmax(120px, 0.6fr) minmax(130px, 1fr) minmax(85px, 1fr) minmax(85px, 1.2fr) minmax(85px, 1fr)
+ minmax(85px, 1.2fr) minmax(95px, 1fr) minmax(130px, 1fr);
+ }
+ .buy_price,
+ .sell_price,
+ .profit_loss {
+ @include desktop {
+ justify-content: flex-end;
+ text-align: right;
+ }
+ @include mobile {
+ justify-content: center;
+ }
+ }
+ .sell_time,
+ .purchase_time {
+ text-align: left;
+ min-width: 120px;
+ }
+ .profit_loss {
+ font-weight: bold;
+ @include tablet-up {
+ word-break: break-word;
+ }
+ @include mobile {
+ display: flex;
+
+ &__row-title {
+ flex: 50%;
+ }
+ }
+ }
+ .market-symbol-icon {
+ @include mobile {
+ width: 80px;
+ }
+ }
+ .duration-type {
+ flex: none;
+ position: relative;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0px 16px;
+ font-size: 1.4rem;
+ font-weight: bold;
+
+ &__background {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ opacity: 0.16;
+ border-radius: 16px;
+ }
+ &__ticks {
+ color: $color-yellow;
+
+ &__background {
+ background: $color-yellow;
+ }
+ }
+ &__seconds {
+ color: $color-green-1;
+
+ &__background {
+ background: $color-green-1;
+ }
+ }
+ &__minutes {
+ color: $color-blue-1;
+
+ &__background {
+ background: $color-blue-1;
+ }
+ }
+ &__hours {
+ color: $COLOR_BLUE;
+
+ &__background {
+ background: $COLOR_BLUE;
+ }
+ }
+ &__days {
+ color: $color-purple;
+
+ &__background {
+ background: $color-purple;
+ }
+ }
+ }
+}
+
+/** @define open-positions; weak */
+.open-positions {
+ height: 100%;
+
+ @include mobile {
+ flex: 1;
+ padding-top: 0.8rem;
+
+ &-multiplier {
+ /* postcss-bem-linter: ignore */
+ & .data-list__item {
+ background-color: var(--general-section-1);
+ border-radius: $BORDER_RADIUS;
+ border: 1px solid var(--border-disabled);
+ padding: 0;
+
+ /* postcss-bem-linter: ignore */
+ & .dc-progress-slider--completed {
+ display: none;
+ }
+ /* postcss-bem-linter: ignore */
+ & .dc-contract-card {
+ background-color: var(--general-main-2);
+ /* postcss-bem-linter: ignore */
+ &__wrapper {
+ background-color: var(--general-main-2);
+ max-width: unset;
+ margin: 0;
+ }
+ /* postcss-bem-linter: ignore */
+ &-item__footer {
+ background-color: var(--general-main-2);
+ border-radius: $BORDER_RADIUS;
+ }
+ /* postcss-bem-linter: ignore */
+ &__grid-underlying-trade {
+ border-bottom: 1px solid var(--border-disabled);
+ margin-bottom: 5px;
+ }
+ /* postcss-bem-linter: ignore */
+ &__grid-items {
+ grid-template-columns: 1fr 1fr 1fr;
+ margin-top: 0.4rem;
+ margin-bottom: 0.4rem;
+ }
+ /* postcss-bem-linter: ignore */
+ &__sell-button {
+ border-top: 1px solid var(--border-disabled);
+ }
+ /* postcss-bem-linter: ignore */
+ &-item {
+ /* postcss-bem-linter: ignore */
+ &__total-profit-loss {
+ border-color: var(--border-disabled);
+ }
+ /* postcss-bem-linter: ignore */
+ &:nth-child(1) {
+ order: 0;
+ }
+ /* postcss-bem-linter: ignore */
+ &:nth-child(3),
+ &:nth-child(5) {
+ order: 2;
+ }
+ /* postcss-bem-linter: ignore */
+ &:nth-child(6) {
+ order: 6;
+ }
+ }
+ }
+ /* postcss-bem-linter: ignore */
+ & .dc-contract-card-dialog-toggle {
+ border-color: var(--border-disabled);
+ }
+ }
+ /* postcss-bem-linter: ignore */
+ & .open-positions__data-list-body {
+ padding: 0;
+ height: calc(100% - 48px);
+ }
+ /* postcss-bem-linter: ignore */
+ & .open-positions__data-list-footer {
+ height: 48px;
+ min-height: 48px;
+ font-weight: bold;
+ align-items: center;
+ padding: 0;
+
+ /* postcss-bem-linter: ignore */
+ &--content {
+ padding: 0 1.6rem;
+ display: grid;
+ grid-template-columns: 0.7fr 1fr;
+
+ /* postcss-bem-linter: ignore */
+ .profit {
+ align-items: flex-start;
+ }
+ }
+ }
+ }
+ &__data-list {
+ margin-top: 0.8rem;
+ }
+ &__data-list-body {
+ height: calc(100% - 95px);
+
+ .dc-progress-bar__container {
+ max-width: 120px;
+ align-self: center;
+ }
+ }
+ &__data-list-footer {
+ height: 95px;
+ min-height: 95px;
+ font-weight: bold;
+ align-items: flex-start;
+ padding: 0.8rem 4rem 0 1rem;
+
+ &--title {
+ font-size: 1.4rem;
+ font-weight: bold;
+ color: var(--text-prominent);
+ }
+ &--content {
+ flex: 1;
+ padding: 0.8rem 1.6rem 0;
+ display: flex;
+ justify-content: space-between;
+
+ .purchase,
+ .indicative {
+ padding-bottom: 8px;
+ }
+ }
+ .data-list__row-title {
+ font-size: 1rem;
+ line-height: 1.4rem;
+ }
+ .data-list__row-content {
+ font-size: 1rem;
+ line-height: 1.4rem;
+ color: var(--text-prominent);
+ }
+ }
+ .dc-contract-card__no-resale-msg {
+ display: flex;
+ font-size: 1.4rem;
+ color: var(--text-general);
+ justify-content: center;
+ padding: 0.8rem 0rem;
+ }
+ }
+
+ &__content {
+ width: fit-content;
+ max-height: 100%;
+ }
+ &__indicative {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ width: 100%;
+
+ &--amount {
+ display: flex;
+ align-items: center;
+ @include desktop {
+ line-height: 2;
+ }
+ }
+ .dc-btn--sell {
+ height: 2.4rem;
+ }
+ &-no-resale-msg {
+ clear: both;
+ text-align: center;
+ font-size: smaller;
+ }
+ }
+ &__profit-loss {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+
+ &--movement {
+ width: 16px;
+ height: 16px;
+
+ &-complete,
+ &-complete:after {
+ display: none;
+ }
+ &:after {
+ content: '';
+ width: 16px;
+ }
+ }
+ &--negative {
+ color: var(--text-loss-danger);
+
+ &:before {
+ content: '-';
+ color: inherit;
+ }
+ }
+ &--positive {
+ color: var(--text-profit-success);
+
+ &:before {
+ content: '+';
+ color: inherit;
+ }
+ }
+ }
+ /*
+ TABLE STYLES
+ */
+ &__table {
+ height: calc(100% - 24px);
+ flex: 1;
+ margin-top: 20px;
+
+ .table__head {
+ height: auto;
+ white-space: normal;
+ @include tablet-up {
+ .profit,
+ .indicative {
+ white-space: break-spaces;
+ }
+ }
+
+ .table__cell {
+ font-weight: bold;
+ align-items: flex-start;
+ }
+ }
+ .table__body {
+ .open-positions__row_wrapper {
+ border-bottom: 1px solid var(--general-section-1);
+ }
+ }
+ .table__foot {
+ font-weight: bold;
+ white-space: normal;
+ }
+ }
+ &__row {
+ /* type refId currency buy_price payout_limit indicative_profit/loss indicative_price rem_time */
+ /* stylelint-disable-next-line declaration-colon-space-after */
+ grid-template-columns:
+ minmax(110px, 0.7fr) minmax(130px, 0.8fr) minmax(100px, 1.1fr) minmax(90px, 1.1fr) minmax(90px, 1.1fr)
+ minmax(120px, 1.1fr) minmax(120px, 1.1fr) minmax(85px, 1.1fr);
+ width: 100%;
+ grid-auto-rows: 100%;
+
+ &_wrapper {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ }
+ }
+ &__reports-meta {
+ @include mobile {
+ padding-bottom: 0px;
+ }
+ }
+ .buy_price,
+ .payout,
+ .indicative,
+ .purchase,
+ .multiplier,
+ .currency,
+ .profit {
+ @include desktop {
+ justify-content: center;
+ }
+ }
+ .type {
+ padding-right: 0;
+ }
+ .dc-progress-slider {
+ border: none;
+ margin: 0;
+
+ &__ticks {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &-step {
+ background: var(--state-hover);
+ }
+ &-wrapper {
+ margin-top: 6px;
+ }
+ &-caption {
+ padding: 0;
+ margin-right: 8px;
+ white-space: nowrap;
+ }
+ }
+ }
+ .market-symbol-icon {
+ @include mobile {
+ width: 80px;
+ }
+ }
+}
+
+/** @define open-positions-multiplier; weak */
+.open-positions-multiplier {
+ .open-positions {
+ &__row {
+ /* icon multiplier currency stake cancellation buy_price limit_order current_stake total_profit_loss action */
+ /* stylelint-disable-next-line declaration-colon-space-after */
+ grid-template-columns:
+ minmax(85px, 1fr) minmax(125px, 1fr) minmax(65px, 1fr) minmax(105px, 1fr) minmax(100px, 1fr)
+ minmax(105px, 1fr) minmax(105px, 1fr) minmax(105px, 1fr) minmax(125px, 1fr) minmax(90px, 1fr);
+
+ &-action {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+
+ .dc-remaining-time {
+ margin-left: 0.4rem;
+ font-size: inherit;
+ }
+ .dc-btn {
+ height: 2.4rem;
+ padding: 0 0.8rem;
+ min-width: 93px;
+
+ .dc-btn__text {
+ font-size: 1.2rem;
+ }
+ &:first-child {
+ margin-bottom: 0.4rem;
+ }
+ }
+ }
+ .limit_order,
+ .cancellation,
+ .bid_price {
+ @include desktop {
+ justify-content: center;
+ text-align: center;
+ width: 100%;
+ white-space: break-spaces;
+ }
+ }
+ .limit_order {
+ flex-direction: column;
+ align-items: flex-end;
+ text-align: right;
+ justify-content: flex-start;
+ }
+ .action {
+ padding-bottom: 0;
+ justify-content: center;
+ }
+ }
+ &__bid_price {
+ font-weight: bold;
+
+ &--negative {
+ color: var(--text-loss-danger);
+ }
+ &--positive {
+ color: var(--text-profit-success);
+ }
+ }
+ }
+}
+
+.open-positions,
+.statement,
+.profit-table {
+ /* postcss-bem-linter: ignore */
+ .data-list__body,
+ .data-list__footer {
+ padding: 0 1.6rem;
+ }
+ /* postcss-bem-linter: ignore */
+ .data-list__item {
+ background-color: var(--general-section-1);
+ }
+ .currency {
+ &__wrapper {
+ background: var(--border-active);
+ border-radius: $BORDER_RADIUS;
+ padding: 0 0.4rem;
+ }
+ }
+}
+
+/** @define empty-trade-history; weak*/
+.empty-trade-history {
+ position: absolute;
+ top: 20%;
+ left: 10%;
+ width: 50%;
+ display: flex;
+ flex-flow: column nowrap;
+ align-items: center;
+ justify-content: center;
+ margin: auto;
+
+ @media only screen and (max-width: 769px) {
+ position: static;
+ width: 50%;
+ }
+
+ &__icon {
+ width: 96px;
+ height: 96px;
+ margin-bottom: 16px;
+ @include colorIcon(var(--text-disabled));
+ }
+ &__text {
+ line-height: 20px;
+ }
+ .dc-btn {
+ width: 100%;
+ height: 40px;
+ border: 1px solid var(--button-secondary-default);
+ color: var(--text-general);
+ background: transparent;
+
+ &:hover {
+ background: var(--button-secondary-hover);
+ }
+ }
+}
diff --git a/packages/reports/src/sass/app/modules/smart-chart.scss b/packages/reports/src/sass/app/modules/smart-chart.scss
new file mode 100644
index 000000000000..3ae7a4689178
--- /dev/null
+++ b/packages/reports/src/sass/app/modules/smart-chart.scss
@@ -0,0 +1,247 @@
+/** @define chart-spot */
+.chart-spot {
+ display: flex;
+ flex-direction: column;
+
+ &__spot {
+ position: absolute;
+ bottom: -11px;
+ margin-left: -11.5px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+ color: var(--text-prominent);
+ background-color: var(--general-main-1);
+ border: 2px solid var(--text-less-prominent);
+
+ &--lost {
+ border-color: var(--text-loss-danger);
+ background: var(--text-loss-danger);
+ }
+ &--won {
+ background-color: var(--text-profit-success);
+ border-color: var(--text-profit-success);
+ }
+ &--won,
+ &--lost {
+ color: $COLOR_WHITE;
+ }
+ @include mobile {
+ bottom: -9.5px;
+ margin-left: -8px;
+ font-size: 0.8rem;
+ }
+ }
+ &__entry {
+ left: -12px;
+ top: -12px;
+ position: relative;
+ border: 6px solid var(--brand-red-coral);
+ background-color: var(--general-main-2);
+
+ @include mobile {
+ border-width: 3px;
+ left: -9px;
+ top: -9px;
+ }
+ }
+ &__spot,
+ &__entry {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+
+ @include mobile {
+ width: 16px;
+ height: 16px;
+ }
+ }
+ &__sell {
+ border-radius: 50%;
+ width: 2px;
+ height: 2px;
+ margin-left: -2px;
+ margin-top: -2px;
+ background-color: var(--text-prominent);
+ border: 2px solid var(--text-prominent);
+ }
+}
+
+/** @define chart-spot-label */
+.chart-spot-label {
+ &__info-container {
+ width: 100%;
+ position: relative;
+ }
+ &__time-value-container {
+ position: absolute;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+
+ &--top {
+ bottom: 21px;
+
+ .chart-spot-label__time-container {
+ margin-bottom: 2px;
+ }
+ @include mobile {
+ bottom: 9.5px;
+ }
+ }
+ &--bottom {
+ top: 18px;
+ flex-direction: column-reverse;
+
+ .chart-spot-label__time-container {
+ margin-top: 2px;
+ }
+ @include mobile {
+ top: 7.5px;
+ }
+ }
+ }
+ &__time-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0px 8px;
+
+ /* postcss-bem-linter: ignore */
+ svg {
+ /* postcss-bem-linter: ignore */
+ g {
+ stroke: var(--text-prominent);
+ }
+ }
+ }
+ &__time-icon {
+ margin-right: 2px;
+ }
+ &__value-container {
+ background: var(--text-less-prominent);
+ font-size: 1.4rem;
+ padding: 4px 8px;
+ border-radius: 11px;
+
+ /* postcss-bem-linter: ignore */
+ p {
+ font-weight: bold;
+ color: $color-white;
+ margin-top: 2px;
+ }
+ &--lost {
+ background-color: var(--text-loss-danger);
+ }
+ &--won {
+ background-color: var(--text-profit-success);
+ }
+ &--won,
+ &--lost {
+ color: var(--text-colored-background);
+ }
+ @include mobile {
+ font-size: 1rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0.2rem;
+ }
+ }
+}
+
+/** @define chart-marker-line */
+.chart-marker-line {
+ height: 94.5%;
+ margin-bottom: 0.8em;
+ z-index: 0 !important;
+ bottom: -100%;
+ transition: none;
+
+ &__wrapper {
+ border-left-width: 2px;
+ padding-left: 0.5em;
+ height: 100%;
+ border-color: var(--text-less-prominent);
+
+ &:after {
+ background: linear-gradient(to bottom, var(--general-main-1) 3%, transparent 10%);
+ position: absolute;
+ content: ' ';
+ top: 0px;
+ left: -1px;
+ height: 100%;
+ width: 3px;
+ }
+ }
+ &__icon {
+ position: absolute;
+ bottom: -23px;
+ left: -11px;
+ white-space: nowrap;
+
+ &--time {
+ /* postcss-bem-linter: ignore */
+ path {
+ fill: var(--text-less-prominent);
+ }
+ }
+ &--won {
+ /* postcss-bem-linter: ignore */
+ circle {
+ fill: var(--text-profit-success);
+ }
+ }
+ &--lost {
+ /* postcss-bem-linter: ignore */
+ circle {
+ fill: var(--text-loss-danger);
+ }
+ }
+ @include mobile {
+ bottom: -15px;
+ left: -7px;
+ width: 16px;
+ height: 16px;
+ }
+ }
+ &--solid {
+ border-left-style: solid;
+ }
+ &--dash {
+ border-left-style: dashed;
+ }
+}
+
+/** @define sc-toolbar-widget; weak */
+.sc-toolbar-widget {
+ &--bottom {
+ .ciq-menu {
+ margin: 0px;
+ }
+ }
+}
+
+/** @define smartcharts-mobile; weak */
+.smartcharts-mobile {
+ .cq-modal-dropdown {
+ left: 0px;
+ top: 0px;
+ }
+ .sc-chart-type,
+ .sc-interval {
+ .sc-tooltip__inner {
+ display: none;
+ }
+ }
+ .cq-chart-title .sc-dialog__body {
+ height: 100% !important;
+ }
+ .sc-categorical-display {
+ height: calc(100% - 38px) !important;
+ }
+ .cq-menu-dropdown .title .title-text {
+ display: inline;
+ }
+}
diff --git a/packages/reports/src/sass/app/modules/trading-mobile.scss b/packages/reports/src/sass/app/modules/trading-mobile.scss
new file mode 100644
index 000000000000..cc78321b8dfa
--- /dev/null
+++ b/packages/reports/src/sass/app/modules/trading-mobile.scss
@@ -0,0 +1,409 @@
+@include mobile {
+ /** @define dc-collapsible; weak */
+ .dc-collapsible {
+ &:before {
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ opacity: 1;
+ z-index: -1;
+ border-radius: $BORDER_RADIUS;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transition: opacity 0.25s;
+ background: var(--general-section-1);
+ }
+ &--is-expanded {
+ background: transparent;
+
+ &:before {
+ opacity: 0.75;
+ }
+ }
+ }
+ /** @define barrier; weak */
+ .barrier {
+ .draggable {
+ pointer-events: none;
+
+ & .price {
+ padding-left: 0;
+
+ &:after {
+ content: none;
+ }
+ }
+ }
+ &__widget {
+ display: grid;
+ grid-template-columns: 3.5fr 1fr;
+ height: 4rem;
+ margin: 0 0 0.8rem;
+ background: var(--general-main-1);
+ border-radius: $BORDER_RADIUS;
+
+ &-title {
+ font-size: 1.4rem;
+ font-weight: 400;
+ text-transform: none;
+ color: var(--text-less-prominent);
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding-right: 0.8rem;
+ }
+ }
+ &__fields {
+ width: 100%;
+
+ &-input {
+ width: 100%;
+ padding: 0;
+ font-weight: bold;
+ border: none;
+
+ &--is-offset {
+ pointer-events: none;
+ }
+ &:focus,
+ &:active,
+ &:hover {
+ border: none;
+ }
+ }
+ &-single {
+ margin: 0;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ }
+ & .dc-tooltip {
+ width: 100%;
+ }
+ & .dc-input-wrapper {
+ &__input {
+ outline: 0;
+ border: none;
+ }
+ &__button {
+ transform: scale(1.4);
+ stroke: var(--text-general);
+
+ &:hover,
+ &:active {
+ background: none;
+ }
+ }
+ }
+ }
+ }
+ /** @define allow-equals; weak */
+ .allow-equals {
+ .dc-checkbox__label {
+ font-weight: bold;
+ color: var(--text-prominent);
+ margin-right: 0.8rem;
+ }
+ }
+ /** @define dc-modal; weak */
+ .dc-modal {
+ &__container_trade-params {
+ border-radius: 2px;
+ box-shadow: 0 16px 16px 0 var(--shadow-menu-2), 0 0 16px 0 var(--shadow-menu-2);
+ overflow-y: scroll;
+
+ /* postcss-bem-linter: ignore */
+ .dc-relative-datepicker {
+ margin-top: -0.8rem;
+ max-width: 110px;
+ margin-left: auto;
+ margin-right: auto;
+
+ /* iPhone SE screen height fixes due to UI space restrictions */
+ @media only screen and (max-height: 480px) {
+ margin-top: -4.6rem;
+ }
+ }
+ /* iPhone SE screen height fixes due to UI space restrictions */
+ @media only screen and (max-height: 480px) {
+ top: 0.4rem;
+ }
+ }
+ &-header {
+ /* postcss-bem-linter: ignore */
+ &--trade-params {
+ line-height: 1.4;
+ border-bottom-width: 1px;
+
+ /* postcss-bem-linter: ignore */
+ .dc-modal-header__close {
+ padding: 0.8rem 0.8rem 0;
+ margin: 0.4rem 0.4rem 0.2rem;
+ }
+ }
+ }
+ }
+ /** @define dc-tabs; weak */
+ .dc-tabs {
+ /* postcss-bem-linter: ignore */
+ &--trade-params__multiplier-tabs {
+ /* postcss-bem-linter: ignore */
+ .dc-tabs__content {
+ display: flex;
+ flex-direction: column;
+ min-height: 400px;
+
+ @media only screen and (max-height: 480px) {
+ min-height: 360px;
+ }
+ }
+ }
+ }
+ /** @define trade-params; weak */
+ .trade-params {
+ &__error-popup {
+ top: 12rem !important;
+ opacity: 0.8;
+ z-index: 2 !important;
+
+ &--has-numpad {
+ z-index: 9999 !important;
+ top: 0.8rem !important;
+ }
+ }
+ &__duration {
+ &-tickpicker {
+ height: 328px;
+
+ .dc-tick-picker {
+ max-width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ }
+ &__amount {
+ &-keypad {
+ width: 100%;
+ padding: 1.6rem;
+ height: auto;
+ margin-top: 0.8rem;
+ margin-bottom: 0.8rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .dc-numpad--is-currency,
+ .dc-numpad--is-regular {
+ max-width: 100%;
+ grid-template-columns: repeat(4, 1fr);
+ grid-gap: 16px;
+ }
+ .dc-numpad__increment,
+ .dc-numpad__decrement {
+ height: 48px !important;
+
+ .dc-btn__text {
+ font-size: 3rem;
+ font-weight: normal;
+ }
+ &[disabled] {
+ .dc-btn__text {
+ color: var(--text-disabled);
+ }
+ }
+ }
+ .dc-numpad__number {
+ border-radius: 2.4rem;
+ background-color: var(--general-section-2);
+ width: 48px;
+ height: 48px !important;
+ font-weight: 700;
+ text-transform: none;
+ line-height: 1.75;
+ color: var(--text-prominent);
+ text-align: left !important;
+
+ &--is-left-aligned {
+ padding: 0 0 0 0.2rem;
+ }
+ }
+ .dc-numpad__bkspace,
+ .dc-numpad__ok {
+ .dc-numpad__number {
+ height: 100% !important;
+ }
+ }
+ .dc-numpad__bkspace {
+ .dc-numpad__number {
+ &[disabled] {
+ background-color: var(--general-disabled);
+ }
+ .dc-text {
+ font-size: 1.8rem;
+ /* -webkit-touch-callout only is supported on iOS webkit engine, thus it should apply iOS only styles */
+ @supports (-webkit-touch-callout: none) {
+ @media only screen and (min-width: 360px) {
+ font-size: 3rem;
+ }
+ }
+ }
+ }
+ }
+ /* iPhone SE screen height fixes due to UI space restrictions */
+ @media only screen and (max-height: 480px) {
+ transform: scale(1, 0.92);
+ transform-origin: top;
+ margin-top: -0.4rem;
+ }
+ }
+ }
+ &__header {
+ @include mobile {
+ padding: 0.5rem 0;
+
+ &-label {
+ line-height: 2rem;
+ }
+ &-value {
+ line-height: 1.8rem;
+ font-size: 1.2rem;
+
+ &--has-error {
+ color: var(--status-danger);
+ font-weight: bold;
+ }
+ }
+ }
+ }
+ &__contract-type-container {
+ display: flex;
+
+ .contract-type-widget {
+ flex: 1;
+ }
+ }
+ &__multiplier {
+ &-radio-group {
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 1.6rem;
+ margin-top: 0rem;
+ flex: 1;
+
+ &--empty {
+ display: none;
+ }
+ .dc-radio-group__item {
+ min-height: 4.8rem;
+ max-height: 4.8rem;
+ width: 100%;
+ align-items: center;
+ margin-bottom: 0.8rem;
+ padding: 0.8rem;
+ border-radius: $BORDER_RADIUS;
+ border: 1px solid var(--border-normal);
+ font-size: 1.4rem;
+ flex-direction: row;
+
+ &--selected {
+ border: 1px solid var(--brand-secondary);
+ }
+ }
+ }
+ &-amount-text {
+ padding: 1.6rem 4rem 0;
+ line-height: 1.4rem;
+ text-align: center;
+ color: var(--text-general);
+ }
+ &-risk-management-dialog {
+ display: grid;
+ grid-template-rows: auto auto auto 1fr;
+
+ &--no-cancel {
+ grid-template-rows: auto auto 1fr;
+ }
+ &-bottom-separator {
+ border-bottom: 1px solid var(--border-disabled);
+ height: calc(100% - 1.6rem);
+ }
+ &-apply-button {
+ display: flex;
+ align-items: flex-end;
+ margin: 0 1.6rem;
+
+ .dc-btn {
+ flex: 1;
+ height: 4rem;
+ }
+ }
+ .trade-container__fieldset {
+ padding: 1rem 1.6rem;
+ margin-bottom: 0;
+ border-bottom: 1px solid var(--border-disabled);
+ border-radius: 0;
+
+ .dc-input-field {
+ z-index: 0;
+ }
+ .dc-popover {
+ padding: 0.6rem 1rem;
+ }
+ }
+ .dc-checkbox__box {
+ margin-left: 0rem;
+ }
+ .dc-radio-group {
+ padding: 1.6rem 0rem;
+ }
+ }
+ &-ic-info-wrapper {
+ display: flex;
+ justify-content: flex-start;
+ position: absolute;
+ top: 0.6rem;
+ left: 0.2rem;
+ z-index: 2;
+
+ .dc-popover {
+ padding: 0.5rem 1rem;
+ }
+ }
+ &-deal-cancellation-dialog {
+ .dc-checkbox {
+ margin-top: 2.6rem;
+
+ .dc-checkbox__box {
+ margin-left: 0;
+ }
+ }
+ }
+ &-trade-info {
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 1.6rem;
+ align-items: center;
+
+ div:nth-child(2) {
+ padding-top: 1.6rem;
+ }
+
+ &-tooltip-text {
+ text-align: right;
+ border-bottom: 1px dotted var(--text-general);
+ display: flex;
+
+ *:first-child {
+ &:before {
+ content: ': ';
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/reports/src/templates/_common/components/loading.jsx b/packages/reports/src/templates/_common/components/loading.jsx
new file mode 100644
index 000000000000..1e1379209e36
--- /dev/null
+++ b/packages/reports/src/templates/_common/components/loading.jsx
@@ -0,0 +1,15 @@
+import classNames from 'classnames';
+import React from 'react';
+
+const Loading = ({ className, is_invisible, theme, id }) => (
+
+ {Array.from(new Array(5)).map((x, inx) => (
+
+ ))}
+
+);
+
+export default Loading;
diff --git a/packages/shared/package.json b/packages/shared/package.json
index d5c11a975e84..5150d1cd25f8 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -45,6 +45,7 @@
"extend": "^3.0.2",
"i18next": "^20.3.2",
"js-cookie": "^2.2.1",
+ "mobx": "^5.15.7",
"moment": "^2.29.2",
"object.fromentries": "^2.0.0",
"react": "^16.14.0",
diff --git a/packages/shared/src/index.js b/packages/shared/src/index.js
index 9b588447e438..b81e2bda6a98 100644
--- a/packages/shared/src/index.js
+++ b/packages/shared/src/index.js
@@ -25,4 +25,6 @@ export * from './utils/string';
export * from './utils/url';
export * from './utils/validation';
export * from './services';
+export * from './utils/helpers';
+export * from './utils/constants';
export * from './utils/loader-handler';
diff --git a/packages/shared/src/loaders/deriv-reports-loader.js b/packages/shared/src/loaders/deriv-reports-loader.js
new file mode 100644
index 000000000000..ba5aaa36cb5f
--- /dev/null
+++ b/packages/shared/src/loaders/deriv-reports-loader.js
@@ -0,0 +1,47 @@
+const resolve = require('path').resolve;
+const existsSync = require('fs').existsSync;
+/* Using this loader you can import components from @deriv/components without having to manually
+import the corresponding stylesheet. The deriv-reports-loader will automatically import
+stylesheets.
+
+ import { PoaExpired } from '@deriv/reports';
+ ↓ ↓ ↓
+ import PoaExpired from '@deriv/reports/dist/js/poa-expired';
+*/
+
+function getKebabCase(str) {
+ return str
+ .split(/(?=[A-Z])/)
+ .join('-')
+ .toLowerCase();
+}
+
+function checkExists(component) {
+ return existsSync(resolve(__dirname, '../../../reports/src/Components/', component, `${component}.scss`));
+}
+
+module.exports = function (source, map) {
+ const lines = source.split(/\n/);
+ const mapped_lines = lines.map(line => {
+ const matches = /\s*import\s+\{(.*)\}\s*from\s+\'@deriv\/reports/.exec(line); // eslint-disable-line no-useless-escape
+ if (!matches || !matches[1]) {
+ return line; // do nothing;
+ }
+ const components = matches[1]
+ .replace(/\sas\s\w+/, '') // Remove aliasing from imports.
+ .replace(/\s+/g, '')
+ .split(',');
+ const replace = components
+ .map(
+ c => `
+import ${c} from '@deriv/reports/dist/reports/js/${getKebabCase(c)}';
+${checkExists(getKebabCase(c)) ? `import '@deriv/reports/dist/reports/css/${getKebabCase(c)}.css';` : ''}
+ `
+ )
+ .join('\n');
+
+ return replace;
+ });
+
+ return this.callback(null, mapped_lines.join('\n'), map);
+};
diff --git a/packages/shared/src/utils/constants/barriers.js b/packages/shared/src/utils/constants/barriers.js
new file mode 100644
index 000000000000..353ef635b148
--- /dev/null
+++ b/packages/shared/src/utils/constants/barriers.js
@@ -0,0 +1,36 @@
+export const CONTRACT_SHADES = {
+ CALL: 'ABOVE',
+ PUT: 'BELOW',
+ CALLE: 'ABOVE',
+ PUTE: 'BELOW',
+ EXPIRYRANGE: 'BETWEEN',
+ EXPIRYMISS: 'OUTSIDE',
+ RANGE: 'BETWEEN',
+ UPORDOWN: 'OUTSIDE',
+ ONETOUCH: 'NONE_SINGLE', // no shade
+ NOTOUCH: 'NONE_SINGLE', // no shade
+ ASIANU: 'ABOVE',
+ ASIAND: 'BELOW',
+ MULTUP: 'ABOVE',
+ MULTDOWN: 'BELOW',
+};
+
+// Default non-shade according to number of barriers
+export const DEFAULT_SHADES = {
+ 1: 'NONE_SINGLE',
+ 2: 'NONE_DOUBLE',
+};
+
+export const BARRIER_COLORS = {
+ GREEN: '#4bb4b3',
+ RED: '#ec3f3f',
+ ORANGE: '#ff6444',
+ GRAY: '#999999',
+ DARK_GRAY: '#6E6E6E',
+};
+
+export const BARRIER_LINE_STYLES = {
+ DASHED: 'dashed',
+ DOTTED: 'dotted',
+ SOLID: 'solid',
+};
diff --git a/packages/shared/src/utils/constants/contract.js b/packages/shared/src/utils/constants/contract.js
new file mode 100644
index 000000000000..f18312c7ac30
--- /dev/null
+++ b/packages/shared/src/utils/constants/contract.js
@@ -0,0 +1,415 @@
+import React from 'react';
+import { localize, Localize } from '@deriv/translations';
+import { shouldShowCancellation, shouldShowExpiration } from '../contract';
+
+export const getLocalizedBasis = () => ({
+ payout: localize('Payout'),
+ stake: localize('Stake'),
+ multiplier: localize('Multiplier'),
+});
+
+/**
+ * components can be undef or an array containing any of: 'start_date', 'barrier', 'last_digit'
+ * ['duration', 'amount'] are omitted, as they're available in all contract types
+ */
+export const getContractTypesConfig = symbol => ({
+ rise_fall: {
+ title: localize('Rise/Fall'),
+ trade_types: ['CALL', 'PUT'],
+ basis: ['stake', 'payout'],
+ components: ['start_date'],
+ barrier_count: 0,
+ },
+ rise_fall_equal: {
+ title: localize('Rise/Fall'),
+ trade_types: ['CALLE', 'PUTE'],
+ basis: ['stake', 'payout'],
+ components: ['start_date'],
+ barrier_count: 0,
+ },
+ high_low: {
+ title: localize('Higher/Lower'),
+ trade_types: ['CALL', 'PUT'],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ barrier_count: 1,
+ },
+ touch: {
+ title: localize('Touch/No Touch'),
+ trade_types: ['ONETOUCH', 'NOTOUCH'],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ },
+ end: {
+ title: localize('Ends In/Ends Out'),
+ trade_types: ['EXPIRYMISS', 'EXPIRYRANGE'],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ },
+ stay: {
+ title: localize('Stays In/Goes Out'),
+ trade_types: ['RANGE', 'UPORDOWN'],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ },
+ asian: {
+ title: localize('Asian Up/Asian Down'),
+ trade_types: ['ASIANU', 'ASIAND'],
+ basis: ['stake', 'payout'],
+ components: [],
+ },
+ match_diff: {
+ title: localize('Matches/Differs'),
+ trade_types: ['DIGITMATCH', 'DIGITDIFF'],
+ basis: ['stake', 'payout'],
+ components: ['last_digit'],
+ },
+ even_odd: {
+ title: localize('Even/Odd'),
+ trade_types: ['DIGITODD', 'DIGITEVEN'],
+ basis: ['stake', 'payout'],
+ components: [],
+ },
+ over_under: {
+ title: localize('Over/Under'),
+ trade_types: ['DIGITOVER', 'DIGITUNDER'],
+ basis: ['stake', 'payout'],
+ components: ['last_digit'],
+ },
+ // TODO: update the rest of these contracts config
+ lb_call: { title: localize('Close-to-Low'), trade_types: ['LBFLOATCALL'], basis: ['multiplier'], components: [] },
+ lb_put: { title: localize('High-to-Close'), trade_types: ['LBFLOATPUT'], basis: ['multiplier'], components: [] },
+ lb_high_low: { title: localize('High-to-Low'), trade_types: ['LBHIGHLOW'], basis: ['multiplier'], components: [] },
+ tick_high_low: {
+ title: localize('High Tick/Low Tick'),
+ trade_types: ['TICKHIGH', 'TICKLOW'],
+ basis: [],
+ components: [],
+ },
+ run_high_low: {
+ title: localize('Only Ups/Only Downs'),
+ trade_types: ['RUNHIGH', 'RUNLOW'],
+ basis: [],
+ components: [],
+ },
+ reset: {
+ title: localize('Reset Up/Reset Down'),
+ trade_types: ['RESETCALL', 'RESETPUT'],
+ basis: [],
+ components: [],
+ },
+ callputspread: {
+ title: localize('Spread Up/Spread Down'),
+ trade_types: ['CALLSPREAD', 'PUTSPREAD'],
+ basis: [],
+ components: [],
+ },
+ multiplier: {
+ title: localize('Multipliers'),
+ trade_types: ['MULTUP', 'MULTDOWN'],
+ basis: ['stake'],
+ components: [
+ 'take_profit',
+ 'stop_loss',
+ ...(shouldShowCancellation(symbol) ? ['cancellation'] : []),
+ ...(shouldShowExpiration(symbol) ? ['expiration'] : []),
+ ],
+ config: { hide_duration: true },
+ }, // hide Duration for Multiplier contracts for now
+});
+
+export const getContractCategoriesConfig = () => ({
+ [localize('Multipliers')]: ['multiplier'],
+ [localize('Ups & Downs')]: ['rise_fall', 'rise_fall_equal', 'run_high_low', 'reset', 'asian', 'callputspread'],
+ [localize('Highs & Lows')]: ['high_low', 'touch', 'tick_high_low'],
+ [localize('Ins & Outs')]: ['end', 'stay'],
+ [localize('Look Backs')]: ['lb_high_low', 'lb_put', 'lb_call'],
+ [localize('Digits')]: ['match_diff', 'even_odd', 'over_under'],
+});
+
+export const unsupported_contract_types_list = [
+ // TODO: remove these once all contract types are supported
+ 'callputspread',
+ 'run_high_low',
+ 'reset',
+ 'asian',
+ 'tick_high_low',
+ 'end',
+ 'stay',
+ 'lb_call',
+ 'lb_put',
+ 'lb_high_low',
+];
+
+export const getCardLabels = () => ({
+ APPLY: localize('Apply'),
+ STAKE: localize('Stake:'),
+ CLOSE: localize('Close'),
+ CANCEL: localize('Cancel'),
+ CURRENT_STAKE: localize('Current stake:'),
+ DEAL_CANCEL_FEE: localize('Deal cancel. fee:'),
+ TAKE_PROFIT: localize('Take profit:'),
+ BUY_PRICE: localize('Buy price:'),
+ STOP_LOSS: localize('Stop loss:'),
+ TOTAL_PROFIT_LOSS: localize('Total profit/loss:'),
+ PROFIT_LOSS: localize('Profit/Loss:'),
+ POTENTIAL_PROFIT_LOSS: localize('Potential profit/loss:'),
+ INDICATIVE_PRICE: localize('Indicative price:'),
+ PAYOUT: localize('Sell price:'),
+ PURCHASE_PRICE: localize('Buy price:'),
+ POTENTIAL_PAYOUT: localize('Payout limit:'),
+ TICK: localize('Tick '),
+ WON: localize('Won'),
+ LOST: localize('Lost'),
+ DAYS: localize('days'),
+ DAY: localize('day'),
+ SELL: localize('Sell'),
+ INCREMENT_VALUE: localize('Increment value'),
+ DECREMENT_VALUE: localize('Decrement value'),
+ TAKE_PROFIT_LOSS_NOT_AVAILABLE: localize(
+ 'Take profit and/or stop loss are not available while deal cancellation is active.'
+ ),
+ DONT_SHOW_THIS_AGAIN: localize("Don't show this again"),
+ RESALE_NOT_OFFERED: localize('Resale not offered'),
+ NOT_AVAILABLE: localize('N/A'),
+});
+
+export const getMarketNamesMap = () => ({
+ FRXAUDCAD: localize('AUD/CAD'),
+ FRXAUDCHF: localize('AUD/CHF'),
+ FRXAUDJPY: localize('AUD/JPY'),
+ FRXAUDNZD: localize('AUD/NZD'),
+ FRXAUDPLN: localize('AUD/PLN'),
+ FRXAUDUSD: localize('AUD/USD'),
+ FRXBROUSD: localize('Oil/USD'),
+ FRXEURAUD: localize('EUR/AUD'),
+ FRXEURCAD: localize('EUR/CAD'),
+ FRXEURCHF: localize('EUR/CHF'),
+ FRXEURGBP: localize('EUR/GBP'),
+ FRXEURJPY: localize('EUR/JPY'),
+ FRXEURNZD: localize('EUR/NZD'),
+ FRXEURUSD: localize('EUR/USD'),
+ FRXGBPAUD: localize('GBP/AUD'),
+ FRXGBPCAD: localize('GBP/CAD'),
+ FRXGBPCHF: localize('GBP/CHF'),
+ FRXGBPJPY: localize('GBP/JPY'),
+ FRXGBPNOK: localize('GBP/NOK'),
+ FRXGBPUSD: localize('GBP/USD'),
+ FRXNZDJPY: localize('NZD/JPY'),
+ FRXNZDUSD: localize('NZD/USD'),
+ FRXUSDCAD: localize('USD/CAD'),
+ FRXUSDCHF: localize('USD/CHF'),
+ FRXUSDJPY: localize('USD/JPY'),
+ FRXUSDNOK: localize('USD/NOK'),
+ FRXUSDPLN: localize('USD/PLN'),
+ FRXUSDSEK: localize('USD/SEK'),
+ FRXXAGUSD: localize('Silver/USD'),
+ FRXXAUUSD: localize('Gold/USD'),
+ FRXXPDUSD: localize('Palladium/USD'),
+ FRXXPTUSD: localize('Platinum/USD'),
+ OTC_AEX: localize('Dutch Index'),
+ OTC_AS51: localize('Australian Index'),
+ OTC_DJI: localize('Wall Street Index'),
+ OTC_FCHI: localize('French Index'),
+ OTC_FTSE: localize('UK Index'),
+ OTC_GDAXI: localize('German Index'),
+ OTC_HSI: localize('Hong Kong Index'),
+ OTC_IBEX35: localize('Spanish Index'),
+ OTC_N225: localize('Japanese Index'),
+ OTC_NDX: localize('US Tech Index'),
+ OTC_SPC: localize('US Index'),
+ OTC_SSMI: localize('Swiss Index'),
+ OTC_SX5E: localize('Euro 50 Index'),
+ R_10: localize('Volatility 10 Index'),
+ R_25: localize('Volatility 25 Index'),
+ R_50: localize('Volatility 50 Index'),
+ R_75: localize('Volatility 75 Index'),
+ R_100: localize('Volatility 100 Index'),
+ BOOM300N: localize('Boom 300 Index'),
+ BOOM500: localize('Boom 500 Index'),
+ BOOM1000: localize('Boom 1000 Index'),
+ CRASH300N: localize('Crash 300 Index'),
+ CRASH500: localize('Crash 500 Index'),
+ CRASH1000: localize('Crash 1000 Index'),
+ RDBEAR: localize('Bear Market Index'),
+ RDBULL: localize('Bull Market Index'),
+ STPRNG: localize('Step Index'),
+ WLDAUD: localize('AUD Basket'),
+ WLDEUR: localize('EUR Basket'),
+ WLDGBP: localize('GBP Basket'),
+ WLDXAU: localize('Gold Basket'),
+ WLDUSD: localize('USD Basket'),
+ '1HZ10V': localize('Volatility 10 (1s) Index'),
+ '1HZ100V': localize('Volatility 100 (1s) Index'),
+ '1HZ200V': localize('Volatility 200 (1s) Index'),
+ '1HZ300V': localize('Volatility 300 (1s) Index'),
+ JD10: localize('Jump 10 Index'),
+ JD25: localize('Jump 25 Index'),
+ JD50: localize('Jump 50 Index'),
+ JD75: localize('Jump 75 Index'),
+ JD100: localize('Jump 100 Index'),
+ JD150: localize('Jump 150 Index'),
+ JD200: localize('Jump 200 Index'),
+ CRYBCHUSD: localize('BCH/USD'),
+ CRYBNBUSD: localize('BNB/USD'),
+ CRYBTCLTC: localize('BTC/LTC'),
+ CRYIOTUSD: localize('IOT/USD'),
+ CRYNEOUSD: localize('NEO/USD'),
+ CRYOMGUSD: localize('OMG/USD'),
+ CRYTRXUSD: localize('TRX/USD'),
+ CRYBTCETH: localize('BTC/ETH'),
+ CRYZECUSD: localize('ZEC/USD'),
+ CRYXMRUSD: localize('ZMR/USD'),
+ CRYXMLUSD: localize('XLM/USD'),
+ CRYXRPUSD: localize('XRP/USD'),
+ CRYBTCUSD: localize('BTC/USD'),
+ CRYDSHUSD: localize('DSH/USD'),
+ CRYETHUSD: localize('ETH/USD'),
+ CRYEOSUSD: localize('EOS/USD'),
+ CRYLTCUSD: localize('LTC/USD'),
+});
+
+export const getUnsupportedContracts = () => ({
+ EXPIRYMISS: {
+ name: ,
+ position: 'top',
+ },
+ EXPIRYRANGE: {
+ name: ,
+ position: 'bottom',
+ },
+ RANGE: {
+ name: ,
+ position: 'top',
+ },
+ UPORDOWN: {
+ name: ,
+ position: 'bottom',
+ },
+ RESETCALL: {
+ name: ,
+ position: 'top',
+ },
+ RESETPUT: {
+ name: ,
+ position: 'bottom',
+ },
+ TICKHIGH: {
+ name: ,
+ position: 'top',
+ },
+ TICKLOW: {
+ name: ,
+ position: 'bottom',
+ },
+ ASIANU: {
+ name: ,
+ position: 'top',
+ },
+ ASIAND: {
+ name: ,
+ position: 'bottom',
+ },
+ LBFLOATCALL: {
+ name: ,
+ position: 'top',
+ },
+ LBFLOATPUT: {
+ name: ,
+ position: 'top',
+ },
+ LBHIGHLOW: {
+ name: ,
+ position: 'top',
+ },
+ CALLSPREAD: {
+ name: ,
+ position: 'top',
+ },
+ PUTSPREAD: {
+ name: ,
+ position: 'bottom',
+ },
+ RUNHIGH: {
+ name: ,
+ position: 'top',
+ },
+ RUNLOW: {
+ name: ,
+ position: 'bottom',
+ },
+});
+
+export const getSupportedContracts = is_high_low => ({
+ CALL: {
+ name: is_high_low ? : ,
+ position: 'top',
+ },
+ PUT: {
+ name: is_high_low ? : ,
+ position: 'bottom',
+ },
+ CALLE: {
+ name: ,
+ position: 'top',
+ },
+ PUTE: {
+ name: ,
+ position: 'bottom',
+ },
+ DIGITMATCH: {
+ name: ,
+ position: 'top',
+ },
+ DIGITDIFF: {
+ name: ,
+ position: 'bottom',
+ },
+ DIGITEVEN: {
+ name: ,
+ position: 'top',
+ },
+ DIGITODD: {
+ name: ,
+ position: 'bottom',
+ },
+ DIGITOVER: {
+ name: ,
+ position: 'top',
+ },
+ DIGITUNDER: {
+ name: ,
+ position: 'bottom',
+ },
+ ONETOUCH: {
+ name: ,
+ position: 'top',
+ },
+ NOTOUCH: {
+ name: ,
+ position: 'bottom',
+ },
+ MULTUP: {
+ name: ,
+ position: 'top',
+ },
+ MULTDOWN: {
+ name: ,
+ position: 'bottom',
+ },
+});
+
+export const getContractConfig = is_high_low => ({
+ ...getSupportedContracts(is_high_low),
+ ...getUnsupportedContracts(),
+});
+
+export const getContractTypeDisplay = (type, is_high_low = false) => {
+ return getContractConfig(is_high_low)[type] ? getContractConfig(is_high_low)[type.toUpperCase()].name : '';
+};
+
+export const getContractTypePosition = (type, is_high_low = false) =>
+ getContractConfig(is_high_low)[type] ? getContractConfig(is_high_low)[type.toUpperCase()].position : 'top';
+
+export const isCallPut = trade_type =>
+ trade_type === 'rise_fall' || trade_type === 'rise_fall_equal' || trade_type === 'high_low';
diff --git a/packages/shared/src/utils/constants/index.js b/packages/shared/src/utils/constants/index.js
new file mode 100644
index 000000000000..b8a5f56939bb
--- /dev/null
+++ b/packages/shared/src/utils/constants/index.js
@@ -0,0 +1,2 @@
+export * from './barriers';
+export * from './contract';
diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/barrier.js b/packages/shared/src/utils/helpers/__tests__/barrier.js
similarity index 98%
rename from packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/barrier.js
rename to packages/shared/src/utils/helpers/__tests__/barrier.js
index 8b00dec5dbdd..bdc7e2b23098 100644
--- a/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/barrier.js
+++ b/packages/shared/src/utils/helpers/__tests__/barrier.js
@@ -1,5 +1,4 @@
import { expect } from 'chai';
-import React from 'react';
import { buildBarriersConfig } from '../barrier';
describe('buildBarriersConfig', () => {
diff --git a/packages/shared/src/utils/helpers/__tests__/barriers.js b/packages/shared/src/utils/helpers/__tests__/barriers.js
new file mode 100644
index 000000000000..af2b46b7ec0f
--- /dev/null
+++ b/packages/shared/src/utils/helpers/__tests__/barriers.js
@@ -0,0 +1,58 @@
+import { expect } from 'chai';
+import * as Barriers from '../barriers';
+
+describe('Barriers', () => {
+ describe('isBarrierSupported', () => {
+ it('should return false when barrier is not in CONTRACT_SHADES', () => {
+ expect(Barriers.isBarrierSupported('SomeThinMadeUp')).to.eql(false);
+ });
+ it('should return true when barrier is in CONTRACT_SHADES', () => {
+ expect(Barriers.isBarrierSupported('CALL')).to.eql(true);
+ });
+ });
+
+ describe('barriersToString', () => {
+ it('should convert non-zero barriers which do not have +/- to string consisting of them without +/- while is_relative is false', () => {
+ expect(Barriers.barriersToString(false, 10, 15)).to.deep.eql(['10', '15']);
+ });
+ it('should convert values without +/- and zero to string consisting of them without +/- while is_relative is false', () => {
+ expect(Barriers.barriersToString(false, 0, 15)).to.deep.eql(['0', '15']);
+ });
+ it('should convert barriers which have +/- to string consisting of them without +/- while is_relative is false', () => {
+ expect(Barriers.barriersToString(false, +11, 15)).to.deep.eql(['11', '15']);
+ });
+ it('should convert barriers which have +/- to string consisting of them with +/- while is_relative is true', () => {
+ expect(Barriers.barriersToString(true, +11, +15)).to.deep.eql(['+11', '+15']);
+ });
+ });
+
+ describe('barriersObjectToArray', () => {
+ const main = {
+ color: 'green',
+ draggable: false,
+ };
+ it('should return an array from values in barriers object', () => {
+ const barriers = {
+ main,
+ };
+ expect(Barriers.barriersObjectToArray(barriers, [])).to.deep.eql([
+ {
+ color: 'green',
+ draggable: false,
+ },
+ ]);
+ });
+ it('should return an array from values in barriers object (empty values should be filtered out)', () => {
+ const barriers = {
+ main,
+ somethingEmpty: {},
+ };
+ expect(Barriers.barriersObjectToArray(barriers, [])).to.deep.eql([
+ {
+ color: 'green',
+ draggable: false,
+ },
+ ]);
+ });
+ });
+});
diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/durations.js b/packages/shared/src/utils/helpers/__tests__/durations.js
similarity index 100%
rename from packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/durations.js
rename to packages/shared/src/utils/helpers/__tests__/durations.js
diff --git a/packages/trader/src/Stores/Modules/Portfolio/Helpers/__tests__/format-response.js b/packages/shared/src/utils/helpers/__tests__/format-response.js
similarity index 97%
rename from packages/trader/src/Stores/Modules/Portfolio/Helpers/__tests__/format-response.js
rename to packages/shared/src/utils/helpers/__tests__/format-response.js
index fb859c4946d1..2a02b7dc8084 100644
--- a/packages/trader/src/Stores/Modules/Portfolio/Helpers/__tests__/format-response.js
+++ b/packages/shared/src/utils/helpers/__tests__/format-response.js
@@ -1,5 +1,4 @@
import { expect } from 'chai';
-import React from 'react';
import { formatPortfolioPosition } from '../format-response';
describe('formatPortfolioPosition', () => {
diff --git a/packages/shared/src/utils/helpers/__tests__/start-date.js b/packages/shared/src/utils/helpers/__tests__/start-date.js
new file mode 100644
index 000000000000..6c143d2eac87
--- /dev/null
+++ b/packages/shared/src/utils/helpers/__tests__/start-date.js
@@ -0,0 +1,27 @@
+import { expect } from 'chai';
+import { buildForwardStartingConfig } from '../start-date';
+
+describe('start_date', () => {
+ describe('buildForwardStartingConfig', () => {
+ it('Returns empty object when forward_starting_options and forward_starting_dates are both empties', () => {
+ const contract = {
+ barrier_category: 'euro_atm',
+ barriers: 0,
+ contract_category: 'callput',
+ contract_category_display: 'Up/Down',
+ contract_display: 'Higher',
+ contract_type: 'CALL',
+ exchange_name: 'FOREX',
+ expiry_type: 'daily',
+ market: 'forex',
+ max_contract_duration: '365d',
+ min_contract_duration: '1d',
+ sentiment: 'up',
+ start_type: 'spot',
+ submarket: 'major_pairs',
+ underlying_symbol: 'frxAUDJPY',
+ };
+ expect(buildForwardStartingConfig(contract, {})).to.be.empty;
+ });
+ });
+});
diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/active-symbols.js b/packages/shared/src/utils/helpers/active-symbols.js
similarity index 97%
rename from packages/trader/src/Stores/Modules/Trading/Helpers/active-symbols.js
rename to packages/shared/src/utils/helpers/active-symbols.js
index cae4884246c1..647632944e67 100644
--- a/packages/trader/src/Stores/Modules/Trading/Helpers/active-symbols.js
+++ b/packages/shared/src/utils/helpers/active-symbols.js
@@ -1,5 +1,8 @@
import { flow } from 'mobx';
-import { LocalStore, redirectToLogin, WS } from '@deriv/shared';
+import { LocalStore } from '../storage';
+import { redirectToLogin } from '../login';
+import { WS } from '../../services';
+
import { getLanguage, localize } from '@deriv/translations';
export const showUnavailableLocationError = flow(function* (showError, is_logged_in) {
diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/barrier.js b/packages/shared/src/utils/helpers/barrier.js
similarity index 100%
rename from packages/trader/src/Stores/Modules/Trading/Helpers/barrier.js
rename to packages/shared/src/utils/helpers/barrier.js
diff --git a/packages/shared/src/utils/helpers/barriers.js b/packages/shared/src/utils/helpers/barriers.js
new file mode 100644
index 000000000000..47eec5896432
--- /dev/null
+++ b/packages/shared/src/utils/helpers/barriers.js
@@ -0,0 +1,28 @@
+import { toJS } from 'mobx';
+import { isEmptyObject } from '../object';
+import { CONTRACT_SHADES } from '../constants';
+
+export const isBarrierSupported = contract_type => contract_type in CONTRACT_SHADES;
+
+export const barriersToString = (is_relative, ...barriers_list) =>
+ barriers_list
+ .filter(barrier => barrier !== undefined && barrier !== null)
+ .map(barrier => `${is_relative && !/^[+-]/.test(barrier) ? '+' : ''}${barrier}`);
+
+export const barriersObjectToArray = (barriers, reference_array) => {
+ Object.keys(barriers).forEach(barrier => {
+ const js_object = toJS(barriers[barrier]);
+ if (!isEmptyObject(js_object)) {
+ reference_array.push(js_object);
+ }
+ });
+
+ return reference_array;
+};
+
+export const removeBarrier = (barriers, key) => {
+ const index = barriers.findIndex(b => b.key === key);
+ if (index > -1) {
+ barriers.splice(index, 1);
+ }
+};
diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/chart-notifications.js b/packages/shared/src/utils/helpers/chart-notifications.js
similarity index 100%
rename from packages/trader/src/Stores/Modules/Contract/Helpers/chart-notifications.js
rename to packages/shared/src/utils/helpers/chart-notifications.js
diff --git a/packages/trader/src/Stores/Modules/Portfolio/Helpers/details.js b/packages/shared/src/utils/helpers/details.js
similarity index 99%
rename from packages/trader/src/Stores/Modules/Portfolio/Helpers/details.js
rename to packages/shared/src/utils/helpers/details.js
index 481af3abb10b..cf1a585acaaf 100644
--- a/packages/trader/src/Stores/Modules/Portfolio/Helpers/details.js
+++ b/packages/shared/src/utils/helpers/details.js
@@ -1,4 +1,4 @@
-import { epochToMoment, formatMilliseconds, getDiffDuration } from '@deriv/shared';
+import { epochToMoment, formatMilliseconds, getDiffDuration } from '../date';
import { localize } from '@deriv/translations';
export const getDurationUnitValue = obj_duration => {
diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/duration.js b/packages/shared/src/utils/helpers/duration.js
similarity index 99%
rename from packages/trader/src/Stores/Modules/Trading/Helpers/duration.js
rename to packages/shared/src/utils/helpers/duration.js
index 6d99dd31c6eb..6cabf969565d 100644
--- a/packages/trader/src/Stores/Modules/Trading/Helpers/duration.js
+++ b/packages/shared/src/utils/helpers/duration.js
@@ -1,5 +1,5 @@
import { localize } from '@deriv/translations';
-import { toMoment } from '@deriv/shared';
+import { toMoment } from '../date';
const getDurationMaps = () => ({
t: { display: localize('Ticks'), order: 1 },
diff --git a/packages/trader/src/Stores/Modules/Portfolio/Helpers/format-response.js b/packages/shared/src/utils/helpers/format-response.js
similarity index 84%
rename from packages/trader/src/Stores/Modules/Portfolio/Helpers/format-response.js
rename to packages/shared/src/utils/helpers/format-response.js
index 7bc0836efa20..da9671ded31d 100644
--- a/packages/trader/src/Stores/Modules/Portfolio/Helpers/format-response.js
+++ b/packages/shared/src/utils/helpers/format-response.js
@@ -1,6 +1,6 @@
-import { getUnsupportedContracts } from 'Constants';
-import { getSymbolDisplayName } from 'Stores/Modules/Trading/Helpers/active-symbols';
-import { getMarketInformation } from 'Modules/Reports/Helpers/market-underlying';
+import { getUnsupportedContracts } from '../constants';
+import { getSymbolDisplayName } from './active-symbols';
+import { getMarketInformation } from './market-underlying';
const isUnSupportedContract = portfolio_pos =>
!!getUnsupportedContracts()[portfolio_pos.contract_type] || // check unsupported contract type
diff --git a/packages/shared/src/utils/helpers/index.js b/packages/shared/src/utils/helpers/index.js
new file mode 100644
index 000000000000..7b61328413af
--- /dev/null
+++ b/packages/shared/src/utils/helpers/index.js
@@ -0,0 +1,12 @@
+export * from './active-symbols';
+export * from './barrier';
+export * from './barriers';
+export * from './chart-notifications';
+export * from './details';
+export * from './duration';
+export * from './format-response';
+export * from './logic';
+export * from './market-underlying';
+export * from './portfolio-notifications';
+export * from './start-date';
+export * from './validation-rules';
diff --git a/packages/shared/src/utils/helpers/logic.js b/packages/shared/src/utils/helpers/logic.js
new file mode 100644
index 000000000000..709641fb105a
--- /dev/null
+++ b/packages/shared/src/utils/helpers/logic.js
@@ -0,0 +1,58 @@
+import moment from 'moment';
+import { isEmptyObject } from '../object';
+import { isUserSold } from '../contract';
+
+export const isContractElapsed = (contract_info, tick) => {
+ if (isEmptyObject(tick) || isEmptyObject(contract_info)) return false;
+ const end_time = getEndTime(contract_info);
+ if (end_time && tick.epoch) {
+ const seconds = moment.duration(moment.unix(tick.epoch).diff(moment.unix(end_time))).asSeconds();
+ return seconds >= 2;
+ }
+ return false;
+};
+
+export const isEndedBeforeCancellationExpired = contract_info =>
+ !!(contract_info.cancellation && getEndTime(contract_info) < contract_info.cancellation.date_expiry);
+
+export const isSoldBeforeStart = contract_info =>
+ contract_info.sell_time && +contract_info.sell_time < +contract_info.date_start;
+
+export const isStarted = contract_info =>
+ !contract_info.is_forward_starting || contract_info.current_spot_time > contract_info.date_start;
+
+export const isUserCancelled = contract_info => contract_info.status === 'cancelled';
+
+export const getEndTime = contract_info => {
+ const {
+ exit_tick_time,
+ date_expiry,
+ is_expired,
+ is_path_dependent,
+ sell_time,
+ status,
+ tick_count: is_tick_contract,
+ } = contract_info;
+
+ const is_finished = is_expired && status !== 'open';
+
+ if (!is_finished && !isUserSold(contract_info) && !isUserCancelled(contract_info)) return undefined;
+
+ if (isUserSold(contract_info)) {
+ return sell_time > date_expiry ? date_expiry : sell_time;
+ } else if (!is_tick_contract && sell_time > date_expiry) {
+ return date_expiry;
+ }
+
+ return date_expiry > exit_tick_time && !+is_path_dependent ? date_expiry : exit_tick_time;
+};
+
+export const getBuyPrice = contract_store => {
+ return contract_store.contract_info.buy_price;
+};
+
+/**
+ * Set contract update form initial values
+ * @param {object} contract_update - contract_update response
+ * @param {object} limit_order - proposal_open_contract.limit_order response
+ */
diff --git a/packages/shared/src/utils/helpers/market-underlying.js b/packages/shared/src/utils/helpers/market-underlying.js
new file mode 100644
index 000000000000..7915a889d04e
--- /dev/null
+++ b/packages/shared/src/utils/helpers/market-underlying.js
@@ -0,0 +1,30 @@
+import { getMarketNamesMap, getContractConfig } from '../constants/contract';
+
+/**
+ * Fetch market information from shortcode
+ * @param shortcode: string
+ * @returns {{underlying: string, category: string}}
+ */
+
+// TODO: Combine with extractInfoFromShortcode function in shared, both are currently used
+export const getMarketInformation = shortcode => {
+ const market_info = {
+ category: '',
+ underlying: '',
+ };
+
+ const pattern = new RegExp(
+ '^([A-Z]+)_((1HZ[0-9-V]+)|((CRASH|BOOM)[0-9\\d]+[A-Z]?)|(OTC_[A-Z0-9]+)|R_[\\d]{2,3}|[A-Z]+)'
+ );
+ const extracted = pattern.exec(shortcode);
+ if (extracted !== null) {
+ market_info.category = extracted[1].toLowerCase();
+ market_info.underlying = extracted[2];
+ }
+
+ return market_info;
+};
+
+export const getMarketName = underlying => (underlying ? getMarketNamesMap()[underlying.toUpperCase()] : null);
+
+export const getTradeTypeName = category => (category ? getContractConfig()[category.toUpperCase()].name : null);
diff --git a/packages/trader/src/Stores/Modules/Portfolio/Helpers/portfolio-notifications.js b/packages/shared/src/utils/helpers/portfolio-notifications.js
similarity index 90%
rename from packages/trader/src/Stores/Modules/Portfolio/Helpers/portfolio-notifications.js
rename to packages/shared/src/utils/helpers/portfolio-notifications.js
index 2f32588530ac..60d23a5a6394 100644
--- a/packages/trader/src/Stores/Modules/Portfolio/Helpers/portfolio-notifications.js
+++ b/packages/shared/src/utils/helpers/portfolio-notifications.js
@@ -1,8 +1,7 @@
import React from 'react';
-import { Money } from '@deriv/components';
import { localize, Localize } from '@deriv/translations';
-export const contractSold = (currency, sold_for) => ({
+export const contractSold = (currency, sold_for, Money) => ({
key: 'contract_sold',
header: localize('Contract sold'),
message: (
diff --git a/packages/shared/src/utils/helpers/start-date.js b/packages/shared/src/utils/helpers/start-date.js
new file mode 100644
index 000000000000..b1b748ed3d11
--- /dev/null
+++ b/packages/shared/src/utils/helpers/start-date.js
@@ -0,0 +1,23 @@
+import { toMoment } from '../date';
+
+export const buildForwardStartingConfig = (contract, forward_starting_dates) => {
+ const forward_starting_config = [];
+
+ if ((contract.forward_starting_options || []).length) {
+ contract.forward_starting_options.forEach(option => {
+ const duplicated_option = forward_starting_config.find(opt => opt.value === parseInt(option.date));
+ const current_session = { open: toMoment(option.open), close: toMoment(option.close) };
+ if (duplicated_option) {
+ duplicated_option.sessions.push(current_session);
+ } else {
+ forward_starting_config.push({
+ text: toMoment(option.date).format('ddd - DD MMM, YYYY'),
+ value: parseInt(option.date),
+ sessions: [current_session],
+ });
+ }
+ });
+ }
+
+ return forward_starting_config.length ? forward_starting_config : forward_starting_dates;
+};
diff --git a/packages/trader/src/Stores/Modules/Contract/Constants/validation-rules.js b/packages/shared/src/utils/helpers/validation-rules.js
similarity index 92%
rename from packages/trader/src/Stores/Modules/Contract/Constants/validation-rules.js
rename to packages/shared/src/utils/helpers/validation-rules.js
index fc370ac0b083..7f6d4ee56ec3 100644
--- a/packages/trader/src/Stores/Modules/Contract/Constants/validation-rules.js
+++ b/packages/shared/src/utils/helpers/validation-rules.js
@@ -1,8 +1,8 @@
import { localize } from '@deriv/translations';
-import { getTotalProfit } from '@deriv/shared';
-import { getBuyPrice } from 'Stores/Modules/Contract/Helpers/logic';
+import { getTotalProfit } from '../contract';
+import { getBuyPrice } from "./logic";
-const getValidationRules = () => ({
+export const getContractValidationRules = () => ({
has_contract_update_stop_loss: {
trigger: 'contract_update_stop_loss',
},
@@ -64,5 +64,3 @@ const getValidationRules = () => ({
],
},
});
-
-export default getValidationRules;
diff --git a/packages/trader/build/loaders-config.js b/packages/trader/build/loaders-config.js
index 244d0aa2d1b8..7d65dfcd1806 100644
--- a/packages/trader/build/loaders-config.js
+++ b/packages/trader/build/loaders-config.js
@@ -8,6 +8,9 @@ const js_loaders = [
{
loader: '@deriv/shared/src/loaders/deriv-account-loader.js',
},
+ {
+ loader: '@deriv/shared/src/loaders/deriv-reports-loader.js',
+ },
{
loader: 'babel-loader',
options: {
diff --git a/packages/trader/build/webpack.config.js b/packages/trader/build/webpack.config.js
index 209bbb561081..35829b550a53 100644
--- a/packages/trader/build/webpack.config.js
+++ b/packages/trader/build/webpack.config.js
@@ -46,11 +46,13 @@ module.exports = function (env) {
'@deriv/translations': '@deriv/translations',
'@deriv/deriv-charts': '@deriv/deriv-charts',
'@deriv/account': '@deriv/account',
+ '@deriv/reports': '@deriv/reports',
},
/^@deriv\/shared\/.+$/,
/^@deriv\/components\/.+$/,
/^@deriv\/translations\/.+$/,
/^@deriv\/account\/.+$/,
+ /^@deriv\/reports\/.+$/,
],
target: 'web',
plugins: plugins(base, false),
diff --git a/packages/trader/package.json b/packages/trader/package.json
index 8db946623708..0f0d519f048a 100644
--- a/packages/trader/package.json
+++ b/packages/trader/package.json
@@ -89,6 +89,7 @@
"@deriv/deriv-api": "^1.0.8",
"@deriv/deriv-charts": "^0.6.0",
"@deriv/shared": "^1.0.0",
+ "@deriv/reports": "^1.0.0",
"@deriv/translations": "^1.0.0",
"@types/classnames": "^2.2.11",
"@types/react-loadable": "^5.5.6",
diff --git a/packages/trader/src/App/Components/Elements/ContentLoader/index.js b/packages/trader/src/App/Components/Elements/ContentLoader/index.js
index 90c86e01a015..40d98f31d6ec 100644
--- a/packages/trader/src/App/Components/Elements/ContentLoader/index.js
+++ b/packages/trader/src/App/Components/Elements/ContentLoader/index.js
@@ -1,3 +1,2 @@
export * from './positions-card.jsx';
-export * from './reports-table-row.jsx';
export * from './trade-params.jsx';
diff --git a/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx b/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx
index 4955343f9392..74112d4115d8 100644
--- a/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx
+++ b/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx
@@ -9,6 +9,8 @@ import {
isMobile,
isMultiplierContract,
isUserSold,
+ isEndedBeforeCancellationExpired,
+ isUserCancelled,
} from '@deriv/shared';
import {
addCommaToNumber,
@@ -16,12 +18,8 @@ import {
getBarrierValue,
isDigitType,
} from 'App/Components/Elements/PositionsDrawer/helpers';
-import {
- isCancellationExpired,
- isEndedBeforeCancellationExpired,
- isUserCancelled,
-} from 'Stores/Modules/Contract/Helpers/logic';
import ContractAuditItem from './contract-audit-item.jsx';
+import { isCancellationExpired } from 'Stores/Modules/Trading/Helpers/logic';
const ContractDetails = ({ contract_end_time, contract_info, duration, duration_unit, exit_spot }) => {
const {
diff --git a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx
index 468dfae040c8..8e0ef54e4e56 100644
--- a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx
+++ b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx
@@ -2,15 +2,18 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { DesktopWrapper, MobileWrapper, Collapsible, ContractCard, useHover } from '@deriv/components';
-import { isCryptoContract, isDesktop } from '@deriv/shared';
+import {
+ isCryptoContract,
+ isDesktop,
+ getEndTime,
+ getSymbolDisplayName,
+} from '@deriv/shared';
import { getCardLabels, getContractTypeDisplay } from 'Constants/contract';
-import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic';
import { connect } from 'Stores/connect';
-import { getSymbolDisplayName } from 'Stores/Modules/Trading/Helpers/active-symbols';
-import { connectWithContractUpdate } from 'Stores/Modules/Contract/Helpers/multiplier';
-import { getMarketInformation } from 'Modules/Reports/Helpers/market-underlying';
+import { getMarketInformation } from 'Utils/Helpers/market-underlying';
import { SwipeableContractDrawer } from './swipeable-components.jsx';
import MarketClosedContractOverlay from './market-closed-contract-overlay.jsx';
+import { connectWithContractUpdate } from 'Stores/Modules/Trading/Helpers/multiplier';
const ContractDrawerCard = ({
active_symbols,
@@ -165,11 +168,11 @@ ContractDrawerCard.propTypes = {
status: PropTypes.string,
};
-export default connect(({ modules, ui }) => ({
+export default connect(({ modules, ui, contract_trade }) => ({
active_symbols: modules.trade.active_symbols,
addToast: ui.addToast,
current_focus: ui.current_focus,
- getContractById: modules.contract_trade.getContractById,
+ getContractById: contract_trade.getContractById,
removeToast: ui.removeToast,
should_show_cancellation_warning: ui.should_show_cancellation_warning,
setCurrentFocus: ui.setCurrentFocus,
diff --git a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx
index aeefad93ab42..0830fb67cc78 100644
--- a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx
+++ b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx
@@ -4,12 +4,17 @@ import React from 'react';
import { withRouter } from 'react-router';
import { CSSTransition } from 'react-transition-group';
import { DesktopWrapper, MobileWrapper, Div100vhContainer } from '@deriv/components';
-import { isUserSold, isMobile } from '@deriv/shared';
+import {
+ isUserSold,
+ isMobile,
+ getDurationPeriod,
+ getDurationTime,
+ getDurationUnitText,
+ getEndTime,
+} from '@deriv/shared';
import ContractAudit from 'App/Components/Elements/ContractAudit';
import { PositionsCardLoader } from 'App/Components/Elements/ContentLoader';
import { connect } from 'Stores/connect';
-import { getDurationPeriod, getDurationTime, getDurationUnitText } from 'Stores/Modules/Portfolio/Helpers/details';
-import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic';
import ContractDrawerCard from './contract-drawer-card.jsx';
import { SwipeableContractAudit } from './swipeable-components.jsx';
diff --git a/packages/trader/src/App/Components/Elements/EmptyPortfolioMessage/empty-portfolio-message.jsx b/packages/trader/src/App/Components/Elements/EmptyPortfolioMessage/empty-portfolio-message.jsx
new file mode 100644
index 000000000000..5f26070b2c88
--- /dev/null
+++ b/packages/trader/src/App/Components/Elements/EmptyPortfolioMessage/empty-portfolio-message.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Icon, Text } from '@deriv/components';
+import { localize } from '@deriv/translations';
+
+const EmptyPortfolioMessage = ({ error }) => (
+
+
+ {error ? (
+
+ {error}
+
+ ) : (
+
+
+
+ {localize(
+ 'You have no open positions for this asset. To view other open positions, click Go to Reports'
+ )}
+
+
+ )}
+
+
+);
+
+export default EmptyPortfolioMessage;
diff --git a/packages/trader/src/App/Components/Elements/EmptyPortfolioMessage/index.js b/packages/trader/src/App/Components/Elements/EmptyPortfolioMessage/index.js
new file mode 100644
index 000000000000..d498bb957778
--- /dev/null
+++ b/packages/trader/src/App/Components/Elements/EmptyPortfolioMessage/index.js
@@ -0,0 +1,3 @@
+import EmptyPortfolioMessage from './empty-portfolio-message.jsx';
+
+export default EmptyPortfolioMessage;
diff --git a/packages/trader/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js b/packages/trader/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js
index 96803078b143..b01b4ce5d0ff 100644
--- a/packages/trader/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js
+++ b/packages/trader/src/App/Components/Elements/PositionsDrawer/helpers/positions-helper.js
@@ -1,7 +1,5 @@
import { localize } from '@deriv/translations';
-import { isHighLow } from '@deriv/shared';
-import { getContractTypesConfig } from 'Stores/Modules/Trading/Constants/contract';
-import { isCallPut } from 'Stores/Modules/Contract/Helpers/contract-type';
+import { isHighLow, getContractTypesConfig, isCallPut } from '@deriv/shared';
export const addCommaToNumber = (num, decimal_places) => {
if (!num || isNaN(num)) {
diff --git a/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx b/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx
index 10548aa88abc..5c45a6a565da 100644
--- a/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx
+++ b/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx
@@ -7,12 +7,12 @@ import { CSSTransition } from 'react-transition-group';
import { Icon, DataList, Text } from '@deriv/components';
import { routes, useNewRowTransition } from '@deriv/shared';
import { localize } from '@deriv/translations';
-import EmptyPortfolioMessage from 'Modules/Reports/Components/empty-portfolio-message.jsx';
+import EmptyPortfolioMessage from '../EmptyPortfolioMessage';
import { connect } from 'Stores/connect';
-import PositionsDrawerCard from './PositionsDrawerCard';
+import { PositionsDrawerCard } from '@deriv/reports';
import { filterByContractType } from './helpers';
-const PositionsDrawerCardItem = ({ row: portfolio_position, measure, onHoverPosition, is_new_row }) => {
+const PositionsDrawerCardItem = ({ row: portfolio_position, measure, onHoverPosition, is_new_row, ...props }) => {
const { in_prop } = useNewRowTransition(is_new_row);
React.useEffect(() => {
@@ -43,6 +43,7 @@ const PositionsDrawerCardItem = ({ row: portfolio_position, measure, onHoverPosi
}}
onFooterEntered={measure}
should_show_transition={is_new_row}
+ {...props}
/>
@@ -58,6 +59,7 @@ const PositionsDrawer = ({
toggleDrawer,
trade_contract_type,
onMount,
+ ...props
}) => {
const drawer_ref = React.useRef(null);
const list_ref = React.useRef(null);
@@ -82,7 +84,7 @@ const PositionsDrawer = ({
const body_content = (