diff --git a/.circleci/config.yml b/.circleci/config.yml index 5158e0f7e8a0..689143c5af23 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,6 +69,7 @@ commands: - "packages/cfd/node_modules" - "packages/indicators/node_modules" - "packages/p2p/node_modules" + - "packages/reports/node_modules" - "packages/shared/node_modules" - "packages/trader/node_modules" - "packages/translations/node_modules" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 76bcee8f568a..755e13cfd5e6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -119,7 +119,11 @@ /packages/trader/**/* @matin-binary @balakrishna-binary @akmal-binary +# ============================================================== +# deriv-app/reports +# ============================================================== +/packages/reports/**/* @matin-binary @balakrishna-binary # ============================================================== # deriv-app/publisher diff --git a/package.json b/package.json index 7db54af4178d..56fda1e51417 100644 --- a/package.json +++ b/package.json @@ -62,10 +62,10 @@ "build": "f () { lerna exec --scope=@deriv/${1:-'*'} -- npm run build $2 ;}; f", "build:publish": "f () { lerna exec --scope=@deriv/${1:-'*'} -- npm run build:publish $2 ;}; f", "build:local": "f () { lerna exec --scope @deriv/trader --scope @deriv/bot-web-ui --parallel -- npm run build $1 ;}; f", - "build:travis": "lerna exec --scope @deriv/shared --scope @deriv/components --scope @deriv/translations --scope @deriv/cashier --scope @deriv/account --scope @deriv/p2p --scope @deriv/cfd -- npm run build:travis", + "build:travis": "lerna exec --scope @deriv/shared --scope @deriv/components --scope @deriv/translations --scope @deriv/cashier --scope @deriv/account --scope @deriv/p2p --scope @deriv/cfd --scope @deriv/reports -- npm run build:travis", "build:prod": "export NODE_ENV=staging && npm run build && export NODE_ENV=", "build:storybook": "cd packages/components && build-storybook --output-dir .out", - "build:gh-pages": "f () { lerna exec --scope @deriv/components --scope @deriv/p2p -- npm run build && lerna exec --scope @deriv/cashier --scope @deriv/account --scope @deriv/cfd -- npm run build $1 && npm run build:local $1 ;}; f", + "build:gh-pages": "f () { lerna exec --scope @deriv/components --scope @deriv/p2p -- npm run build && lerna exec --scope @deriv/cashier --scope @deriv/account --scope @deriv/cfd --scope @deriv/reports -- npm run build $1 && npm run build:local $1 ;}; f", "clean": "echo \"Remove $(git rev-parse --show-toplevel)/node_modules\" && lerna clean && rm -rf \"$(git rev-parse --show-toplevel)/node_modules\"", "deploy": "f () { npm run build:travis && npm run build:local && lerna exec --scope @deriv/core -- npm run deploy $@ ;}; f", "deploy:clean": "f () { npm run build:travis && npm run build:local && lerna exec --scope @deriv/core -- npm run deploy:clean $@ ;}; f", diff --git a/packages/cfd/src/Containers/cfd-password-manager-modal.tsx b/packages/cfd/src/Containers/cfd-password-manager-modal.tsx index 3354fe4d52b3..390e9b72c066 100644 --- a/packages/cfd/src/Containers/cfd-password-manager-modal.tsx +++ b/packages/cfd/src/Containers/cfd-password-manager-modal.tsx @@ -52,7 +52,6 @@ const CountdownComponent = ({ count_from = 60, onTimeout }: TCountdownComponent) } onTimeout(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [count]); return {count}; diff --git a/packages/cfd/src/Containers/cfd-password-modal.tsx b/packages/cfd/src/Containers/cfd-password-modal.tsx index b6bab62c18a3..bd945c2fdfff 100644 --- a/packages/cfd/src/Containers/cfd-password-modal.tsx +++ b/packages/cfd/src/Containers/cfd-password-modal.tsx @@ -275,8 +275,7 @@ const getCancelButtonLabel = ({ const handlePasswordInputChange = ( e: React.ChangeEvent, - // eslint-disable-next-line no-shadow - handleChange: (e: React.ChangeEvent) => void, + handleChange: (el: React.ChangeEvent) => void, validateForm: (values?: TCFDPasswordFormValues) => Promise>, setFieldTouched: (field: string, isTouched?: boolean, shouldValidate?: boolean) => void ) => { diff --git a/packages/components/src/components/contract-card/contract-card-items/contract-update-form.jsx b/packages/components/src/components/contract-card/contract-card-items/contract-update-form.jsx index 3af32aa3a712..6bd9097f114a 100644 --- a/packages/components/src/components/contract-card/contract-card-items/contract-update-form.jsx +++ b/packages/components/src/components/contract-card/contract-card-items/contract-update-form.jsx @@ -44,6 +44,11 @@ const ContractUpdateForm = props => { validation_errors, } = contract; + const [contract_profit_or_loss, setContractProfitOrLoss] = React.useState({ + contract_update_take_profit, + contract_update_stop_loss, + }); + const { buy_price, currency, is_valid_to_cancel, is_sold } = contract_info; const { stop_loss, take_profit } = getLimitOrderAmount(contract_info.limit_order); const { contract_update_stop_loss: stop_loss_error, contract_update_take_profit: take_profit_error } = @@ -77,7 +82,10 @@ const ContractUpdateForm = props => { const onChange = e => { const { name, value } = e.target; - + setContractProfitOrLoss({ + ...contract_profit_or_loss, + [name]: value, + }); if (typeof contract.onChange === 'function') { contract.onChange({ name, @@ -107,7 +115,7 @@ const ContractUpdateForm = props => { name='contract_update_take_profit' onChange={onChange} error_message_alignment={error_message_alignment || 'right'} - value={contract_update_take_profit} + value={contract_profit_or_loss.contract_update_take_profit} is_disabled={!!is_valid_to_cancel} setCurrentFocus={setCurrentFocus} /> @@ -130,7 +138,7 @@ const ContractUpdateForm = props => { name='contract_update_stop_loss' onChange={onChange} error_message_alignment={error_message_alignment || 'right'} - value={contract_update_stop_loss} + value={contract_profit_or_loss.contract_update_stop_loss} is_disabled={!!is_valid_to_cancel} setCurrentFocus={setCurrentFocus} /> diff --git a/packages/core/build/config.js b/packages/core/build/config.js index 4ec7e675cfcd..9250dbe65b29 100644 --- a/packages/core/build/config.js +++ b/packages/core/build/config.js @@ -49,6 +49,14 @@ const copyConfig = base => { from: path.resolve(__dirname, '../node_modules/@deriv/trader/dist/trader'), to: 'trader', }, + { + from: path.resolve(__dirname, '../node_modules/@deriv/reports/dist/reports/js/'), + to: 'reports/js', + }, + { + from: path.resolve(__dirname, '../node_modules/@deriv/reports/dist/reports/css/'), + to: 'reports/css', + }, { from: path.resolve(__dirname, '../node_modules/@deriv/cfd/dist/cfd'), to: 'cfd', diff --git a/packages/core/package.json b/packages/core/package.json index 84c26327a46b..4da6a1fd21c1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -102,6 +102,7 @@ "@deriv/deriv-api": "^1.0.8", "@deriv/deriv-charts": "^0.6.0", "@deriv/p2p": "^0.7.3", + "@deriv/reports": "^1.0.0", "@deriv/shared": "^1.0.0", "@deriv/trader": "^3.8.0", "@deriv/translations": "^1.0.0", @@ -116,6 +117,7 @@ "js-cookie": "^2.2.1", "loadjs": "^4.2.0", "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", "mobx": "^5.15.7", "mobx-react": "6.3.1", "mobx-utils": "^5.5.5", diff --git a/packages/core/src/App/Constants/routes-config.js b/packages/core/src/App/Constants/routes-config.js index 7c2f76bfaac4..5f094bc90d10 100644 --- a/packages/core/src/App/Constants/routes-config.js +++ b/packages/core/src/App/Constants/routes-config.js @@ -16,6 +16,11 @@ const Trader = React.lazy(() => }) ); +const Reports = React.lazy(() => { + // eslint-disable-next-line import/no-unresolved + return import(/* webpackChunkName: "reports" */ '@deriv/reports'); +}); + const CFD = React.lazy(() => moduleLoader(() => { // eslint-disable-next-line import/no-unresolved @@ -59,6 +64,34 @@ const getModules = ({ is_appstore }, is_social_signup) => { // Don't use `Localize` component since native html tag like `option` cannot render them getTitle: () => localize('Bot'), }, + { + path: routes.reports, + component: Reports, + getTitle: () => localize('Reports'), + icon_component: 'IcReports', + is_authenticated: true, + routes: [ + { + path: routes.positions, + component: Reports, + getTitle: () => localize('Open positions'), + icon_component: 'IcOpenPositions', + default: true, + }, + { + path: routes.profit, + component: Reports, + getTitle: () => localize('Profit table'), + icon_component: 'IcProfitTable', + }, + { + path: routes.statement, + component: Reports, + getTitle: () => localize('Statement'), + icon_component: 'IcStatement', + }, + ], + }, { path: routes.dxtrade, component: props => , @@ -235,34 +268,6 @@ const getModules = ({ is_appstore }, is_social_signup) => { component: Trader, getTitle: () => localize('Trader'), routes: [ - { - path: routes.reports, - component: Trader, - getTitle: () => localize('Reports'), - icon_component: 'IcReports', - is_authenticated: true, - routes: [ - { - path: routes.positions, - component: Trader, - getTitle: () => localize('Open positions'), - icon_component: 'IcOpenPositions', - default: true, - }, - { - path: routes.profit, - component: Trader, - getTitle: () => localize('Profit table'), - icon_component: 'IcProfitTable', - }, - { - path: routes.statement, - component: Trader, - getTitle: () => localize('Statement'), - icon_component: 'IcStatement', - }, - ], - }, { path: routes.contract, component: Trader, diff --git a/packages/core/src/Components/markers/__tests__/marker-spot-label.spec.js b/packages/core/src/Components/markers/__tests__/marker-spot-label.spec.js new file mode 100644 index 000000000000..26403c0342da --- /dev/null +++ b/packages/core/src/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/core/src/Components/markers/__tests__/marker-spot.spec.js b/packages/core/src/Components/markers/__tests__/marker-spot.spec.js new file mode 100644 index 000000000000..4327cb046a54 --- /dev/null +++ b/packages/core/src/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/core/src/Components/markers/marker-line.jsx b/packages/core/src/Components/markers/marker-line.jsx new file mode 100644 index 000000000000..c4a5c7083a7e --- /dev/null +++ b/packages/core/src/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/core/src/Components/markers/marker-spot-label.jsx b/packages/core/src/Components/markers/marker-spot-label.jsx new file mode 100644 index 000000000000..bfcb77a258f9 --- /dev/null +++ b/packages/core/src/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/core/src/Components/markers/marker-spot.jsx b/packages/core/src/Components/markers/marker-spot.jsx new file mode 100644 index 000000000000..a242334713b6 --- /dev/null +++ b/packages/core/src/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/core/src/Stores/Constants/markers.js b/packages/core/src/Stores/Constants/markers.js new file mode 100644 index 000000000000..8441b64d016d --- /dev/null +++ b/packages/core/src/Stores/Constants/markers.js @@ -0,0 +1,59 @@ +import { localize } from '@deriv/translations'; +import MarkerLine from '../../Components/markers/marker-line'; +import MarkerSpotLabel from '../../Components/markers/marker-spot-label'; +import MarkerSpot from '../../Components/markers/marker-spot'; + +export const MARKER_TYPES_CONFIG = { + LINE_END: { + type: 'LINE_END', + marker_config: { + ContentComponent: MarkerLine, + className: 'chart-marker-line', + }, + content_config: { line_style: 'dash', label: localize('End Time') }, + }, + LINE_PURCHASE: { + type: 'LINE_PURCHASE', + marker_config: { + ContentComponent: MarkerLine, + className: 'chart-marker-line', + }, + content_config: { line_style: 'solid', label: localize('Purchase Time') }, + }, + LINE_START: { + type: 'LINE_START', + marker_config: { + ContentComponent: MarkerLine, + className: 'chart-marker-line', + }, + content_config: { line_style: 'solid', label: localize('Start Time') }, + }, + SPOT_ENTRY: { + type: 'SPOT_ENTRY', + marker_config: { + ContentComponent: MarkerSpot, + }, + content_config: { className: 'chart-spot__entry' }, + }, + SPOT_SELL: { + type: 'SPOT_SELL', + marker_config: { + ContentComponent: MarkerSpot, + }, + content_config: { className: 'chart-spot__sell' }, + }, + SPOT_EXIT: { + type: 'SPOT_EXIT', + marker_config: { + ContentComponent: MarkerSpotLabel, + }, + content_config: { spot_className: 'chart-spot__spot' }, + }, + SPOT_MIDDLE: { + type: 'SPOT_MIDDLE', + marker_config: { + ContentComponent: MarkerSpotLabel, + }, + content_config: { spot_className: 'chart-spot__spot' }, + }, +}; diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/chart-marker-helpers.js b/packages/core/src/Stores/Helpers/chart-marker-helpers.js similarity index 94% rename from packages/trader/src/Stores/Modules/Contract/Helpers/chart-marker-helpers.js rename to packages/core/src/Stores/Helpers/chart-marker-helpers.js index debc2f7eb069..c4bca3611d39 100644 --- a/packages/trader/src/Stores/Modules/Contract/Helpers/chart-marker-helpers.js +++ b/packages/core/src/Stores/Helpers/chart-marker-helpers.js @@ -1,7 +1,7 @@ import extend from 'extend'; -import { isMobile, isUserSold, isMultiplierContract, isDigitContract } from '@deriv/shared'; -import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic'; -import { MARKER_TYPES_CONFIG } from '../../SmartChart/Constants/markers'; +import { isUserSold, isMultiplierContract, isDigitContract, getEndTime, isMobile } from '@deriv/shared'; + +import { MARKER_TYPES_CONFIG } from '../Constants/markers'; const createMarkerConfig = (marker_type, x, y, content_config) => extend(true, {}, MARKER_TYPES_CONFIG[marker_type], { diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/chart-markers.js b/packages/core/src/Stores/Helpers/chart-markers.js similarity index 94% rename from packages/trader/src/Stores/Modules/Contract/Helpers/chart-markers.js rename to packages/core/src/Stores/Helpers/chart-markers.js index d1681b471b68..02ebb3291ab4 100644 --- a/packages/trader/src/Stores/Modules/Contract/Helpers/chart-markers.js +++ b/packages/core/src/Stores/Helpers/chart-markers.js @@ -1,4 +1,3 @@ -import { unique } from '@deriv/shared'; import { createMarkerEndTime, createMarkerPurchaseTime, @@ -8,8 +7,9 @@ import { createMarkerSpotMiddle, getSpotCount, } from './chart-marker-helpers'; -import { getChartType, getEndTime } from './logic'; -import { MARKER_TYPES_CONFIG } from '../../SmartChart/Constants/markers'; +import { getEndTime, unique } from '@deriv/shared'; +import { MARKER_TYPES_CONFIG } from '../Constants/markers'; +import { getChartType } from './logic'; export const createChartMarkers = contract_info => { let markers = []; diff --git a/packages/core/src/Stores/Helpers/limit-orders.js b/packages/core/src/Stores/Helpers/limit-orders.js new file mode 100644 index 000000000000..aaa5044ae27c --- /dev/null +++ b/packages/core/src/Stores/Helpers/limit-orders.js @@ -0,0 +1,98 @@ +import { ChartBarrierStore } from '../chart-barrier-store'; +import { isMultiplierContract, BARRIER_COLORS, BARRIER_LINE_STYLES, removeBarrier } from '@deriv/shared'; + +const isLimitOrderBarrierSupported = (contract_type, contract_info) => + isMultiplierContract(contract_type) && contract_info.limit_order; + +export const LIMIT_ORDER_TYPES = { + STOP_OUT: 'stop_out', + TAKE_PROFIT: 'take_profit', + STOP_LOSS: 'stop_loss', +}; + +export const setLimitOrderBarriers = ({ barriers, contract_type, contract_info = {}, is_over }) => { + if (is_over && isLimitOrderBarrierSupported(contract_type, contract_info)) { + const limit_orders = Object.values(LIMIT_ORDER_TYPES); + const has_stop_loss = Object.keys(contract_info.limit_order).some( + k => k === LIMIT_ORDER_TYPES.STOP_LOSS && contract_info.limit_order[k].value + ); + + limit_orders.forEach(key => { + const obj_limit_order = contract_info.limit_order[key]; + + if (!obj_limit_order || !obj_limit_order.value) { + removeBarrier(barriers, key); + return; + } + + let barrier = barriers.find(b => b.key === key); + + if (barrier) { + if (barrier.high === +obj_limit_order.value) { + return; + } + + barrier.onChange({ + high: obj_limit_order.value, + }); + } else { + const obj_barrier = { + key, + title: `${obj_limit_order.display_name}`, + color: key === LIMIT_ORDER_TYPES.TAKE_PROFIT ? BARRIER_COLORS.GREEN : BARRIER_COLORS.ORANGE, + draggable: false, + lineStyle: + key === LIMIT_ORDER_TYPES.STOP_OUT ? BARRIER_LINE_STYLES.DOTTED : BARRIER_LINE_STYLES.SOLID, + hidePriceLines: has_stop_loss && key === LIMIT_ORDER_TYPES.STOP_OUT, + hideOffscreenLine: true, + showOffscreenArrows: true, + isSingleBarrier: true, + opacityOnOverlap: key === LIMIT_ORDER_TYPES.STOP_OUT && 0.15, + }; + barrier = new ChartBarrierStore(obj_limit_order.value); + + Object.assign(barrier, obj_barrier); + barriers.push(barrier); + } + }); + } else { + const limit_orders = Object.values(LIMIT_ORDER_TYPES); + limit_orders.forEach(l => removeBarrier(barriers, l)); + } +}; + +/** + * Get limit_order for contract_update API + * @param {object} contract_update - contract_update input & checkbox values + */ +export const getLimitOrder = contract_update => { + const { + has_contract_update_stop_loss, + has_contract_update_take_profit, + contract_update_stop_loss, + contract_update_take_profit, + contract_info, + } = contract_update; + + const limit_order = {}; + + const new_take_profit = has_contract_update_take_profit ? +contract_update_take_profit : null; + const has_take_profit_changed = + Math.abs(contract_info.limit_order?.take_profit?.order_amount) !== Math.abs(new_take_profit); + + if (has_take_profit_changed) { + // send positive take_profit to update or null cancel + limit_order.take_profit = new_take_profit; + } + + const new_stop_loss = has_contract_update_stop_loss ? +contract_update_stop_loss : null; + const has_stop_loss_changed = + Math.abs(contract_info.limit_order?.stop_loss?.order_amount) !== Math.abs(new_stop_loss); + + if (has_stop_loss_changed) { + // send positive stop_loss to update or null to cancel + limit_order.stop_loss = new_stop_loss; + } + + return limit_order; +}; diff --git a/packages/core/src/Stores/Helpers/logic.js b/packages/core/src/Stores/Helpers/logic.js new file mode 100644 index 000000000000..c6b563500fa9 --- /dev/null +++ b/packages/core/src/Stores/Helpers/logic.js @@ -0,0 +1,52 @@ +import moment from 'moment'; +import { isEmptyObject, getEndTime } from '@deriv/shared'; +import ServerTime from '../../_common/base/server_time'; + +export const getChartConfig = contract_info => { + if (isEmptyObject(contract_info)) return null; + const start = contract_info.date_start; + const end = getEndTime(contract_info); + const granularity = getChartGranularity(start, end || null); + const chart_type = getChartType(start, end || null); + + return { + chart_type: contract_info.tick_count ? 'mountain' : chart_type, + granularity: contract_info.tick_count ? 0 : granularity, + end_epoch: end, + start_epoch: start, + scroll_to_epoch: contract_info.purchase_time, + }; +}; + +const hour_to_granularity_map = [ + [1, 0], + [2, 120], + [6, 600], + [24, 900], + [5 * 24, 3600], + [30 * 24, 14400], +]; + +const getExpiryTime = time => time || ServerTime.get().unix(); + +export const getChartType = (start_time, expiry_time) => { + const duration = moment.duration(moment.unix(getExpiryTime(expiry_time)).diff(moment.unix(start_time))).asHours(); + // use line chart if duration is equal or less than 1 hour + return duration <= 1 ? 'mountain' : 'candle'; +}; + +export const getChartGranularity = (start_time, expiry_time) => + calculateGranularity(getExpiryTime(expiry_time) - start_time); + +export const calculateGranularity = duration => + (hour_to_granularity_map.find(m => duration <= m[0] * 3600) || [null, 86400])[1]; + +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/core/src/Stores/active-symbols-store.js b/packages/core/src/Stores/active-symbols-store.js new file mode 100644 index 000000000000..9ac0e9d7dc38 --- /dev/null +++ b/packages/core/src/Stores/active-symbols-store.js @@ -0,0 +1,19 @@ +import { WS } from '@deriv/shared'; +import { observable, action, runInAction } from 'mobx'; +import BaseStore from './base-store'; + +export default class ActiveSymbolsStore extends BaseStore { + @observable active_symbols = []; + + @action.bound + async setActiveSymbols() { + const { active_symbols, error } = await WS.authorized.activeSymbols(); + runInAction(() => { + if (!active_symbols.length || error) { + this.active_symbols = []; + return; + } + this.active_symbols = active_symbols; + }); + } +} diff --git a/packages/core/src/Stores/chart-barrier-store.js b/packages/core/src/Stores/chart-barrier-store.js new file mode 100644 index 000000000000..1912722c65a6 --- /dev/null +++ b/packages/core/src/Stores/chart-barrier-store.js @@ -0,0 +1,79 @@ +import { action, computed, observable } from 'mobx'; +import { BARRIER_COLORS, BARRIER_LINE_STYLES, CONTRACT_SHADES, DEFAULT_SHADES, barriersToString } from '@deriv/shared'; + +export class ChartBarrierStore { + @observable color; + @observable lineStyle; + @observable shade; + @observable shadeColor; + + @observable high; + @observable low; + + @observable relative; + @observable draggable; + + @observable hidePriceLines; + @observable hideBarrierLine; + @observable hideOffscreenLine; + @observable title; + + onChartBarrierChange; + + constructor(high_barrier, low_barrier, onChartBarrierChange = null, { color, line_style, not_draggable } = {}) { + this.color = color; + this.lineStyle = line_style || BARRIER_LINE_STYLES.SOLID; + this.onChange = this.onBarrierChange; + + // trade_store's action to process new barriers on dragged + this.onChartBarrierChange = + typeof onChartBarrierChange === 'function' ? onChartBarrierChange.bind(this) : () => {}; + + this.high = +high_barrier || 0; // 0 to follow the price + if (low_barrier) { + this.low = +low_barrier; + } + + this.shade = this.default_shade; + + const has_barrier = !!high_barrier; + this.relative = !has_barrier || /^[+-]/.test(high_barrier); + this.draggable = !not_draggable && has_barrier; + this.hidePriceLines = !has_barrier; + } + + @action.bound + updateBarriers(high, low, isFromChart = false) { + if (!isFromChart) { + this.relative = /^[+-]/.test(high); + } + this.high = +high || undefined; + this.low = +low || undefined; + } + + @action.bound + updateBarrierShade(should_display, contract_type) { + this.shade = (should_display && CONTRACT_SHADES[contract_type]) || this.default_shade; + } + + @action.bound + onBarrierChange({ high, low }) { + this.updateBarriers(high, low, true); + this.onChartBarrierChange(...barriersToString(this.relative, high, low)); + } + + @action.bound + updateBarrierColor(is_dark_mode) { + this.color = is_dark_mode ? BARRIER_COLORS.DARK_GRAY : BARRIER_COLORS.GRAY; + } + + @computed + get barrier_count() { + return (typeof this.high !== 'undefined') + (typeof this.low !== 'undefined'); + } + + @computed + get default_shade() { + return DEFAULT_SHADES[this.barrier_count]; + } +} diff --git a/packages/trader/src/Stores/Modules/Contract/contract-replay-store.js b/packages/core/src/Stores/contract-replay-store.js similarity index 96% rename from packages/trader/src/Stores/Modules/Contract/contract-replay-store.js rename to packages/core/src/Stores/contract-replay-store.js index a3150859cf5f..aa403aed5364 100644 --- a/packages/trader/src/Stores/Modules/Contract/contract-replay-store.js +++ b/packages/core/src/Stores/contract-replay-store.js @@ -1,9 +1,9 @@ import { action, observable } from 'mobx'; -import { routes, isEmptyObject, isForwardStarting, WS } from '@deriv/shared'; +import { routes, isEmptyObject, isForwardStarting, WS, contractCancelled, contractSold } from '@deriv/shared'; +import { Money } from '@deriv/components'; import { localize } from '@deriv/translations'; import ContractStore from './contract-store'; -import { contractCancelled, contractSold } from '../Portfolio/Helpers/portfolio-notifications'; -import BaseStore from '../../base-store'; +import BaseStore from './base-store'; export default class ContractReplayStore extends BaseStore { @observable chart_state = ''; @@ -36,6 +36,14 @@ export default class ContractReplayStore extends BaseStore { // Forget old proposal_open_contract stream on account switch from ErrorComponent should_forget_first = false; + constructor(root_store) { + super({ + root_store, + }); + + this.root_store = root_store; + } + // ------------------- // ----- Actions ----- // ------------------- @@ -250,7 +258,7 @@ export default class ContractReplayStore extends BaseStore { transaction_id: response.sell.transaction_id, }; this.root_store.notifications.addNotificationMessage( - contractSold(this.root_store.client.currency, response.sell.sold_for) + contractSold(this.root_store.client.currency, response.sell.sold_for, Money) ); } } diff --git a/packages/trader/src/Stores/Modules/Contract/contract-store.js b/packages/core/src/Stores/contract-store.js similarity index 92% rename from packages/trader/src/Stores/Modules/Contract/contract-store.js rename to packages/core/src/Stores/contract-store.js index 94f5bfe8457a..541dcefd66e4 100644 --- a/packages/trader/src/Stores/Modules/Contract/contract-store.js +++ b/packages/core/src/Stores/contract-store.js @@ -7,21 +7,24 @@ import { getDigitInfo, getDisplayStatus, WS, + getContractUpdateConfig, + getContractValidationRules, + BARRIER_COLORS, + BARRIER_LINE_STYLES, + isBarrierSupported, + getEndTime, } from '@deriv/shared'; -import { createChartMarkers } from './Helpers/chart-markers'; +import { getChartConfig } from './Helpers/logic'; import { setLimitOrderBarriers, getLimitOrder } from './Helpers/limit-orders'; -import { getChartConfig, getContractUpdateConfig, getEndTime } from './Helpers/logic'; -import getValidationRules from './Constants/validation-rules'; -import BaseStore from '../../base-store'; -import { BARRIER_COLORS, BARRIER_LINE_STYLES } from '../SmartChart/Constants/barriers'; -import { isBarrierSupported } from '../SmartChart/Helpers/barriers'; -import { ChartBarrierStore } from '../SmartChart/chart-barrier-store'; +import { ChartBarrierStore } from './chart-barrier-store'; +import { createChartMarkers } from './Helpers/chart-markers'; +import BaseStore from './base-store'; export default class ContractStore extends BaseStore { constructor(root_store, { contract_id }) { super({ root_store, - validation_rules: getValidationRules(), + validation_rules: getContractValidationRules(), }); this.root_store = root_store; @@ -119,7 +122,7 @@ export default class ContractStore extends BaseStore { @action.bound populateContractUpdateHistory({ contract_update_history }) { - this.root_store.modules.contract_replay.contract_store.contract_update_history = contract_update_history.sort( + this.root_store.contract_replay.contract_store.contract_update_history = contract_update_history.sort( (a, b) => b.order_date - a.order_date ); } @@ -140,7 +143,7 @@ export default class ContractStore extends BaseStore { } if ( contract_info.contract_id && - contract_info.contract_id === this.root_store.modules.contract_replay.contract_id + contract_info.contract_id === this.root_store.contract_replay.contract_id ) { setLimitOrderBarriers({ barriers: this.barriers_array, @@ -205,7 +208,7 @@ export default class ContractStore extends BaseStore { } // Update portfolio store - this.root_store.modules.portfolio.populateContractUpdate(response, this.contract_id); + this.root_store.portfolio.populateContractUpdate(response, this.contract_id); }); } } diff --git a/packages/trader/src/Stores/Modules/Contract/contract-trade-store.js b/packages/core/src/Stores/contract-trade-store.js similarity index 91% rename from packages/trader/src/Stores/Modules/Contract/contract-trade-store.js rename to packages/core/src/Stores/contract-trade-store.js index a859122f4519..7d97c491a46b 100644 --- a/packages/trader/src/Stores/Modules/Contract/contract-trade-store.js +++ b/packages/core/src/Stores/contract-trade-store.js @@ -1,10 +1,16 @@ import { action, computed, observable, toJS } from 'mobx'; -import { isDesktop, isEnded, isMultiplierContract, LocalStore } from '@deriv/shared'; -import { switch_to_tick_chart } from './Helpers/chart-notifications'; +import { + isDesktop, + isEnded, + isMultiplierContract, + LocalStore, + switch_to_tick_chart, + isCallPut, + getContractTypesConfig, +} from '@deriv/shared'; import ContractStore from './contract-store'; -import { isCallPut } from './Helpers/contract-type'; -import { getContractTypesConfig } from '../Trading/Constants/contract'; -import BaseStore from '../../base-store'; + +import BaseStore from './base-store'; export default class ContractTradeStore extends BaseStore { // --- Observable properties --- @@ -17,11 +23,12 @@ export default class ContractTradeStore extends BaseStore { @observable granularity = +LocalStore.get('contract_trade.granularity') || 0; @observable chart_type = LocalStore.get('contract_trade.chart_type') || 'mountain'; - constructor({ root_store }) { + constructor(root_store) { super({ root_store, }); + this.root_store = root_store; this.onSwitchAccount(this.accountSwitchListener); } @@ -49,7 +56,8 @@ export default class ContractTradeStore extends BaseStore { } applicable_contracts = () => { - const { symbol: underlying, contract_type: trade_type } = this.root_store.modules.trade; + const { symbol: underlying, contract_type: trade_type } = JSON.parse(localStorage.getItem('trade_store')) || {}; + if (!trade_type || !underlying) { return []; } @@ -125,7 +133,7 @@ export default class ContractTradeStore extends BaseStore { this.contracts.push(contract); this.contracts_map[contract_id] = contract; - if (is_tick_contract && !this.root_store.modules.trade.is_multiplier && this.granularity !== 0 && isDesktop()) { + if (is_tick_contract && !this.root_store.portfolio.is_multiplier && this.granularity !== 0 && isDesktop()) { this.root_store.notifications.addNotificationMessage(switch_to_tick_chart); } } @@ -189,7 +197,7 @@ export default class ContractTradeStore extends BaseStore { return ( this.contracts_map[contract_id] || // or get contract from contract_replay store when user is on the contract details page - this.root_store.modules.contract_replay.contract_store + this.root_store.contract_replay.contract_store ); } } diff --git a/packages/core/src/Stores/index.js b/packages/core/src/Stores/index.js index 6c9cb40281a6..8f627e04468f 100644 --- a/packages/core/src/Stores/index.js +++ b/packages/core/src/Stores/index.js @@ -7,6 +7,11 @@ import ModulesStore from './Modules'; import MenuStore from './menu-store'; import NotificationStore from './notification-store'; import UIStore from './ui-store'; +import ActiveSymbolsStore from './active-symbols-store'; +import PortfolioStore from './portfolio-store'; +import ContractReplayStore from './contract-replay-store'; +import ContractTradeStore from './contract-trade-store'; +import { ChartBarrierStore } from './chart-barrier-store'; export default class RootStore { constructor() { @@ -19,5 +24,10 @@ export default class RootStore { this.menu = new MenuStore(this); this.pushwoosh = new PushWooshStore(this); this.notifications = new NotificationStore(this); + this.active_symbols = new ActiveSymbolsStore(this); + this.portfolio = new PortfolioStore(this); + this.contract_replay = new ContractReplayStore(this); + this.contract_trade = new ContractTradeStore(this); + this.chart_barrier_store = new ChartBarrierStore(this); } } diff --git a/packages/trader/src/Stores/Modules/Portfolio/portfolio-store.js b/packages/core/src/Stores/portfolio-store.js similarity index 82% rename from packages/trader/src/Stores/Modules/Portfolio/portfolio-store.js rename to packages/core/src/Stores/portfolio-store.js index 14b3269ecd34..a6d1abb0ca95 100644 --- a/packages/trader/src/Stores/Modules/Portfolio/portfolio-store.js +++ b/packages/core/src/Stores/portfolio-store.js @@ -10,13 +10,20 @@ import { getCurrentTick, getDisplayStatus, WS, + formatPortfolioPosition, + contractCancelled, + contractSold, + getDurationPeriod, + getDurationTime, + getDurationUnitText, + getEndTime, + removeBarrier, } from '@deriv/shared'; -import { formatPortfolioPosition } from './Helpers/format-response'; -import { contractCancelled, contractSold } from './Helpers/portfolio-notifications'; -import { getDurationPeriod, getDurationTime, getDurationUnitText } from './Helpers/details'; -import { getEndTime } from '../Contract/Helpers/logic'; +import { Money } from '@deriv/components'; +import { ChartBarrierStore } from './chart-barrier-store'; +import { setLimitOrderBarriers } from './Helpers/limit-orders'; -import BaseStore from '../../base-store'; +import BaseStore from './base-store'; export default class PortfolioStore extends BaseStore { @observable.shallow positions = []; @@ -24,12 +31,25 @@ export default class PortfolioStore extends BaseStore { positions_map = {}; @observable is_loading = false; @observable error = ''; + + // barriers + @observable barriers = []; + @observable main_barrier = null; + @observable contract_type = ''; + getPositionById = createTransformer(id => this.positions.find(position => +position.id === +id)); responseQueue = []; @observable.shallow active_positions = []; + constructor(root_store) { + super({ + root_store, + }); + + this.root_store = root_store; + } @action.bound async initializePortfolio() { if (this.has_subscribed_to_poc_and_transaction) { @@ -66,7 +86,7 @@ export default class PortfolioStore extends BaseStore { this.error = ''; if (response.portfolio.contracts) { this.positions = response.portfolio.contracts - .map(pos => formatPortfolioPosition(pos, this.root_store.modules.trade.active_symbols)) + .map(pos => formatPortfolioPosition(pos, this.root_store.active_symbols.active_symbols)) .sort((pos1, pos2) => pos2.reference - pos1.reference); // new contracts first this.positions.forEach(p => { @@ -128,7 +148,7 @@ export default class PortfolioStore extends BaseStore { deepClone = obj => JSON.parse(JSON.stringify(obj)); updateContractTradeStore(response) { - const contract_trade = this.root_store.modules.contract_trade; + const contract_trade = this.root_store.contract_trade; const has_poc = !isEmptyObject(response.proposal_open_contract); const has_error = !!response.error; if (!has_poc && !has_error) return; @@ -139,18 +159,18 @@ export default class PortfolioStore extends BaseStore { } updateContractReplayStore(response) { - const contract_replay = this.root_store.modules.contract_replay; + const contract_replay = this.root_store.contract_replay; if (contract_replay.contract_id === response.proposal_open_contract?.contract_id) { contract_replay.populateConfig(response); } } updateTradeStore(is_over, portfolio_position, is_limit_order_update) { - const trade = this.root_store.modules.trade; + // const trade = this.root_store.modules.trade; if (!is_limit_order_update) { - trade.setPurchaseSpotBarrier(is_over, portfolio_position); + this.setPurchaseSpotBarrier(is_over, portfolio_position); } - trade.updateLimitOrderBarriers(is_over, portfolio_position); + this.updateLimitOrderBarriers(is_over, portfolio_position); } proposalOpenContractQueueHandler = response => { @@ -175,7 +195,7 @@ export default class PortfolioStore extends BaseStore { const formatted_position = formatPortfolioPosition( proposal, - this.root_store.modules.trade.active_symbols, + this.root_store.active_symbols.active_symbols, portfolio_position.indicative ); Object.assign(portfolio_position, formatted_position); @@ -274,12 +294,12 @@ export default class PortfolioStore extends BaseStore { } } else if (!response.error && response.sell) { // update contract store sell info after sell - this.root_store.modules.contract_trade.sell_info = { + this.root_store.contract_trade.sell_info = { sell_price: response.sell.sold_for, transaction_id: response.sell.transaction_id, }; this.root_store.notifications.addNotificationMessage( - contractSold(this.root_store.client.currency, response.sell.sold_for) + contractSold(this.root_store.client.currency, response.sell.sold_for, Money) ); } } @@ -353,7 +373,7 @@ export default class PortfolioStore extends BaseStore { @action.bound pushNewPosition(new_pos) { - const position = formatPortfolioPosition(new_pos, this.root_store.modules.trade.active_symbols); + const position = formatPortfolioPosition(new_pos, this.root_store.active_symbols.active_symbols); if (this.positions_map[position.id]) return; this.positions.unshift(position); @@ -368,7 +388,7 @@ export default class PortfolioStore extends BaseStore { this.positions.splice(contract_idx, 1); delete this.positions_map[contract_id]; this.updatePositions(); - this.root_store.modules.contract_trade.removeContract({ contract_id }); + this.root_store.contract_trade.removeContract({ contract_id }); } async accountSwitcherListener() { @@ -378,7 +398,7 @@ export default class PortfolioStore extends BaseStore { @action.bound onHoverPosition(is_over, position) { - const { symbol: underlying } = this.root_store.modules.trade; + const { symbol: underlying } = this.root_store.active_symbols; if ( position.contract_info.underlying !== underlying || isEnded(position.contract_info) || @@ -432,10 +452,13 @@ export default class PortfolioStore extends BaseStore { @action.bound onUnmount() { - this.disposePreSwitchAccount(); - this.disposeSwitchAccount(); - this.disposeLogout(); - this.clearTable(); + const is_reports_path = /^\/reports/.test(window.location.pathname); + if (!is_reports_path) { + this.clearTable(); + this.disposePreSwitchAccount(); + this.disposeSwitchAccount(); + this.disposeLogout(); + } } getPositionIndexById(contract_id) { @@ -488,4 +511,62 @@ export default class PortfolioStore extends BaseStore { get is_empty() { return !this.is_loading && this.all_positions.length === 0; } + + // from trade store + @action.bound + setPurchaseSpotBarrier(is_over, position) { + const key = 'PURCHASE_SPOT_BARRIER'; + if (!is_over) { + removeBarrier(this.barriers, key); + return; + } + + let purchase_spot_barrier = this.barriers.find(b => b.key === key); + if (purchase_spot_barrier) { + if (purchase_spot_barrier.high !== +position.contract_info.entry_spot) { + purchase_spot_barrier.onChange({ + high: position.contract_info.entry_spot, + }); + } + } else { + purchase_spot_barrier = new ChartBarrierStore(position.contract_info.entry_spot); + purchase_spot_barrier.key = key; + purchase_spot_barrier.draggable = false; + purchase_spot_barrier.hideOffscreenBarrier = true; + purchase_spot_barrier.isSingleBarrier = true; + purchase_spot_barrier.updateBarrierColor(this.root_store.ui.is_dark_mode_on); + this.barriers.push(purchase_spot_barrier); + } + } + + @action.bound + updateBarrierColor(is_dark_mode) { + const { main_barrier } = JSON.parse(localStorage.getItem('trade_store')) || {}; + this.main_barrier = main_barrier; + if (this.main_barrier) { + this.main_barrier.updateBarrierColor(is_dark_mode); + } + } + + @action.bound + updateLimitOrderBarriers(is_over, position) { + const contract_info = position.contract_info; + const { barriers } = this; + setLimitOrderBarriers({ + barriers, + contract_info, + contract_type: contract_info.contract_type, + is_over, + }); + } + + @action.bound + setContractType(contract_type) { + this.contract_type = contract_type; + } + + @computed + get is_multiplier() { + return this.contract_type === 'multiplier'; + } } diff --git a/packages/reports/.eslintignore b/packages/reports/.eslintignore new file mode 100644 index 000000000000..d33f07c6df7b --- /dev/null +++ b/packages/reports/.eslintignore @@ -0,0 +1,5 @@ +build/*.js +src/**/__tests__/*.js +src/_common/lib/**/*.js +.eslintrc.js +.stylelintrc.js diff --git a/packages/reports/.eslintrc.js b/packages/reports/.eslintrc.js new file mode 100644 index 000000000000..6718b8cddc2d --- /dev/null +++ b/packages/reports/.eslintrc.js @@ -0,0 +1,10 @@ +const webpackConfig = require('./build/webpack.config-test.js'); + +module.exports = { + extends: '../../.eslintrc.js', + settings: { + 'import/resolver': { + webpack: { config: webpackConfig({}) }, + }, + }, +}; diff --git a/packages/reports/.eslintrc.ts b/packages/reports/.eslintrc.ts new file mode 100644 index 000000000000..c56c88f54e6e --- /dev/null +++ b/packages/reports/.eslintrc.ts @@ -0,0 +1,16 @@ +const webpackConfig = require('./build/webpack.config-test.js'); + +module.exports = { + extends: [ + '../../.eslintrc.js', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + settings: { + 'import/resolver': { + webpack: { config: webpackConfig({}) }, + }, + }, +}; diff --git a/packages/reports/README.md b/packages/reports/README.md new file mode 100644 index 000000000000..321f3a03fac1 --- /dev/null +++ b/packages/reports/README.md @@ -0,0 +1,97 @@ +# Deriv App + +This repository contains the static HTML, Javascript, CSS, and images content of the [Deriv](http://app.deriv.com) website. + +**In this document** + +- [Other documents](#other-documents) +- [Installation](#installation) +- [How to work with this project](#how-to-work-with-this-project) + - [Deploy to your gh-pages for the first time](#deploy-to-your-gh-pages-for-the-first-time) + - [Deploy to the root of the gh-pages](#deploy-to-the-root-of-the-gh-pages) + - [Clean root and deploy to it](#clean-root-and-deploy-to-it) + - [Deploy to test folder](#deploy-to-test-folder) + - [Preview on your local machine](#preview-on-your-local-machine) +- [Miscellaneous](#Miscellaneous) +- [Selenium tests](#selenium-tests) + +## Other documents + +- [Modules docs](docs/Modules/README.md) - Contains implementation guides (i.e., scaffolding, code usage) + +## Installation + +In order to work on your own version of the Deriv Javascript and CSS, please **fork this project**. + +You will also need to install the following on your development machine: + +- Node.js (10.14.2 or higher is recommended) and NPM (see ) +- Go to project root, then run `npm install` + +### Use a custom domain + +In order to use your custom domain, please put it in a file named `CNAME` inside `scripts` folder of your local clone of deriv-app. + +## How to work with this project + +### Deploy to your gh-pages for the first time + +1. Register your application [here](https://developers.binary.com/applications/). This will give you the ability to redirect back to your github pages after login. + Use `https://YOUR_GITHUB_USERNAME.github.io/deriv-app/` for the Redirect URL and `https://YOUR_GITHUB_USERNAME.github.io/deriv-app/en/redirect` for the Verification URL. + + If you're using a custom domain, replace the github URLs above with your domain and remove the `deriv-app` base path. + +2. In `src/config.js`: Insert the `Application ID` of your registered application in `user_app_id`. + + - **NOTE:** In order to avoid accidentally committing personal changes to this file, use `git update-index --assume-unchanged src/config.js` + +3. Set `NODE_ENV` to `development` with `export NODE_ENV=development` + +4. Run `npm run deploy:clean` + +### Deploy to the root of the gh-pages + +This will overwrite modified files and only clear the content of `js` folder before pushing changes. It will leave other folders as they are. + +```sh +npm run deploy +``` + +### Clean root and deploy to it + +This removes all files and folders and deploys your `dist` folder to the root. + +```sh +npm run deploy:clean +``` + +### Deploy to test folder + +This will add all your changes to the test folder specified. +Please ensure it is prefixed with `br_`. + +```sh +npm run deploy:folder "br_my_test_folder" +``` + +### Preview on your local machine + +- Edit your `/etc/hosts` file to include this domain: + +``` +127.0.0.1 localhost.binary.sx +``` + +- To preview your changes locally for the first time, run `sudo npm start`: + - It will run all tests, compile all CSS, and JS/JSX as well as watch for further js/jsx/css changes and rebuild on every change you make. +- To preview your changes locally without any tests, run `npm run serve` + - It will watch for JS/JSX/CSS changes and rebuild on every change you make. +- To run all tests, run `npm run test` + +## Miscellaneous + +- In Webstorm, right-click on `src`, hover over `Mark directory as`, and click `Resource root` to enable import alias resolution. + +## Selenium tests + +Elements for selenium test purposes should have `id` attributes, with the value of `dt_[element_name]_[unique_name|id]_[element_type]`. (e.g. `dt_settings_dark_button`) diff --git a/packages/reports/build/config.js b/packages/reports/build/config.js new file mode 100644 index 000000000000..86801396ab25 --- /dev/null +++ b/packages/reports/build/config.js @@ -0,0 +1,43 @@ +const path = require('path'); +const stylelintFormatter = require('stylelint-formatter-pretty'); +const { IS_RELEASE } = require('./constants'); + +const generateSWConfig = () => ({ + importWorkboxFrom: 'local', + cleanupOutdatedCaches: true, + exclude: [/CNAME$/, /index\.html$/, /404\.html$/], + skipWaiting: true, + clientsClaim: true, +}); + +const htmlOutputConfig = () => ({ + template: 'index.html', + filename: 'index.html', + minify: !IS_RELEASE + ? false + : { + collapseWhitespace: true, + removeComments: true, + removeRedundantAttributes: true, + useShortDoctype: true, + }, +}); + +const cssConfig = () => ({ + filename: 'reports/css/reports.main.[contenthash].css', + chunkFilename: 'reports/css/reports.[name].[contenthash].css', +}); + +const stylelintConfig = () => ({ + configFile: path.resolve(__dirname, '../.stylelintrc.js'), + formatter: stylelintFormatter, + files: 'sass/**/*.s?(a|c)ss', + failOnError: false, // Even though it's false, it will fail on error, and we need this to be false to display trace +}); + +module.exports = { + htmlOutputConfig, + cssConfig, + stylelintConfig, + generateSWConfig, +}; diff --git a/packages/reports/build/constants.js b/packages/reports/build/constants.js new file mode 100644 index 000000000000..dda0d55e4cd5 --- /dev/null +++ b/packages/reports/build/constants.js @@ -0,0 +1,137 @@ +const CircularDependencyPlugin = require('circular-dependency-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const IgnorePlugin = require('webpack').IgnorePlugin; +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const path = require('path'); +const StylelintPlugin = require('stylelint-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); + +const { cssConfig, stylelintConfig } = require('./config'); +const { + css_loaders, + file_loaders, + html_loaders, + js_loaders, + svg_file_loaders, + svg_loaders, +} = require('./loaders-config'); + +const IS_RELEASE = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging'; + +const ALIASES = { + _common: path.resolve(__dirname, '../src/_common'), + Constants: path.resolve(__dirname, '../src/Constants'), + Components: path.resolve(__dirname, '../src/Components'), + Containers: path.resolve(__dirname, '../src/Containers'), + Modules: path.resolve(__dirname, '../src/Modules'), + Sass: path.resolve(__dirname, '../src/sass'), + Stores: path.resolve(__dirname, '../src/Stores'), +}; + +const rules = (is_test_env = false, is_mocha_only = false) => [ + ...(is_test_env && !is_mocha_only + ? [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules|__tests__|(build\/.*\.js$)|(_common\/lib)/, + include: /src/, + loader: 'eslint-loader', + enforce: 'pre', + options: { + formatter: require('eslint-formatter-pretty'), + configFile: path.resolve(__dirname, '../.eslintrc.js'), + ignorePath: path.resolve(__dirname, '../.eslintignore'), + }, + }, + { + test: /\.(ts|tsx)$/, + exclude: /node_modules|__tests__|(build\/.*\.js$)|(_common\/lib)/, + include: /src/, + loader: 'eslint-loader', + enforce: 'pre', + options: { + formatter: require('eslint-formatter-pretty'), + configFile: path.resolve(__dirname, '../.eslintrc.ts'), + ignorePath: path.resolve(__dirname, '../.eslintignore'), + }, + }, + ] + : []), + { + test: /\.(js|jsx|ts|tsx)$/, + exclude: is_test_env ? /node_modules/ : /node_modules|__tests__/, + include: is_test_env ? /__tests__|src/ : /src/, + use: js_loaders, + }, + { + test: /\.html$/, + exclude: /node_modules/, + use: html_loaders, + }, + { + test: /\.(png|jpg|gif|woff|woff2|eot|ttf|otf)$/, + exclude: /node_modules/, + use: file_loaders, + }, + { + test: /\.svg$/, + exclude: /node_modules/, + include: /public\//, + use: svg_file_loaders, + }, + { + test: /\.svg$/, + exclude: /node_modules|public\//, + use: svg_loaders, + }, + is_test_env + ? { + test: /\.(sc|sa|c)ss$/, + loaders: 'null-loader', + } + : { + test: /\.(sc|sa|c)ss$/, + use: css_loaders, + }, +]; + +const MINIMIZERS = !IS_RELEASE + ? [] + : [ + new TerserPlugin({ + test: /\.js$/, + exclude: /(smartcharts)/, + parallel: 2, + }), + new CssMinimizerPlugin(), + ]; + +const plugins = (base, is_test_env, is_mocha_only) => [ + new CleanWebpackPlugin(), + new IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ }), + new MiniCssExtractPlugin(cssConfig()), + new CircularDependencyPlugin({ exclude: /node_modules/, failOnError: true }), + ...(IS_RELEASE + ? [] + : [ + new WebpackManifestPlugin({ + fileName: 'reports/asset-manifest.json', + filter: file => file.name !== 'CNAME', + }), + ]), + ...(is_test_env && !is_mocha_only + ? [new StylelintPlugin(stylelintConfig())] + : [ + // ...(!IS_RELEASE ? [ new BundleAnalyzerPlugin({ analyzerMode: 'static' }) ] : []), + ]), +]; + +module.exports = { + IS_RELEASE, + ALIASES, + plugins, + rules, + MINIMIZERS, +}; diff --git a/packages/reports/build/helpers.js b/packages/reports/build/helpers.js new file mode 100644 index 000000000000..ff63efbbbfae --- /dev/null +++ b/packages/reports/build/helpers.js @@ -0,0 +1,33 @@ +/** + * @param {Buffer} content + * @param {string} path + * @param {string|RegExp} base + * */ +const transformContentUrlBase = (content, path, base) => { + return content.toString().replace(/{root_url}|{start_url_base}/g, base); +}; + +/** + * @returns {string} Chrome browser string + * */ +const openChromeBasedOnPlatform = platform => { + switch (platform) { + case 'win32': { + return 'chrome'; + } + case 'linux': { + return 'google-chrome'; + } + case 'darwin': { + return 'Google Chrome'; + } + default: { + return 'Google Chrome'; + } + } +}; + +module.exports = { + transformContentUrlBase, + openChromeBasedOnPlatform, +}; diff --git a/packages/reports/build/loaders-config.js b/packages/reports/build/loaders-config.js new file mode 100644 index 000000000000..244d0aa2d1b8 --- /dev/null +++ b/packages/reports/build/loaders-config.js @@ -0,0 +1,115 @@ +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const path = require('path'); + +const js_loaders = [ + { + loader: '@deriv/shared/src/loaders/react-import-loader.js', + }, + { + loader: '@deriv/shared/src/loaders/deriv-account-loader.js', + }, + { + loader: 'babel-loader', + options: { + cacheDirectory: true, + rootMode: 'upward', + }, + }, +]; + +const html_loaders = [ + { + loader: 'html-loader', + }, +]; + +const file_loaders = [ + { + loader: 'file-loader', + options: { + name: '[path][name].[ext]', + }, + }, +]; + +const svg_file_loaders = [ + { + loader: 'file-loader', + options: { + name: '[path][name].[ext]', + }, + }, +]; + +const svg_loaders = [ + { + loader: 'babel-loader', + options: { + cacheDirectory: true, + rootMode: 'upward', + }, + }, + { + loader: 'react-svg-loader', + options: { + jsx: true, + svgo: { + plugins: [ + { removeTitle: false }, + { removeUselessStrokeAndFill: false }, + { removeUknownsAndDefaults: false }, + ], + floatPrecision: 2, + }, + }, + }, +]; + +const css_loaders = [ + { + loader: MiniCssExtractPlugin.loader, + }, + { + loader: 'css-loader', + options: { + sourceMap: true, + }, + }, + { + loader: 'postcss-loader', + options: { + sourceMap: true, + postcssOptions: { + config: path.resolve(__dirname), + }, + }, + }, + { + loader: 'resolve-url-loader', + options: { + sourceMap: true, + keepQuery: true, + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: true, + }, + }, + { + loader: 'sass-resources-loader', + options: { + resources: require('@deriv/shared/src/styles/index.js'), + }, + }, +]; + +module.exports = { + js_loaders, + html_loaders, + file_loaders, + svg_loaders, + svg_file_loaders, + css_loaders, +}; diff --git a/packages/reports/build/postcss.config.js b/packages/reports/build/postcss.config.js new file mode 100644 index 000000000000..ce1fe7ec00d9 --- /dev/null +++ b/packages/reports/build/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + parser: 'postcss-scss', + plugins: { + 'postcss-preset-env': {}, + }, +}; diff --git a/packages/reports/build/webpack.config-test.js b/packages/reports/build/webpack.config-test.js new file mode 100644 index 000000000000..3fb503cb753c --- /dev/null +++ b/packages/reports/build/webpack.config-test.js @@ -0,0 +1,33 @@ +const { ALIASES, IS_RELEASE, MINIMIZERS, plugins, rules } = require('./constants'); +const path = require('path'); +const nodeExternals = require('webpack-node-externals'); + +module.exports = function (env) { + const base = env && env.base && env.base !== true ? `/${env.base}/` : '/'; + + return { + context: path.resolve(__dirname, '../src'), + devtool: IS_RELEASE ? undefined : 'eval-cheap-module-source-map', + entry: './index.js', + externals: [nodeExternals()], + mode: IS_RELEASE ? 'development' : 'production', + module: { + rules: rules(true, env && env.mocha_only), + }, + optimization: { + chunkIds: 'named', + minimize: true, + minimizer: MINIMIZERS, + }, + output: { + filename: 'js/[name].[hash].js', + publicPath: '/', + }, + plugins: plugins(base, true, env && env.mocha_only), + resolve: { + alias: ALIASES, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + target: 'node', + }; +}; diff --git a/packages/reports/build/webpack.config.js b/packages/reports/build/webpack.config.js new file mode 100644 index 000000000000..2f69814b2796 --- /dev/null +++ b/packages/reports/build/webpack.config.js @@ -0,0 +1,58 @@ +const path = require('path'); +const { ALIASES, IS_RELEASE, MINIMIZERS, plugins, rules } = require('./constants'); + +module.exports = function (env) { + const base = env && env.base && env.base !== true ? `/${env.base}/` : '/'; + + return { + context: path.resolve(__dirname, '../'), + devtool: IS_RELEASE ? undefined : 'eval-cheap-module-source-map', + entry: { + reports: path.resolve(__dirname, '../src', 'index.jsx'), + 'positions-drawer-card': 'Components/Elements/PositionsDrawerCard', + }, + mode: IS_RELEASE ? 'production' : 'development', + module: { + rules: rules(), + }, + resolve: { + alias: ALIASES, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + optimization: { + chunkIds: 'named', + moduleIds: 'named', + minimize: IS_RELEASE, + minimizer: MINIMIZERS, + }, + output: { + filename: 'reports/js/[name].js', + publicPath: base, + path: path.resolve(__dirname, '../dist'), + chunkFilename: 'reports/js/reports.[name].[contenthash].js', + libraryExport: 'default', + library: '@deriv/reports', + libraryTarget: 'umd', + }, + externals: [ + { + react: 'react', + 'react-dom': 'react-dom', + 'react-router-dom': 'react-router-dom', + 'react-router': 'react-router', + mobx: 'mobx', + 'mobx-react': 'mobx-react', + '@deriv/shared': '@deriv/shared', + '@deriv/components': '@deriv/components', + '@deriv/translations': '@deriv/translations', + '@deriv/deriv-charts': '@deriv/deriv-charts', + }, + /^@deriv\/shared\/.+$/, + /^@deriv\/components\/.+$/, + /^@deriv\/translations\/.+$/, + /^@deriv\/account\/.+$/, + ], + target: 'web', + plugins: plugins(base, false), + }; +}; diff --git a/packages/reports/jest.config.js b/packages/reports/jest.config.js new file mode 100644 index 000000000000..d252ad18aadd --- /dev/null +++ b/packages/reports/jest.config.js @@ -0,0 +1,19 @@ +const baseConfigForPackages = require('../../jest.config.base'); + +module.exports = { + ...baseConfigForPackages, + moduleNameMapper: { + "\\.s(c|a)ss$": "/../../__mocks__/styleMock.js", + "^.+\\.svg$": "/../../__mocks__/styleMock.js", + '^_common\/(.*)$': "/src/_common/$1", + '^App\/(.*)$': "/src/App/$1", + '^Assets\/(.*)$': "/src/Assets/$1", + '^Constants\/(.*)$': "/src/Constants/$1", + '^Constants$': "/src/Constants/index.js", + '^Documents\/(.*)$': "/src/Documents/$1", + '^Modules\/(.*)$': "/src/Modules/$1", + '^Utils\/(.*)$': "/src/Utils/$1", + '^Services\/(.*)$': "/src/Services/$1", + '^Stores\/(.*)$': "/src/Stores/$1", + }, +}; diff --git a/packages/reports/package.json b/packages/reports/package.json new file mode 100644 index 000000000000..2102253cd215 --- /dev/null +++ b/packages/reports/package.json @@ -0,0 +1,127 @@ +{ + "name": "@deriv/reports", + "version": "1.0.0", + "description": "Deriv content", + "main": "dist/reports/js/reports.js", + "private": true, + "scripts": { + "start": "npm run test && npm run serve", + "serve": "echo \"Serving...\" && webpack --progress --watch --config \"./build/webpack.config.js\"", + "build": "f () { webpack --config \"./build/webpack.config.js\" --env base=$1;}; f", + "build:travis": "echo \"No build:travis specified\"", + "test": "echo \"No mocha:test specified\"", + "test:eslint": "eslint \"./src/**/*.?(js|jsx)\"", + "test:mocha": "mochapack -r babel-polyfill -r jsdom-global/register -r mock-local-storage --webpack-config \"./build/webpack.config-test.js\" \"src/**/__tests__/*.js\" --webpack-env.mocha_only --require ignore-styles", + "deploy": "echo \"No deploy specified\"", + "deploy:clean": "echo \"No deploy:clean specified\"", + "deploy:folder": "echo \"No deploy:folder specified\"", + "deploy:staging": "echo \"No deploy:staging specified\"", + "deploy:production": "echo \"No deploy:production specified\"" + }, + "engines": { + "node": "^14.17.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/binary-com/deriv-app.git" + }, + "keywords": [ + "deriv" + ], + "author": "Deriv", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/binary-com/deriv-app/issues" + }, + "homepage": "https://github.com/binary-com/deriv-app", + "devDependencies": { + "babel-eslint": "^10.1.0", + "babel-loader": "^8.1.0", + "chai": "^4.2.0", + "circular-dependency-plugin": "^5.2.2", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^9.0.1", + "css-loader": "^5.0.1", + "css-minimizer-webpack-plugin": "^3.0.1", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.14.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-config-binary": "^1.0.2", + "eslint-config-prettier": "^7.2.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", + "file-loader": "^6.2.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "html-loader": "^1.3.2", + "html-webpack-plugin": "^5.0.0-beta.5", + "html-webpack-tags-plugin": "^2.0.17", + "ignore-styles": "^5.0.1", + "jsdom": "^16.2.1", + "jsdom-global": "^2.1.1", + "mini-css-extract-plugin": "^1.3.4", + "mocha": "^7.1.1", + "mochapack": "^2.1.2", + "mock-local-storage": "^1.1.8", + "node-sass": "^7.0.1", + "postcss-loader": "^6.2.1", + "postcss-preset-env": "^7.4.3", + "postcss-scss": "^4.0.3", + "react-svg-loader": "^3.0.3", + "resolve-url-loader": "^3.1.2", + "sass-loader": "^12.6.0", + "sass-resources-loader": "^2.1.1", + "sinon": "^7.3.2", + "stylelint-formatter-pretty": "^2.1.1", + "svgo": "^2.8.0", + "terser-webpack-plugin": "^5.1.1", + "webpack": "^5.46.0", + "webpack-bundle-analyzer": "^4.3.0", + "webpack-cli": "^4.7.2", + "webpack-manifest-plugin": "^4.0.2", + "webpack-node-externals": "^2.5.2" + }, + "dependencies": { + "@deriv/api-types": "1.0.48", + "@deriv/components": "^1.0.0", + "@deriv/deriv-api": "^1.0.8", + "@deriv/shared": "^1.0.0", + "@deriv/translations": "^1.0.0", + "@types/classnames": "^2.2.11", + "@types/react": "^18.0.7", + "@types/react-dom": "^18.0.0", + "@types/react-loadable": "^5.5.6", + "acorn": "^6.1.1", + "babel-polyfill": "^6.26.0", + "canvas-toBlob": "^1.0.0", + "classnames": "^2.2.6", + "crc-32": "^1.2.0", + "event-source-polyfill": "1.0.25", + "extend": "^3.0.2", + "formik": "^2.1.4", + "i18next": "^20.3.2", + "js-cookie": "^2.2.1", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "mobx": "^5.15.7", + "mobx-react": "6.3.1", + "mobx-utils": "^5.5.5", + "moment": "^2.29.2", + "null-loader": "^4.0.1", + "object.fromentries": "^2.0.0", + "promise-polyfill": "^8.1.3", + "prop-types": "^15.7.2", + "react": "^16.14.0", + "react-content-loader": "^4.3.2", + "react-dom": "^16.14.0", + "react-i18next": "^11.11.0", + "react-loadable": "^5.5.0", + "react-pose": "^4.0.10", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", + "react-transition-group": "^4.3.0", + "typescript": "^4.6.3", + "workbox-webpack-plugin": "^6.0.2" + } + } \ No newline at end of file diff --git a/packages/reports/src/Components/Elements/ContentLoader/index.js b/packages/reports/src/Components/Elements/ContentLoader/index.js new file mode 100644 index 000000000000..9e70635b68c6 --- /dev/null +++ b/packages/reports/src/Components/Elements/ContentLoader/index.js @@ -0,0 +1 @@ +export * from './reports-table-row.jsx'; diff --git a/packages/reports/src/Components/Elements/ContentLoader/positions-card.jsx b/packages/reports/src/Components/Elements/ContentLoader/positions-card.jsx new file mode 100644 index 000000000000..3e2708bc34b4 --- /dev/null +++ b/packages/reports/src/Components/Elements/ContentLoader/positions-card.jsx @@ -0,0 +1,35 @@ +import ContentLoader from 'react-content-loader'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const PositionsCardLoader = ({ speed }) => ( + + + + + + + + + + + + + + + + + +); + +PositionsCardLoader.propTypes = { + speed: PropTypes.number, +}; + +export { PositionsCardLoader }; diff --git a/packages/trader/src/App/Components/Elements/ContentLoader/reports-table-row.jsx b/packages/reports/src/Components/Elements/ContentLoader/reports-table-row.jsx similarity index 100% rename from packages/trader/src/App/Components/Elements/ContentLoader/reports-table-row.jsx rename to packages/reports/src/Components/Elements/ContentLoader/reports-table-row.jsx diff --git a/packages/trader/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/index.js b/packages/reports/src/Components/Elements/PositionsDrawerCard/index.js similarity index 100% rename from packages/trader/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/index.js rename to packages/reports/src/Components/Elements/PositionsDrawerCard/index.js diff --git a/packages/trader/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx b/packages/reports/src/Components/Elements/PositionsDrawerCard/positions-drawer-card.jsx similarity index 85% rename from packages/trader/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx rename to packages/reports/src/Components/Elements/PositionsDrawerCard/positions-drawer-card.jsx index 9b5d4e791d80..8e9a4dd23eb7 100644 --- a/packages/trader/src/App/Components/Elements/PositionsDrawer/PositionsDrawerCard/positions-drawer-card.jsx +++ b/packages/reports/src/Components/Elements/PositionsDrawerCard/positions-drawer-card.jsx @@ -3,11 +3,14 @@ import PropTypes from 'prop-types'; import React from 'react'; import { NavLink } from 'react-router-dom'; import { ContractCard } from '@deriv/components'; -import { getContractPath, isCryptoContract, isMultiplierContract } from '@deriv/shared'; -import { getCardLabels, getContractTypeDisplay } from 'Constants/contract'; -import { connect } from 'Stores/connect'; -import { connectWithContractUpdate } from 'Stores/Modules/Contract/Helpers/multiplier'; -import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic'; +import { + getContractPath, + isCryptoContract, + isMultiplierContract, + getCardLabels, + getContractTypeDisplay, + getEndTime, +} from '@deriv/shared'; const PositionsDrawerCard = ({ addToast, @@ -68,7 +71,6 @@ const PositionsDrawerCard = ({ const card_body = ( ({ - currency: client.currency, - server_time: common.server_time, - addToast: ui.addToast, - current_focus: ui.current_focus, - onClickCancel: modules.portfolio.onClickCancel, - onClickSell: modules.portfolio.onClickSell, - onClickRemove: modules.portfolio.removePositionById, - getContractById: modules.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, -}))(PositionsDrawerCard); +export default PositionsDrawerCard; diff --git a/packages/reports/src/Components/Errors/error-component.jsx b/packages/reports/src/Components/Errors/error-component.jsx new file mode 100644 index 000000000000..868959ab38b8 --- /dev/null +++ b/packages/reports/src/Components/Errors/error-component.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { PageError, Dialog } from '@deriv/components'; +import { routes } from '@deriv/shared'; +import { localize } from '@deriv/translations'; + +const ErrorComponent = ({ + header, + message, + is_dialog, + redirect_label, + redirectOnClick, + should_show_refresh = true, +}) => { + const refresh_message = should_show_refresh ? localize('Please refresh this page to continue.') : ''; + + return is_dialog ? ( + location.reload())} + > + {message || localize('Sorry, an error occured while processing your request.')} + + ) : ( + 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 = ( } + rowRenderer={args => } keyMapper={row => row.id} row_gap={8} /> @@ -130,7 +132,6 @@ const PositionsDrawer = ({
); - // } }; PositionsDrawer.propTypes = { @@ -144,16 +145,42 @@ PositionsDrawer.propTypes = { onMount: PropTypes.func, symbol: PropTypes.string, toggleDrawer: PropTypes.func, + currency: PropTypes.string, + server_time: PropTypes.object, + addToast: PropTypes.func, + current_focus: PropTypes.string, + onClickCancel: PropTypes.func, + onClickSell: PropTypes.func, + onClickRemove: PropTypes.func, + getContractById: PropTypes.func, + removeToast: PropTypes.func, + setCurrentFocus: PropTypes.func, + should_show_cancellation_warning: PropTypes.bool, + toggleCancellationWarning: PropTypes.func, + toggleUnsupportedContractModal: PropTypes.func, }; -export default connect(({ modules, ui }) => ({ - all_positions: modules.portfolio.all_positions, - error: modules.portfolio.error, - onHoverPosition: modules.portfolio.onHoverPosition, - onMount: modules.portfolio.onMount, +export default connect(({ modules, ui, client, common, portfolio, contract_trade }) => ({ + all_positions: portfolio.all_positions, + error: portfolio.error, + onHoverPosition: portfolio.onHoverPosition, + onMount: portfolio.onMount, symbol: modules.trade.symbol, trade_contract_type: modules.trade.contract_type, is_mobile: ui.is_mobile, is_positions_drawer_on: ui.is_positions_drawer_on, toggleDrawer: ui.togglePositionsDrawer, + currency: client.currency, + server_time: common.server_time, + addToast: ui.addToast, + current_focus: ui.current_focus, + onClickCancel: portfolio.onClickCancel, + onClickSell: portfolio.onClickSell, + onClickRemove: portfolio.removePositionById, + getContractById: contract_trade.getContractById, + removeToast: ui.removeToast, + setCurrentFocus: ui.setCurrentFocus, + should_show_cancellation_warning: ui.should_show_cancellation_warning, + toggleCancellationWarning: ui.toggleCancellationWarning, + toggleUnsupportedContractModal: ui.toggleUnsupportedContractModal, }))(PositionsDrawer); diff --git a/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx b/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx index 8b28f99770d0..f50a65f7f6c3 100644 --- a/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx +++ b/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-modal-card.jsx @@ -11,16 +11,16 @@ import { isCryptocurrency, hasContractEntered, isOpen, + getSymbolDisplayName, + getEndTime, } from '@deriv/shared'; import { localize } from '@deriv/translations'; import { BinaryLink } from 'App/Components/Routes'; import { connect } from 'Stores/connect'; -import { getSymbolDisplayName } from 'Stores/Modules/Trading/Helpers/active-symbols'; -import { connectWithContractUpdate } from 'Stores/Modules/Contract/Helpers/multiplier'; -import { getEndTime } from 'Stores/Modules/Contract/Helpers/logic'; +import { connectWithContractUpdate } from 'Stores/Modules/Trading/Helpers/multiplier'; import { PositionsCardLoader } from 'App/Components/Elements/ContentLoader'; import { getContractTypeDisplay, getCardLabels } from 'Constants/contract'; -import { getMarketInformation } from 'Modules/Reports/Helpers/market-underlying'; +import { getMarketInformation } from 'Utils/Helpers/market-underlying'; import ResultMobile from './result-mobile.jsx'; const PositionsModalCard = ({ @@ -324,16 +324,16 @@ PositionsModalCard.propTypes = { type: PropTypes.string, }; -export default connect(({ common, ui, modules }) => ({ +export default connect(({ common, ui, contract_trade, modules }) => ({ active_symbols: modules.trade.active_symbols, addToast: ui.addToast, current_focus: ui.current_focus, - getContractById: modules.contract_trade.getContractById, + getContractById: contract_trade.getContractById, is_mobile: ui.is_mobile, removeToast: ui.removeToast, server_time: common.server_time, setCurrentFocus: ui.setCurrentFocus, should_show_cancellation_warning: ui.should_show_cancellation_warning, toggleCancellationWarning: ui.toggleCancellationWarning, - updateLimitOrder: modules.contract_trade.updateLimitOrder, + updateLimitOrder: contract_trade.updateLimitOrder, }))(PositionsModalCard); diff --git a/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx b/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx index ede157507766..48f45da766f4 100644 --- a/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx +++ b/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx @@ -3,8 +3,8 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'; import { Icon, Div100vhContainer, Modal, Text } from '@deriv/components'; import { routes } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import { BinaryLink } from 'App/Components/Routes'; -import EmptyPortfolioMessage from 'Modules/Reports/Components/empty-portfolio-message.jsx'; +import { NavLink } from 'react-router-dom'; +import EmptyPortfolioMessage from '../EmptyPortfolioMessage'; import PositionsModalCard from 'App/Components/Elements/PositionsDrawer/positions-modal-card.jsx'; import { filterByContractType } from 'App/Components/Elements/PositionsDrawer/helpers'; import { connect } from 'Stores/connect'; @@ -111,13 +111,13 @@ const TogglePositionsMobile = ({ {is_empty || error ? : body_content}
- {localize('Go to Reports')} - +
@@ -126,10 +126,10 @@ const TogglePositionsMobile = ({ }; // TODO: Needs to be connected to store due to issue with trade-header-extensions not updating all_positions prop // Fixes issue with positions not updated in positions modal -export default connect(({ modules, ui }) => ({ +export default connect(({ modules, ui, portfolio }) => ({ symbol: modules.trade.symbol, trade_contract_type: modules.trade.contract_type, - onClickRemove: modules.portfolio.removePositionById, + onClickRemove: portfolio.removePositionById, togglePositionsDrawer: ui.togglePositionsDrawer, is_positions_drawer_on: ui.is_positions_drawer_on, }))(TogglePositionsMobile); diff --git a/packages/trader/src/App/Components/Elements/chart-loader.jsx b/packages/trader/src/App/Components/Elements/chart-loader.jsx index a456a82bc382..db52cf92d5e7 100644 --- a/packages/trader/src/App/Components/Elements/chart-loader.jsx +++ b/packages/trader/src/App/Components/Elements/chart-loader.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Loading from '../../../templates/app/components/loading'; +import Loading from '_common/components/loading'; const ChartLoader = ({ is_dark, is_visible }) => is_visible ? ( diff --git a/packages/trader/src/App/Components/Elements/market-countdown-timer.jsx b/packages/trader/src/App/Components/Elements/market-countdown-timer.jsx index a3880327154e..f9d16678b475 100644 --- a/packages/trader/src/App/Components/Elements/market-countdown-timer.jsx +++ b/packages/trader/src/App/Components/Elements/market-countdown-timer.jsx @@ -3,10 +3,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import { Text } from '@deriv/components'; -import { useIsMounted, WS, convertTimeFormat } from '@deriv/shared'; +import { useIsMounted, WS, convertTimeFormat, isMarketClosed } from '@deriv/shared'; import { Localize } from '@deriv/translations'; import { connect } from 'Stores/connect'; -import { isMarketClosed } from 'Stores/Modules/Trading/Helpers/active-symbols'; // check market in coming 7 days const days_to_check_before_exit = 7; diff --git a/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js b/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js index 9b2be737be77..e41bb20685bd 100644 --- a/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js +++ b/packages/trader/src/App/Components/Routes/__tests__/helpers.spec.js @@ -28,23 +28,6 @@ describe('Helpers', () => { expect(result.exact).to.equal(true); expect(result.component).to.equal(Trade); }); - 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', () => { diff --git a/packages/trader/src/App/Constants/routes-config.js b/packages/trader/src/App/Constants/routes-config.js index 0601b89e7f9e..5bbbf238210e 100644 --- a/packages/trader/src/App/Constants/routes-config.js +++ b/packages/trader/src/App/Constants/routes-config.js @@ -1,6 +1,5 @@ import React from 'react'; -import { routes, makeLazyLoader, moduleLoader } from '@deriv/shared'; -import { Loading } from '@deriv/components'; +import { routes, moduleLoader } from '@deriv/shared'; import { localize } from '@deriv/translations'; import Trade from 'Modules/Trading'; @@ -11,11 +10,6 @@ const ContractDetails = React.lazy(() => // Error Routes const Page404 = React.lazy(() => moduleLoader(() => import(/* webpackChunkName: "404" */ 'Modules/Page404'))); -const lazyLoadReportComponent = makeLazyLoader( - () => moduleLoader(() => import(/* webpackChunkName: "reports" */ 'Modules/Reports')), - () => -); - // Order matters const initRoutesConfig = () => { return [ @@ -26,34 +20,6 @@ const initRoutesConfig = () => { getTitle: () => localize('Contract Details'), is_authenticated: true, }, - { - 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', - }, - ], - }, { path: routes.trade, component: Trade, getTitle: () => localize('Trader'), exact: true }, ]; }; diff --git a/packages/trader/src/App/Containers/ProgressSliderStream/progress-slider-stream.jsx b/packages/trader/src/App/Containers/ProgressSliderStream/progress-slider-stream.jsx index a548e6ed54fb..ad46df3b3359 100644 --- a/packages/trader/src/App/Containers/ProgressSliderStream/progress-slider-stream.jsx +++ b/packages/trader/src/App/Containers/ProgressSliderStream/progress-slider-stream.jsx @@ -30,7 +30,7 @@ ProgressSliderStream.propTypes = { server_time: PropTypes.object, }; -export default connect(({ common, modules }) => ({ - is_loading: modules.portfolio.is_loading, +export default connect(({ common, portfolio }) => ({ + is_loading: portfolio.is_loading, server_time: common.server_time, }))(ProgressSliderStream); diff --git a/packages/trader/src/App/Containers/Routes/routes.jsx b/packages/trader/src/App/Containers/Routes/routes.jsx index 55f20c8d0afe..4512ca0c4c19 100644 --- a/packages/trader/src/App/Containers/Routes/routes.jsx +++ b/packages/trader/src/App/Containers/Routes/routes.jsx @@ -117,8 +117,8 @@ Routes.propTypes = { // need to wrap withRouter around connect // to prevent updates on from being blocked export default withRouter( - connect(({ client, common, modules, ui }) => ({ - onUnmountPortfolio: modules.portfolio.onUnmount, + connect(({ client, common, modules, ui, portfolio }) => ({ + onUnmountPortfolio: portfolio.onUnmount, error: common.error, has_error: common.has_error, is_logged_in: client.is_logged_in, diff --git a/packages/trader/src/App/Containers/trade-footer-extensions.jsx b/packages/trader/src/App/Containers/trade-footer-extensions.jsx index a02bd59d96ef..cf3ace658170 100644 --- a/packages/trader/src/App/Containers/trade-footer-extensions.jsx +++ b/packages/trader/src/App/Containers/trade-footer-extensions.jsx @@ -52,8 +52,8 @@ TradeFooterExtensions.propTypes = { togglePositionsDrawer: PropTypes.func, }; -export default connect(({ client, modules, ui }) => ({ - active_positions_count: modules.portfolio.active_positions_count, +export default connect(({ client, ui, portfolio }) => ({ + active_positions_count: portfolio.active_positions_count, is_logged_in: client.is_logged_in, is_positions_drawer_on: ui.is_positions_drawer_on, populateFooterExtensions: ui.populateFooterExtensions, diff --git a/packages/trader/src/App/Containers/trade-header-extensions.jsx b/packages/trader/src/App/Containers/trade-header-extensions.jsx index b8793b387ab2..866d9ed466b7 100644 --- a/packages/trader/src/App/Containers/trade-header-extensions.jsx +++ b/packages/trader/src/App/Containers/trade-header-extensions.jsx @@ -118,17 +118,17 @@ TradeHeaderExtensions.propTypes = { populateHeaderExtensions: PropTypes.func, }; -export default connect(({ client, modules, ui }) => ({ +export default connect(({ client, modules, ui, portfolio }) => ({ positions_currency: client.currency, is_logged_in: client.is_logged_in, - positions: modules.portfolio.all_positions, - onPositionsSell: modules.portfolio.onClickSell, - positions_error: modules.portfolio.error, - onPositionsRemove: modules.portfolio.removePositionById, - onPositionsCancel: modules.portfolio.onClickCancel, + positions: portfolio.all_positions, + onPositionsSell: portfolio.onClickSell, + positions_error: portfolio.error, + onPositionsRemove: portfolio.removePositionById, + onPositionsCancel: portfolio.onClickCancel, onMountCashier: modules.cashier.general_store.onMountCommon, - onMountPositions: modules.portfolio.onMount, - active_positions_count: modules.portfolio.active_positions_count, + onMountPositions: portfolio.onMount, + active_positions_count: portfolio.active_positions_count, trade_contract_type: modules.trade.contract_type, symbol: modules.trade.symbol, disableApp: ui.disableApp, diff --git a/packages/trader/src/Modules/Contract/Components/Digits/digits.jsx b/packages/trader/src/Modules/Contract/Components/Digits/digits.jsx index 90e29c728a6b..288882bbafb7 100644 --- a/packages/trader/src/Modules/Contract/Components/Digits/digits.jsx +++ b/packages/trader/src/Modules/Contract/Components/Digits/digits.jsx @@ -3,9 +3,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import { toJS } from 'mobx'; import { DesktopWrapper, MobileWrapper, Popover, Text } from '@deriv/components'; -import { isMobile, useIsMounted } from '@deriv/shared'; +import { isMobile, useIsMounted, isContractElapsed } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; -import { isContractElapsed } from 'Stores/Modules/Contract/Helpers/logic'; import { Bounce, SlideIn } from 'App/Components/Animations'; import { getMarketNamesMap } from '../../../../Constants'; import { DigitSpot, LastDigitPrediction } from '../LastDigitPrediction'; diff --git a/packages/trader/src/Modules/Contract/Containers/contract-replay-widget.jsx b/packages/trader/src/Modules/Contract/Containers/contract-replay-widget.jsx index 844dc926cb55..989c525ce7cf 100644 --- a/packages/trader/src/Modules/Contract/Containers/contract-replay-widget.jsx +++ b/packages/trader/src/Modules/Contract/Containers/contract-replay-widget.jsx @@ -5,12 +5,12 @@ import { connect } from 'Stores/connect'; import BottomWidgets from '../../SmartChart/Components/bottom-widgets.jsx'; import TopWidgets from '../../SmartChart/Components/top-widgets.jsx'; -export const DigitsWidget = connect(({ modules }) => ({ - contract_info: modules.contract_replay.contract_store.contract_info, - digits_info: modules.contract_replay.contract_store.digits_info, - display_status: modules.contract_replay.contract_store.display_status, - is_digit_contract: modules.contract_replay.contract_store.is_digit_contract, - is_ended: modules.contract_replay.contract_store.is_ended, +export const DigitsWidget = connect(({ contract_replay }) => ({ + contract_info: contract_replay.contract_store.contract_info, + digits_info: contract_replay.contract_store.digits_info, + display_status: contract_replay.contract_store.display_status, + is_digit_contract: contract_replay.contract_store.is_digit_contract, + is_ended: contract_replay.contract_store.is_ended, }))(({ is_digit_contract, is_ended, contract_info, digits_info, display_status }) => ( ({ /> )); -export const InfoBoxWidget = connect(({ modules }) => ({ - contract_info: modules.contract_replay.contract_store.contract_info, - error_message: modules.contract_replay.error_message, - removeError: modules.contract_replay.removeErrorMessage, +export const InfoBoxWidget = connect(({ contract_replay }) => ({ + contract_info: contract_replay.contract_store.contract_info, + error_message: contract_replay.error_message, + removeError: contract_replay.removeErrorMessage, }))(({ contract_info, error_message, removeError }) => ( )); diff --git a/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx b/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx index fefd8dd35169..f006d6b9a485 100644 --- a/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx +++ b/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx @@ -10,7 +10,16 @@ import { SwipeableWrapper, FadeWrapper, } from '@deriv/components'; -import { isDesktop, isMobile, isMultiplierContract, isEmptyObject, getPlatformRedirect, urlFor } from '@deriv/shared'; +import { + isDesktop, + isMobile, + isMultiplierContract, + isEmptyObject, + getPlatformRedirect, + urlFor, + getDurationPeriod, + getDurationUnitText, +} from '@deriv/shared'; import { localize } from '@deriv/translations'; import ChartLoader from 'App/Components/Elements/chart-loader.jsx'; import ContractDrawer from 'App/Components/Elements/ContractDrawer'; @@ -19,7 +28,6 @@ import { SmartChart } from 'Modules/SmartChart'; import { connect } from 'Stores/connect'; import { ChartBottomWidgets, ChartTopWidgets, DigitsWidget, InfoBoxWidget } from './contract-replay-widget.jsx'; import ChartMarker from '../../SmartChart/Components/Markers/marker.jsx'; -import { getDurationPeriod, getDurationUnitText } from 'Stores/Modules/Portfolio/Helpers/details'; const ContractReplay = ({ contract_id, @@ -169,25 +177,25 @@ ContractReplay.propTypes = { routes: PropTypes.arrayOf(PropTypes.object), }; -export default connect(({ common, modules, ui }) => { - const contract_replay = modules.contract_replay; - const contract_store = contract_replay.contract_store; +export default connect(({ common, contract_replay, ui }) => { + const local_contract_replay = contract_replay; + const contract_store = local_contract_replay.contract_store; return { routeBackInApp: common.routeBackInApp, contract_info: contract_store.contract_info, contract_update: contract_store.contract_update, contract_update_history: contract_store.contract_update_history, is_digit_contract: contract_store.is_digit_contract, - is_market_closed: contract_replay.is_market_closed, - is_sell_requested: contract_replay.is_sell_requested, - is_valid_to_cancel: contract_replay.is_valid_to_cancel, - onClickCancel: contract_replay.onClickCancel, - onClickSell: contract_replay.onClickSell, - onMount: contract_replay.onMount, - onUnmount: contract_replay.onUnmount, - indicative_status: contract_replay.indicative_status, - is_chart_loading: contract_replay.is_chart_loading, - is_forward_starting: contract_replay.is_forward_starting, + is_market_closed: local_contract_replay.is_market_closed, + is_sell_requested: local_contract_replay.is_sell_requested, + is_valid_to_cancel: local_contract_replay.is_valid_to_cancel, + onClickCancel: local_contract_replay.onClickCancel, + onClickSell: local_contract_replay.onClickSell, + onMount: local_contract_replay.onMount, + onUnmount: local_contract_replay.onUnmount, + indicative_status: local_contract_replay.indicative_status, + is_chart_loading: local_contract_replay.is_chart_loading, + is_forward_starting: local_contract_replay.is_forward_starting, is_dark_theme: ui.is_dark_mode_on, NotificationMessages: ui.notification_messages_ui, toggleHistoryTab: ui.toggleHistoryTab, @@ -285,9 +293,8 @@ Chart.propTypes = { shouldFetchTickHistory: PropTypes.bool, }; -const ReplayChart = connect(({ modules, ui, common }) => { +const ReplayChart = connect(({ modules, ui, common, contract_replay }) => { const trade = modules.trade; - const contract_replay = modules.contract_replay; const contract_store = contract_replay.contract_store; const contract_config = contract_store.contract_config; const allow_scroll_to_epoch = diff --git a/packages/trader/src/Modules/Contract/Containers/contract.jsx b/packages/trader/src/Modules/Contract/Containers/contract.jsx index e845ab45d54a..45c096f8c110 100644 --- a/packages/trader/src/Modules/Contract/Containers/contract.jsx +++ b/packages/trader/src/Modules/Contract/Containers/contract.jsx @@ -68,14 +68,14 @@ Contract.propTypes = { }; export default withRouter( - connect(({ modules, ui }) => ({ - error_message: modules.contract_replay.error_message, - error_code: modules.contract_replay.error_code, - has_error: modules.contract_replay.has_error, - onMount: modules.contract_replay.setAccountSwitcherListener, - onUnmount: modules.contract_replay.removeAccountSwitcherListener, - removeErrorMessage: modules.contract_replay.removeErrorMessage, - symbol: modules.contract_replay.contract_info.underlying, + connect(({ ui, contract_replay }) => ({ + error_message: contract_replay.error_message, + error_code: contract_replay.error_code, + has_error: contract_replay.has_error, + onMount: contract_replay.setAccountSwitcherListener, + onUnmount: contract_replay.removeAccountSwitcherListener, + removeErrorMessage: contract_replay.removeErrorMessage, + symbol: contract_replay.contract_info.underlying, is_mobile: ui.is_mobile, }))(Contract) ); diff --git a/packages/trader/src/Modules/Reports/index.js b/packages/trader/src/Modules/Reports/index.js deleted file mode 100644 index aac566b021bf..000000000000 --- a/packages/trader/src/Modules/Reports/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import OpenPositions from './Containers/open-positions.jsx'; -import ProfitTable from './Containers/profit-table.jsx'; -import Statement from './Containers/statement.jsx'; -import Reports from './Containers/reports.jsx'; - -export default { - OpenPositions, - ProfitTable, - Statement, - Reports, -}; diff --git a/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx b/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx index 8aa5de06de2c..c20092927e16 100644 --- a/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx +++ b/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx @@ -23,9 +23,9 @@ const TradeInfo = ({ markers_array, granularity }) => { ); }; -const RecentTradeInfo = connect(({ modules }) => ({ - granularity: modules.contract_trade.granularity, - markers_array: modules.contract_trade.markers_array, +const RecentTradeInfo = connect(({ contract_trade }) => ({ + granularity: contract_trade.granularity, + markers_array: contract_trade.markers_array, }))(TradeInfo); const TopWidgets = ({ diff --git a/packages/trader/src/Modules/Trading/Components/Elements/Multiplier/cancel-deal-mobile.jsx b/packages/trader/src/Modules/Trading/Components/Elements/Multiplier/cancel-deal-mobile.jsx index 11e1a565cb60..aec1e6044a68 100644 --- a/packages/trader/src/Modules/Trading/Components/Elements/Multiplier/cancel-deal-mobile.jsx +++ b/packages/trader/src/Modules/Trading/Components/Elements/Multiplier/cancel-deal-mobile.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Checkbox, RadioGroup, Dialog, Popover, Text } from '@deriv/components'; import { localize, Localize } from '@deriv/translations'; import { connect } from 'Stores/connect'; -import { onToggleCancellation, onChangeCancellationDuration } from 'Stores/Modules/Contract/Helpers/multiplier'; +import { onToggleCancellation, onChangeCancellationDuration } from 'Stores/Modules/Trading/Helpers/multiplier'; import Fieldset from 'App/Components/Form/fieldset.jsx'; const DealCancellationWarning = ({ diff --git a/packages/trader/src/Modules/Trading/Components/Elements/mobile-widget.jsx b/packages/trader/src/Modules/Trading/Components/Elements/mobile-widget.jsx index c9549e59cb0c..e681154c7e5c 100644 --- a/packages/trader/src/Modules/Trading/Components/Elements/mobile-widget.jsx +++ b/packages/trader/src/Modules/Trading/Components/Elements/mobile-widget.jsx @@ -2,8 +2,7 @@ import React from 'react'; import { Money } from '@deriv/components'; import { localize, Localize } from '@deriv/translations'; import { connect } from 'Stores/connect'; -import { getExpiryType, getDurationMinMaxValues } from 'Stores/Modules/Trading/Helpers/duration'; -import { getLocalizedBasis } from 'Stores/Modules/Trading/Constants/contract'; +import { getExpiryType, getDurationMinMaxValues, getLocalizedBasis } from '@deriv/shared'; import { MultiplierAmountWidget } from 'Modules/Trading/Components/Form/TradeParams/Multiplier/widgets.jsx'; import TradeParamsModal from '../../Containers/trade-params-mobile.jsx'; diff --git a/packages/trader/src/Modules/Trading/Components/Form/DatePicker/trading-date-picker.jsx b/packages/trader/src/Modules/Trading/Components/Form/DatePicker/trading-date-picker.jsx index 471eef3c29d3..a898662da23f 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/DatePicker/trading-date-picker.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/DatePicker/trading-date-picker.jsx @@ -3,11 +3,10 @@ import PropTypes from 'prop-types'; import { PropTypes as MobxPropTypes } from 'mobx-react'; import React from 'react'; import { DatePicker, Tooltip } from '@deriv/components'; -import { isTimeValid, setTime, toMoment, useIsMounted } from '@deriv/shared'; +import { isTimeValid, setTime, toMoment, useIsMounted, hasIntradayDurationUnit } from '@deriv/shared'; import { localize } from '@deriv/translations'; import { connect } from 'Stores/connect'; -import ContractType from 'Stores/Modules/Trading/Helpers/contract-type'; -import { hasIntradayDurationUnit } from 'Stores/Modules/Trading/Helpers/duration'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; const TradingDatePicker = ({ duration: current_duration, diff --git a/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx b/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx index 359b77c236fa..78d5df7ad90b 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx @@ -3,8 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Icon, DesktopWrapper, Money, MobileWrapper, Popover, Text } from '@deriv/components'; import { localize } from '@deriv/translations'; -import { getCurrencyDisplayCode } from '@deriv/shared'; -import { getLocalizedBasis } from 'Stores/Modules/Trading/Constants/contract'; +import { getCurrencyDisplayCode, getLocalizedBasis } from '@deriv/shared'; import CancelDealInfo from './cancel-deal-info.jsx'; const ValueMovement = ({ has_error_or_not_loaded, proposal_info, currency, has_increased }) => ( diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx index 7193ec00d66f..a747b4b501d5 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx @@ -3,10 +3,9 @@ import { PropTypes as MobxPropTypes } from 'mobx-react'; import PropTypes from 'prop-types'; import React from 'react'; import { Dropdown, ButtonToggle, InputField } from '@deriv/components'; -import { toMoment } from '@deriv/shared'; +import { toMoment, hasIntradayDurationUnit } from '@deriv/shared'; import RangeSlider from 'App/Components/Form/RangeSlider'; import { connect } from 'Stores/connect'; -import { hasIntradayDurationUnit } from 'Stores/Modules/Trading/Helpers/duration'; import TradingDatePicker from '../../DatePicker'; import TradingTimePicker from '../../TimePicker'; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx index 586fa476c22b..915b1b361bce 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx @@ -1,10 +1,9 @@ import React from 'react'; import { Tabs, TickPicker, Numpad, RelativeDatepicker } from '@deriv/components'; -import { isEmptyObject, addComma } from '@deriv/shared'; +import { isEmptyObject, addComma, getDurationMinMaxValues } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; import { connect } from 'Stores/connect'; -import { getDurationMinMaxValues } from 'Stores/Modules/Trading/Helpers/duration'; const submit_label = localize('OK'); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.jsx index a32555503d9e..e927df3cf8f3 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { PropTypes as MobxPropTypes } from 'mobx-react'; import { connect } from 'Stores/connect'; -import { getDurationMinMaxValues } from 'Stores/Modules/Trading/Helpers/duration'; +import { getDurationMinMaxValues } from '@deriv/shared'; import Duration from './duration.jsx'; const DurationWrapper = props => { diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Multiplier/cancel-deal.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Multiplier/cancel-deal.jsx index 0b6a826e6eba..a9d32d7821c3 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Multiplier/cancel-deal.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Multiplier/cancel-deal.jsx @@ -4,7 +4,7 @@ import { Checkbox, Dropdown, Popover, PopoverMessageCheckbox } from '@deriv/comp import { localize } from '@deriv/translations'; import Fieldset from 'App/Components/Form/fieldset.jsx'; import { connect } from 'Stores/connect'; -import { onToggleCancellation, onChangeCancellationDuration } from 'Stores/Modules/Contract/Helpers/multiplier'; +import { onToggleCancellation, onChangeCancellationDuration } from 'Stores/Modules/Trading/Helpers/multiplier'; const CancelDeal = ({ cancellation_range_list, diff --git a/packages/trader/src/Modules/Trading/Containers/chart-widgets.jsx b/packages/trader/src/Modules/Trading/Containers/chart-widgets.jsx index 0ec651ca40ef..3b7bfa3a0f10 100644 --- a/packages/trader/src/Modules/Trading/Containers/chart-widgets.jsx +++ b/packages/trader/src/Modules/Trading/Containers/chart-widgets.jsx @@ -7,12 +7,12 @@ import ControlWidgets from '../../SmartChart/Components/control-widgets.jsx'; import TopWidgets from '../../SmartChart/Components/top-widgets.jsx'; import { symbolChange } from '../../SmartChart/Helpers/symbol'; -export const DigitsWidget = connect(({ modules }) => ({ - contract_info: modules.contract_trade.last_contract.contract_info || {}, - digits_info: modules.contract_trade.last_contract.digits_info || {}, - display_status: modules.contract_trade.last_contract.display_status, - is_digit_contract: modules.contract_trade.last_contract.is_digit_contract, - is_ended: modules.contract_trade.last_contract.is_ended, +export const DigitsWidget = connect(({ modules, contract_trade }) => ({ + contract_info: contract_trade.last_contract.contract_info || {}, + digits_info: contract_trade.last_contract.digits_info || {}, + display_status: contract_trade.last_contract.display_status, + is_digit_contract: contract_trade.last_contract.is_digit_contract, + is_ended: contract_trade.last_contract.is_ended, selected_digit: modules.trade.last_digit, onDigitChange: modules.trade.onChange, underlying: modules.trade.symbol, @@ -74,9 +74,9 @@ export const ChartBottomWidgets = ({ digits, tick }) => ( } /> ); -export const ChartControlWidgets = connect(({ modules }) => ({ - updateChartType: modules.contract_trade.updateChartType, - updateGranularity: modules.contract_trade.updateGranularity, +export const ChartControlWidgets = connect(({ contract_trade }) => ({ + updateChartType: contract_trade.updateChartType, + updateGranularity: contract_trade.updateGranularity, }))(({ updateChartType, updateGranularity }) => ( )); diff --git a/packages/trader/src/Modules/Trading/Containers/contract-type.jsx b/packages/trader/src/Modules/Trading/Containers/contract-type.jsx index 8b0bf72e9fcb..1e8d3d5b59d2 100644 --- a/packages/trader/src/Modules/Trading/Containers/contract-type.jsx +++ b/packages/trader/src/Modules/Trading/Containers/contract-type.jsx @@ -1,14 +1,14 @@ import PropTypes from 'prop-types'; import React from 'react'; import { MobileWrapper, usePrevious } from '@deriv/components'; -import { connect } from 'Stores/connect'; +import { unsupported_contract_types_list } from '@deriv/shared'; import { isDigitTradeType } from 'Modules/Trading/Helpers/digits'; import { localize } from '@deriv/translations'; -import { unsupported_contract_types_list } from 'Stores/Modules/Trading/Constants/contract'; import { ToastPopup } from 'Modules/Trading/Containers/toast-popup.jsx'; import { getMarketNamesMap } from '../../../Constants'; import ContractTypeWidget from '../Components/Form/ContractType'; import { getAvailableContractTypes } from '../Helpers/contract-type'; +import { connect } from 'Stores/connect'; const Contract = ({ contract_type, diff --git a/packages/trader/src/Modules/Trading/Containers/test.jsx b/packages/trader/src/Modules/Trading/Containers/test.jsx index e05e910f6a48..5e6fb7368a55 100644 --- a/packages/trader/src/Modules/Trading/Containers/test.jsx +++ b/packages/trader/src/Modules/Trading/Containers/test.jsx @@ -82,9 +82,9 @@ Test.propTypes = { entries: PropTypes.array, }; -export default connect(({ modules, client, ui }) => ({ +export default connect(({ modules, client, ui, portfolio }) => ({ trade: Object.entries(modules.trade), client: Object.entries(client), ui: Object.entries(ui), - portfolio: Object.entries(modules.portfolio), + portfolio: Object.entries(portfolio), }))(Test); diff --git a/packages/trader/src/Modules/Trading/Containers/trade.jsx b/packages/trader/src/Modules/Trading/Containers/trade.jsx index 9ea083a7bf63..a826b0b5ba58 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade.jsx +++ b/packages/trader/src/Modules/Trading/Containers/trade.jsx @@ -259,10 +259,10 @@ const Markers = ({ markers_array, is_dark_theme, granularity, currency, config } ); }); -const ChartMarkers = connect(({ modules, ui, client }) => ({ - markers_array: modules.contract_trade.markers_array, - is_digit_contract: modules.contract_trade.is_digit_contract, - granularity: modules.contract_trade.granularity, +const ChartMarkers = connect(({ ui, client, contract_trade }) => ({ + markers_array: contract_trade.markers_array, + is_digit_contract: contract_trade.is_digit_contract, + granularity: contract_trade.granularity, is_dark_theme: ui.is_dark_mode_on, currency: client.currency, }))(Markers); @@ -427,13 +427,13 @@ Chart.propTypes = { wsSubscribe: PropTypes.func, }; -const ChartTrade = connect(({ modules, ui, common }) => ({ +const ChartTrade = connect(({ modules, ui, common, contract_trade }) => ({ is_socket_opened: common.is_socket_opened, - granularity: modules.contract_trade.granularity, - chart_type: modules.contract_trade.chart_type, + granularity: contract_trade.granularity, + chart_type: contract_trade.chart_type, chartStateChange: modules.trade.chartStateChange, - updateChartType: modules.contract_trade.updateChartType, - updateGranularity: modules.contract_trade.updateGranularity, + updateChartType: contract_trade.updateChartType, + updateGranularity: contract_trade.updateGranularity, settings: { assetInformation: false, // ui.is_chart_asset_info_visible, countdown: ui.is_chart_countdown_visible, @@ -443,8 +443,8 @@ const ChartTrade = connect(({ modules, ui, common }) => ({ theme: ui.is_dark_mode_on ? 'dark' : 'light', }, last_contract: { - is_digit_contract: modules.contract_trade.last_contract.is_digit_contract, - is_ended: modules.contract_trade.last_contract.is_ended, + is_digit_contract: contract_trade.last_contract.is_digit_contract, + is_ended: contract_trade.last_contract.is_ended, }, is_trade_enabled: modules.trade.is_trade_enabled, main_barrier: modules.trade.main_barrier_flattened, diff --git a/packages/trader/src/Stores/Modules/Contract/Constants/ui.js b/packages/trader/src/Stores/Modules/Contract/Constants/ui.js deleted file mode 100644 index 74e8e69ce7ea..000000000000 --- a/packages/trader/src/Stores/Modules/Contract/Constants/ui.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { Icon } from '@deriv/components'; -import { localize } from '@deriv/translations'; - -export const getHeaderConfig = () => ({ - purchased: { title: localize('Contract Purchased'), icon: }, - won: { title: localize('Contract Won'), icon: }, - lost: { title: localize('Contract Lost'), icon: }, -}); diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/__tests__/logic.js b/packages/trader/src/Stores/Modules/Contract/Helpers/__tests__/logic.js deleted file mode 100644 index 25ceec86f83e..000000000000 --- a/packages/trader/src/Stores/Modules/Contract/Helpers/__tests__/logic.js +++ /dev/null @@ -1,125 +0,0 @@ -import { expect } from 'chai'; -import React from 'react'; -import * as Logic from '../logic'; - -describe('logic', () => { - describe('isSoldBeforeStart', () => { - it('should return true when sell_time is before date_start', () => { - const contract_info = { - sell_time: 1000000, - date_start: 1000001, - }; - expect(Logic.isSoldBeforeStart(contract_info)).to.eql(true); - }); - it('should return true when sell_time is after date_start', () => { - const contract_info = { - sell_time: 1000000, - date_start: 99999, - }; - expect(Logic.isSoldBeforeStart(contract_info)).to.eql(false); - }); - }); - - describe('isStarted', () => { - it('should return true if contract is not forward_starting and current_spot_time is after start_time', () => { - const contract_info = { - is_forward_starting: false, - current_spot_time: 1000000, - date_start: 99999, - }; - expect(Logic.isStarted(contract_info)).to.eql(true); - }); - it('should return true if contract is not forward_starting and current_spot_time is before start_time', () => { - const contract_info = { - is_forward_starting: false, - current_spot_time: 99999, - date_start: 1000000, - }; - expect(Logic.isStarted(contract_info)).to.eql(true); - }); - it('should return true if contract is forward_starting and current_spot_time is after start_time', () => { - const contract_info = { - is_forward_starting: true, - current_spot_time: 1000000, - date_start: 99999, - }; - expect(Logic.isStarted(contract_info)).to.eql(true); - }); - it('should return false if contract is forward_starting and current_spot_time is before start_time', () => { - const contract_info = { - is_forward_starting: true, - current_spot_time: 99999, - date_start: 1000000, - }; - expect(Logic.isStarted(contract_info)).to.eql(false); - }); - }); - - describe('getEndTime', () => { - it('Should return exit tick time for tick contracts', () => { - const contract_info = { - tick_count: 5, - exit_tick_time: 9999999, - sell_time: 1000000, - is_expired: 1, - date_expiry: 8888888, - }; - expect(Logic.getEndTime(contract_info)).to.eql(9999999); - }); - it('Should return date expiry if sell time is after date expiry for non-tick contracts', () => { - const contract_info = { - exit_tick_time: 9999999, - sell_time: 8888888, - is_expired: 1, - date_expiry: 7777777, - }; - expect(Logic.getEndTime(contract_info)).to.eql(7777777); - }); - it('Should return exit tick time if sell time is before date expiry for non-tick contracts', () => { - const contract_info = { - exit_tick_time: 9999999, - sell_time: 7777777, - is_expired: 1, - date_expiry: 8888888, - }; - expect(Logic.getEndTime(contract_info)).to.eql(9999999); - }); - it('Should return date_expiry time for user sold contracts if sell_time is after date_expiry', () => { - const contract_info = { - date_expiry: 8888888, - exit_tick_time: 8888887, - sell_time: 8888889, - status: 'sold', - }; - expect(Logic.getEndTime(contract_info)).to.eql(8888888); - }); - it('Should return sell_time time for user sold contracts if sell_time is before date_expiry', () => { - const contract_info = { - date_expiry: 8888889, - exit_tick_time: 8888887, - sell_time: 8888888, - status: 'sold', - }; - expect(Logic.getEndTime(contract_info)).to.eql(8888888); - }); - it('Should return undefined if not sold for all contracts', () => { - const contract_info = { - exit_tick_time: 9999999, - sell_time: 1000000, - is_expired: 0, - date_expiry: 888888, - }; - expect(Logic.getEndTime(contract_info)).to.eql(undefined); - }); - it('Should return exit_tick_time if contract is_path_dependent', () => { - const contract_info = { - exit_tick_time: 9999999, - sell_time: 1000000, - is_expired: 0, - is_path_dependent: '1', - date_expiry: 888888, - }; - expect(Logic.getEndTime(contract_info)).to.eql(undefined); - }); - }); -}); diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/contract-type.js b/packages/trader/src/Stores/Modules/Contract/Helpers/contract-type.js deleted file mode 100644 index ffd73b0a7112..000000000000 --- a/packages/trader/src/Stores/Modules/Contract/Helpers/contract-type.js +++ /dev/null @@ -1,2 +0,0 @@ -export const isCallPut = trade_type => - trade_type === 'rise_fall' || trade_type === 'rise_fall_equal' || trade_type === 'high_low'; diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/logic.js b/packages/trader/src/Stores/Modules/Contract/Helpers/logic.js deleted file mode 100644 index 4255dc37ec48..000000000000 --- a/packages/trader/src/Stores/Modules/Contract/Helpers/logic.js +++ /dev/null @@ -1,111 +0,0 @@ -import moment from 'moment'; -import { isEmptyObject, isUserSold, getLimitOrderAmount } from '@deriv/shared'; -import ServerTime from '_common/base/server_time'; - -export const getChartConfig = contract_info => { - if (isEmptyObject(contract_info)) return null; - const start = contract_info.date_start; - const end = getEndTime(contract_info); - const granularity = getChartGranularity(start, end || null); - const chart_type = getChartType(start, end || null); - - return { - chart_type: contract_info.tick_count ? 'mountain' : chart_type, - granularity: contract_info.tick_count ? 0 : granularity, - end_epoch: end, - start_epoch: start, - scroll_to_epoch: contract_info.purchase_time, - }; -}; - -const hour_to_granularity_map = [ - [1, 0], - [2, 120], - [6, 600], - [24, 900], - [5 * 24, 3600], - [30 * 24, 14400], -]; - -const getExpiryTime = time => time || ServerTime.get().unix(); - -export const getChartType = (start_time, expiry_time) => { - const duration = moment.duration(moment.unix(getExpiryTime(expiry_time)).diff(moment.unix(start_time))).asHours(); - // use line chart if duration is equal or less than 1 hour - return duration <= 1 ? 'mountain' : 'candle'; -}; - -export const getChartGranularity = (start_time, expiry_time) => - calculateGranularity(getExpiryTime(expiry_time) - start_time); - -export const calculateGranularity = duration => - (hour_to_granularity_map.find(m => duration <= m[0] * 3600) || [null, 86400])[1]; - -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 isCancellationExpired = contract_info => - !!(contract_info.cancellation.date_expiry < ServerTime.get().unix()); - -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 - */ -export const getContractUpdateConfig = ({ contract_update, limit_order }) => { - const { stop_loss, take_profit } = getLimitOrderAmount(limit_order || contract_update); - - return { - // convert stop_loss, take_profit value to string for validation to work - contract_update_stop_loss: stop_loss ? Math.abs(stop_loss).toString() : '', - contract_update_take_profit: take_profit ? take_profit.toString() : '', - has_contract_update_stop_loss: !!stop_loss, - has_contract_update_take_profit: !!take_profit, - }; -}; diff --git a/packages/trader/src/Stores/Modules/Trading/Actions/contract-type.js b/packages/trader/src/Stores/Modules/Trading/Actions/contract-type.js index 844e16e7b0d7..ee14a3eb1074 100644 --- a/packages/trader/src/Stores/Modules/Trading/Actions/contract-type.js +++ b/packages/trader/src/Stores/Modules/Trading/Actions/contract-type.js @@ -1,4 +1,4 @@ -import ContractType from '../Helpers/contract-type'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; export const onChangeContractTypeList = ({ contract_types_list, contract_type }) => ContractType.getContractType(contract_types_list, contract_type); diff --git a/packages/trader/src/Stores/Modules/Trading/Actions/duration.js b/packages/trader/src/Stores/Modules/Trading/Actions/duration.js index e60912493fd0..070d9d7c5555 100644 --- a/packages/trader/src/Stores/Modules/Trading/Actions/duration.js +++ b/packages/trader/src/Stores/Modules/Trading/Actions/duration.js @@ -1,5 +1,5 @@ -import ContractType from '../Helpers/contract-type'; -import { getExpiryType, getDurationMinMaxValues } from '../Helpers/duration'; +import { getExpiryType, getDurationMinMaxValues } from '@deriv/shared'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; export const onChangeExpiry = store => { const contract_expiry_type = getExpiryType(store); diff --git a/packages/trader/src/Stores/Modules/Trading/Actions/start-date.js b/packages/trader/src/Stores/Modules/Trading/Actions/start-date.js index dda9da7604fb..4f4f0f8ff351 100644 --- a/packages/trader/src/Stores/Modules/Trading/Actions/start-date.js +++ b/packages/trader/src/Stores/Modules/Trading/Actions/start-date.js @@ -1,4 +1,4 @@ -import ContractType from '../Helpers/contract-type'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; export const onChangeStartDate = async store => { const { contract_type, duration_unit, start_date } = store; diff --git a/packages/trader/src/Stores/Modules/Trading/Actions/symbol.js b/packages/trader/src/Stores/Modules/Trading/Actions/symbol.js index 34597c0beb4a..95af64de0647 100644 --- a/packages/trader/src/Stores/Modules/Trading/Actions/symbol.js +++ b/packages/trader/src/Stores/Modules/Trading/Actions/symbol.js @@ -1,4 +1,4 @@ -import ContractType from '../Helpers/contract-type'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; export const onChangeSymbolAsync = async symbol => { await ContractType.buildContractTypesConfig(symbol); diff --git a/packages/trader/src/Stores/Modules/Trading/Constants/contract.js b/packages/trader/src/Stores/Modules/Trading/Constants/contract.js deleted file mode 100644 index 938bd50dc811..000000000000 --- a/packages/trader/src/Stores/Modules/Trading/Constants/contract.js +++ /dev/null @@ -1,141 +0,0 @@ -import { localize } from '@deriv/translations'; -import { shouldShowCancellation, shouldShowExpiration } from '@deriv/shared'; - -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', -]; diff --git a/packages/trader/src/Stores/Modules/Trading/Constants/validation-rules.js b/packages/trader/src/Stores/Modules/Trading/Constants/validation-rules.js index f3eb0fcdc70b..ca9f29043833 100644 --- a/packages/trader/src/Stores/Modules/Trading/Constants/validation-rules.js +++ b/packages/trader/src/Stores/Modules/Trading/Constants/validation-rules.js @@ -1,8 +1,8 @@ import { localize } from '@deriv/translations'; -import { isSessionAvailable } from 'Stores/Modules/Trading/Helpers/start-date'; import { isHourValid, isMinuteValid, isTimeValid, toMoment } from '@deriv/shared'; +import { isSessionAvailable } from '../Helpers/start-date'; -const getValidationRules = () => ({ +export const getValidationRules = () => ({ amount: { rules: [ ['req', { message: localize('Amount is a required field.') }], @@ -181,5 +181,3 @@ export const getMultiplierValidationRules = () => ({ ], }, }); - -export default getValidationRules; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/start-date.js b/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/start-date.js deleted file mode 100644 index 1efefc9f2097..000000000000 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/start-date.js +++ /dev/null @@ -1,94 +0,0 @@ -import { expect } from 'chai'; -import moment from 'moment'; -import React from 'react'; -import { buildForwardStartingConfig, isSessionAvailable } 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; - }); - }); - - describe('isSessionAvailable', () => { - it('should return true with default values', () => { - expect(isSessionAvailable()).to.eql(true); - }); - it('should return true when should_only_check_hour is false and there is a session which opens before now and closes after now', () => { - const sessions = [ - { - open: moment.utc(new Date().getTime() - 100000), - close: moment.utc(new Date().getTime() + 100000), - }, - { - open: moment.utc(new Date().getTime() + 300000), - close: moment.utc(new Date().getTime() + 400000), - }, - ]; - expect(isSessionAvailable(sessions)).to.eql(true); - }); - it('should return false when should_only_check_hour is false and there is not a session which opens before now and closes after now and compare_moment is before start_moment', () => { - const sessions = [ - { - open: moment.utc(new Date().getTime() + 400000), - close: moment.utc(new Date().getTime() + 500000), - }, - { - open: moment.utc(new Date().getTime() + 300000), - close: moment.utc(new Date().getTime() + 400000), - }, - ]; - const compare_moment = moment.utc(new Date().getTime() - 1000); - - expect(isSessionAvailable(sessions, compare_moment)).to.eql(false); - }); - it('should return false when should_only_check_hour is false and there is not a session which opens before now and closes after now and compare_moment is before start_moment', () => { - const sessions = [ - { - open: moment.utc(new Date().getTime() + 400000), - close: moment.utc(new Date().getTime() + 500000), - }, - { - open: moment.utc(new Date().getTime() + 300000), - close: moment.utc(new Date().getTime() + 400000), - }, - ]; - expect(isSessionAvailable(sessions, moment.utc(), moment.utc(), false)).to.eql(false); - }); - it('should return false when should_only_check_hour is true and there is not a session which opens before now and closes after now', () => { - const sessions = [ - { - open: moment.utc(new Date().getTime() + 3600000), - close: moment.utc(new Date().getTime() + 3600001), - }, - ]; - expect(isSessionAvailable(sessions, moment.utc(), moment.utc(), true)).to.eql(false); - }); - it('should return true when should_only_check_hour is true and there is a session which opens before now and closes after now', () => { - const sessions = [ - { - open: moment.utc(new Date().getTime() - 1000), - close: moment.utc(new Date().getTime() + 1000), - }, - ]; - expect(isSessionAvailable(sessions, moment.utc(), moment.utc(), true)).to.eql(true); - }); - }); -}); diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/allow-equals.js b/packages/trader/src/Stores/Modules/Trading/Helpers/allow-equals.js index 952def5c31dd..57317967bc0e 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/allow-equals.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/allow-equals.js @@ -1,6 +1,6 @@ import { localize } from '@deriv/translations'; import { isEmptyObject, getPropertyValue } from '@deriv/shared'; -import ContractType from './contract-type'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; export const hasCallPutEqual = contract_type_list => { if (isEmptyObject(contract_type_list)) return false; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js b/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js index 2dfeadecf51e..a76a6ba38a52 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js @@ -1,27 +1,26 @@ import { + WS, getPropertyValue, cloneObject, isTimeValid, minDate, toMoment, shouldShowCancellation, - WS, -} from '@deriv/shared'; -import ServerTime from '_common/base/server_time'; -import { localize } from '@deriv/translations'; - -import { getUnitMap } from 'Stores/Modules/Portfolio/Helpers/details'; -import { buildBarriersConfig } from './barrier'; -import { buildDurationConfig, hasIntradayDurationUnit } from './duration'; -import { buildForwardStartingConfig, isSessionAvailable } from './start-date'; -import { + getUnitMap, + buildBarriersConfig, + buildDurationConfig, + hasIntradayDurationUnit, + buildForwardStartingConfig, unsupported_contract_types_list, getContractCategoriesConfig, getContractTypesConfig, getLocalizedBasis, -} from '../Constants/contract'; +} from '@deriv/shared'; +import ServerTime from '_common/base/server_time'; +import { localize } from '@deriv/translations'; +import { isSessionAvailable } from './start-date'; -const ContractType = (() => { +export const ContractType = (() => { let available_contract_types = {}; let available_categories = {}; let contract_types; @@ -599,5 +598,3 @@ const ContractType = (() => { }), }; })(); - -export default ContractType; diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/limit-orders.js b/packages/trader/src/Stores/Modules/Trading/Helpers/limit-orders.js similarity index 100% rename from packages/trader/src/Stores/Modules/Contract/Helpers/limit-orders.js rename to packages/trader/src/Stores/Modules/Trading/Helpers/limit-orders.js diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/logic.js b/packages/trader/src/Stores/Modules/Trading/Helpers/logic.js new file mode 100644 index 000000000000..837683fe659f --- /dev/null +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/logic.js @@ -0,0 +1,4 @@ +import ServerTime from '_common/base/server_time'; + +export const isCancellationExpired = contract_info => + !!(contract_info.cancellation.date_expiry < ServerTime.get().unix()); diff --git a/packages/trader/src/Stores/Modules/Contract/Helpers/multiplier.js b/packages/trader/src/Stores/Modules/Trading/Helpers/multiplier.js similarity index 100% rename from packages/trader/src/Stores/Modules/Contract/Helpers/multiplier.js rename to packages/trader/src/Stores/Modules/Trading/Helpers/multiplier.js diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/process.js b/packages/trader/src/Stores/Modules/Trading/Helpers/process.js index 574dd124951a..b03c5545b74a 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/process.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/process.js @@ -1,4 +1,4 @@ -import ContractTypeHelper from './contract-type'; +import { ContractType as ContractTypeHelper } from 'Stores/Modules/Trading/Helpers/contract-type'; import * as ContractType from '../Actions/contract-type'; import * as Duration from '../Actions/duration'; import * as StartDate from '../Actions/start-date'; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/start-date.js b/packages/trader/src/Stores/Modules/Trading/Helpers/start-date.js index 010e86ae6daf..d380db266c55 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/start-date.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/start-date.js @@ -1,29 +1,6 @@ import ServerTime from '_common/base/server_time'; import { toMoment } from '@deriv/shared'; -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; -}; - -// returns false if same as now const isBeforeDate = (compare_moment, start_moment, should_only_check_hour) => { const now_moment = toMoment(start_moment); if (should_only_check_hour) { diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.js b/packages/trader/src/Stores/Modules/Trading/trade-store.js index cfe59ac1fece..4ff54091c0b5 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.js +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.js @@ -12,29 +12,30 @@ import { isMobile, showDigitalOptionsUnavailableError, WS, + pickDefaultSymbol, + showUnavailableLocationError, + isMarketClosed, + findFirstOpenMarket, + showMxMltUnavailableError, + convertDurationLimit, + resetEndTimeOnVolatilityIndices, + getBarrierPipSize, + isBarrierSupported, + removeBarrier, } from '@deriv/shared'; import { localize } from '@deriv/translations'; +import { getValidationRules, getMultiplierValidationRules } from 'Stores/Modules/Trading/Constants/validation-rules'; +import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; import { isDigitContractType, isDigitTradeType } from 'Modules/Trading/Helpers/digits'; import ServerTime from '_common/base/server_time'; import { processPurchase } from './Actions/purchase'; import * as Symbol from './Actions/symbol'; -import getValidationRules, { getMultiplierValidationRules } from './Constants/validation-rules'; -import { - pickDefaultSymbol, - showUnavailableLocationError, - isMarketClosed, - findFirstOpenMarket, - showMxMltUnavailableError, -} from './Helpers/active-symbols'; -import ContractType from './Helpers/contract-type'; -import { convertDurationLimit, resetEndTimeOnVolatilityIndices } from './Helpers/duration'; + import { processTradeParams } from './Helpers/process'; import { createProposalRequests, getProposalErrorField, getProposalInfo } from './Helpers/proposal'; -import { getBarrierPipSize } from './Helpers/barrier'; -import { setLimitOrderBarriers } from '../Contract/Helpers/limit-orders'; +import { setLimitOrderBarriers } from './Helpers/limit-orders'; import { ChartBarrierStore } from '../SmartChart/chart-barrier-store'; import { BARRIER_COLORS } from '../SmartChart/Constants/barriers'; -import { isBarrierSupported, removeBarrier } from '../SmartChart/Helpers/barriers'; import BaseStore from '../../base-store'; const store_name = 'trade_store'; @@ -217,6 +218,7 @@ export default class TradeStore extends BaseStore { reaction( () => [this.contract_type], () => { + this.root_store.portfolio.setContractType(this.contract_type); if (this.contract_type === 'multiplier') { // when switching back to Multiplier contract, re-apply Stop loss / Take profit validation rules Object.assign(this.validation_rules, getMultiplierValidationRules()); @@ -260,7 +262,7 @@ export default class TradeStore extends BaseStore { @action.bound clearContracts = () => { - this.root_store.modules.contract_trade.contracts = []; + this.root_store.contract_trade.contracts = []; }; @action.bound @@ -268,6 +270,7 @@ export default class TradeStore extends BaseStore { this.should_show_active_symbols_loading = should_show_loading; await this.setActiveSymbols(); + await this.root_store.active_symbols.setActiveSymbols(); if (should_set_default_symbol) await this.setDefaultSymbol(); const r = await WS.storage.contractsFor(this.symbol); @@ -371,6 +374,7 @@ export default class TradeStore extends BaseStore { }); } this.root_store.common.setSelectedContractType(this.contract_type); + this.root_store.portfolio.setContractType(this.contract_type); } @action.bound @@ -637,7 +641,7 @@ export default class TradeStore extends BaseStore { const { category, underlying } = extractInfoFromShortcode(shortcode); const is_digit_contract = isDigitContractType(category.toUpperCase()); const contract_type = category.toUpperCase(); - this.root_store.modules.contract_trade.addContract({ + this.root_store.contract_trade.addContract({ contract_id, start_time, longcode, @@ -646,7 +650,7 @@ export default class TradeStore extends BaseStore { contract_type, is_tick_contract, }); - this.root_store.modules.portfolio.onBuyResponse({ + this.root_store.portfolio.onBuyResponse({ contract_id, longcode, contract_type, @@ -864,8 +868,8 @@ export default class TradeStore extends BaseStore { chart: { toolbar_position: this.root_store.ui.is_chart_layout_default ? 'bottom' : 'left', chart_asset_info: this.root_store.ui.is_chart_asset_info_visible ? 'visible' : 'hidden', - chart_type: this.root_store.modules.contract_trade.chart_type, - granularity: this.root_store.modules.contract_trade.granularity, + chart_type: this.root_store.contract_trade.chart_type, + granularity: this.root_store.contract_trade.granularity, }, }, }; @@ -1168,7 +1172,7 @@ export default class TradeStore extends BaseStore { this.disposeThemeChange(); this.is_trade_component_mounted = false; // TODO: Find a more elegant solution to unmount contract-trade-store - this.root_store.modules.contract_trade.onUnmount(); + this.root_store.contract_trade.onUnmount(); this.refresh(); this.resetErrorServices(); if (this.root_store.notifications.is_notifications_visible) { diff --git a/packages/trader/src/Stores/Modules/index.js b/packages/trader/src/Stores/Modules/index.js index 61c3a8e725dd..27463b9f10eb 100644 --- a/packages/trader/src/Stores/Modules/index.js +++ b/packages/trader/src/Stores/Modules/index.js @@ -1,18 +1,8 @@ -import ContractReplayStore from './Contract/contract-replay-store'; -import ContractTradeStore from './Contract/contract-trade-store'; -import PortfolioStore from './Portfolio/portfolio-store'; -import ProfitTableStore from './Profit/profit-store'; -import StatementStore from './Statement/statement-store'; import TradeStore from './Trading/trade-store'; export default class ModulesStore { constructor(root_store, core_store) { this.cashier = core_store.modules.cashier; - this.contract_replay = new ContractReplayStore({ root_store }); - this.contract_trade = new ContractTradeStore({ root_store }); - this.portfolio = new PortfolioStore({ root_store }); - this.profit_table = new ProfitTableStore({ root_store }); - this.statement = new StatementStore({ root_store }); this.trade = new TradeStore({ root_store }); } } diff --git a/packages/trader/src/Stores/index.js b/packages/trader/src/Stores/index.js index fef448cd42cb..f0495d2fb985 100644 --- a/packages/trader/src/Stores/index.js +++ b/packages/trader/src/Stores/index.js @@ -10,5 +10,10 @@ export default class RootStore { 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/trader/src/Utils/Helpers/market-underlying.js b/packages/trader/src/Utils/Helpers/market-underlying.js new file mode 100644 index 000000000000..7885f6fd09e7 --- /dev/null +++ b/packages/trader/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/templates/_common/components/loading.tsx b/packages/trader/src/_common/components/loading.tsx similarity index 91% rename from packages/trader/src/templates/_common/components/loading.tsx rename to packages/trader/src/_common/components/loading.tsx index 431514fd7330..fcd1fb275174 100644 --- a/packages/trader/src/templates/_common/components/loading.tsx +++ b/packages/trader/src/_common/components/loading.tsx @@ -13,7 +13,7 @@ const Loading = ({ className, is_invisible, theme, id }: LoadingProps) => ( id={id} className={classNames('barspinner', `barspinner--${theme || 'dark'}`, { invisible: is_invisible }, className)} > - {Array.from(new Array(5)).map((x, inx) => ( + {Array.from(new Array(5)).map((_x, inx) => (
))}
diff --git a/packages/trader/src/templates/app/components/loading.jsx b/packages/trader/src/templates/app/components/loading.jsx deleted file mode 100644 index 8bda86ee71c5..000000000000 --- a/packages/trader/src/templates/app/components/loading.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { Text } from '@deriv/components'; - -const Loading = ({ className, id, is_fullscreen = true, is_slow_loading, status, theme }) => { - const theme_class = theme ? `barspinner-${theme}` : 'barspinner-light'; - return ( -
-
- {Array.from(new Array(5)).map((x, inx) => ( -
- ))} -
- {is_slow_loading && - status.map((text, inx) => ( - - {text} - - ))} -
- ); -}; - -Loading.propTypes = { - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - is_slow_loading: PropTypes.bool, - status: PropTypes.array, - theme: PropTypes.string, -}; -export default Loading; diff --git a/packages/translations/scripts/extract-string.js b/packages/translations/scripts/extract-string.js index 7d57b4b21421..11afefde3ac0 100644 --- a/packages/translations/scripts/extract-string.js +++ b/packages/translations/scripts/extract-string.js @@ -43,6 +43,8 @@ const getTranslatableFiles = () => { 'cfd', 'trader', 'bot-skeleton', + 'reports', + 'shared', ]; const globs = ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/xml/*.xml']; const file_paths = [];