From 08044761972ab6c3bd661f8b6d10eb2b7199cb68 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:51:25 +0300 Subject: [PATCH] Maryia/DTRA-324/refactor: TS migration of trade.jsx and refactoring (#45) * chore: migrate trade to ts * chore: remove unnecessary line * chore: migrate to ts - test and init-store * chore: remove unused test component * chore: more files to ts * chore: add types to trade * chore: trade-chart to ts * refactor: is_mobile * chore: add types to SmartChartSwitcher * chore: ToolbarWidgetsBeta to ts * fix: a type * fix: ws type * chore: remove unused setting * refactor: alphabetical order * chore: index.js to index.ts * refactor: address comments --- .../src/utils/contract/contract-types.ts | 10 +- packages/shared/src/utils/helpers/logic.ts | 2 +- packages/stores/src/mockStore.ts | 4 +- packages/stores/types.ts | 22 +- .../App/Components/Elements/chart-loader.tsx | 2 +- packages/trader/src/App/app.tsx | 3 +- .../src/App/{init-store.js => init-store.ts} | 10 +- packages/trader/src/Documents/STYLE-GUIDE.MD | 222 ++++----- .../Contract/Components/Digits/digits.tsx | 14 +- .../Contract/Containers/contract-replay.tsx | 1 + .../Contract/Containers/replay-chart.tsx | 10 +- .../accumulators-chart-elements.spec.tsx | 2 +- .../Markers/accumulators-chart-elements.tsx | 21 +- .../accumulators-profit-loss-tooltip.tsx | 22 +- .../SmartChart/Components/toolbar-widgets.tsx | 4 +- .../SmartChart/Components/top-widgets.tsx | 6 +- ...oolbar-widgets.jsx => toolbar-widgets.tsx} | 20 +- .../Trading/Containers/chart-widgets.tsx | 6 +- .../Containers/smart-chart-switcher.jsx | 10 - .../Containers/smart-chart-switcher.tsx | 174 +++++++ .../src/Modules/Trading/Containers/test.jsx | 95 ---- .../Trading/Containers/trade-chart.tsx | 199 ++++++++ .../src/Modules/Trading/Containers/trade.jsx | 437 ------------------ .../src/Modules/Trading/Containers/trade.tsx | 235 ++++++++++ packages/trader/src/Modules/Trading/index.js | 3 - packages/trader/src/Modules/Trading/index.ts | 3 + .../src/Stores/Modules/Trading/trade-store.ts | 16 +- packages/trader/src/Types/common-prop.type.ts | 78 +++- 28 files changed, 891 insertions(+), 740 deletions(-) rename packages/trader/src/App/{init-store.js => init-store.ts} (53%) rename packages/trader/src/Modules/SmartChartBeta/Components/{toolbar-widgets.jsx => toolbar-widgets.tsx} (64%) delete mode 100644 packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.jsx create mode 100644 packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.tsx delete mode 100644 packages/trader/src/Modules/Trading/Containers/test.jsx create mode 100644 packages/trader/src/Modules/Trading/Containers/trade-chart.tsx delete mode 100644 packages/trader/src/Modules/Trading/Containers/trade.jsx create mode 100644 packages/trader/src/Modules/Trading/Containers/trade.tsx delete mode 100644 packages/trader/src/Modules/Trading/index.js create mode 100644 packages/trader/src/Modules/Trading/index.ts diff --git a/packages/shared/src/utils/contract/contract-types.ts b/packages/shared/src/utils/contract/contract-types.ts index 0e5be1ce6746..6a6e303f4058 100644 --- a/packages/shared/src/utils/contract/contract-types.ts +++ b/packages/shared/src/utils/contract/contract-types.ts @@ -1,10 +1,4 @@ -import { - ContractUpdate, - ContractUpdateHistory, - Portfolio1, - ProposalOpenContract, - TickSpotData, -} from '@deriv/api-types'; +import { ContractUpdate, ContractUpdateHistory, Portfolio1, ProposalOpenContract } from '@deriv/api-types'; export type TContractStore = { clearContractUpdateConfigValues: () => void; @@ -44,5 +38,3 @@ type TLimitProperty = { }; export type TLimitOrder = Partial>; - -export type TTickSpotData = TickSpotData; diff --git a/packages/shared/src/utils/helpers/logic.ts b/packages/shared/src/utils/helpers/logic.ts index 435d9c5a6a34..108296e90797 100644 --- a/packages/shared/src/utils/helpers/logic.ts +++ b/packages/shared/src/utils/helpers/logic.ts @@ -8,7 +8,7 @@ type TIsSoldBeforeStart = Required>; -export const isContractElapsed = (contract_info: TContractInfo, tick?: TickSpotData) => { +export const isContractElapsed = (contract_info: TContractInfo, tick?: null | TickSpotData) => { if (isEmptyObject(tick) || isEmptyObject(contract_info)) return false; const end_time = getEndTime(contract_info) || 0; if (end_time && tick && tick.epoch) { diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index 90f20a1cd58d..e0873076108c 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -223,6 +223,7 @@ const mock = (): TStores & { is_mock: boolean } => { residence_list: [], should_restrict_bvi_account_creation: false, should_restrict_vanuatu_account_creation: false, + should_show_eu_content: false, fetchAccountSettings: jest.fn(), setAccountSettings: jest.fn(), upgradeable_landing_companies: [], @@ -371,6 +372,7 @@ const mock = (): TStores & { is_mock: boolean } => { setAppContentsScrollRef: jest.fn(), shouldNavigateAfterChooseCrypto: jest.fn(), simple_duration_unit: 't', + should_show_multipliers_onboarding: false, toggleHistoryTab: jest.fn(), toggleLanguageSettingsModal: jest.fn(), togglePositionsDrawer: jest.fn(), @@ -629,7 +631,7 @@ const mock = (): TStores & { is_mock: boolean } => { removeAccountSwitcherListener: jest.fn(), setAccountSwitcherListener: jest.fn(), }, - chart_barrier_store: {}, + chart_barrier_store: {} as TCoreStores['chart_barrier_store'], active_symbols: { active_symbols: [], setActiveSymbols: jest.fn(), diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 60854adbd0e8..74082e878d38 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -258,6 +258,20 @@ type TActionProps = TButtonProps & { route?: string; }; +type TChartStateChangeOption = { + indicator_type_name?: string; + indicators_category_name?: string; + isClosed?: boolean; + is_favorite?: boolean; + is_info_open?: boolean; + is_open?: boolean; + chart_type_name?: string; + search_string?: string; + symbol?: string; + symbol_category?: string; + time_interval_name?: string; +}; + type TNotificationMessage = { action?: TActionProps; className?: string; @@ -461,6 +475,7 @@ type TClientStore = { residence_list: ResidenceList; should_restrict_bvi_account_creation: boolean; should_restrict_vanuatu_account_creation: boolean; + should_show_eu_content: boolean; updateMT5Status: () => Promise; fetchAccountSettings: () => Promise; setAccountSettings: (get_settings_response: GetSettings) => void; @@ -646,6 +661,7 @@ type TUiStore = { is_ready_to_deposit_modal_visible: boolean; reports_route_tab_index: number; should_show_cancellation_warning: boolean; + should_show_multipliers_onboarding: boolean; toggleCancellationWarning: (state_change?: boolean) => void; toggleUnsupportedContractModal: (state_change: boolean) => void; toggleReports: (is_visible: boolean) => void; @@ -803,7 +819,7 @@ type TContractTradeStore = { underlying, }: Partial) => void; updateChartType: (type: string) => void; - updateGranularity: (granularity: number) => void; + updateGranularity: (granularity: number | null) => void; updateProposal: (response: ProposalOpenContract) => void; }; @@ -986,7 +1002,7 @@ type TContractReplay = { }>; }; chart_state: string; - chartStateChange: (state: string, option: Record) => void; + chartStateChange: (state: string, option?: TChartStateChangeOption) => void; error_code?: string; error_message?: string; has_error: boolean; @@ -1041,7 +1057,7 @@ export type TCoreStores = { gtm: TGtmStore; pushwoosh: Record; contract_replay: TContractReplay; - chart_barrier_store: Record; + chart_barrier_store: TBarriers[number]; active_symbols: TActiveSymbolsStore; }; diff --git a/packages/trader/src/App/Components/Elements/chart-loader.tsx b/packages/trader/src/App/Components/Elements/chart-loader.tsx index 06343df6a417..7babd8768db9 100644 --- a/packages/trader/src/App/Components/Elements/chart-loader.tsx +++ b/packages/trader/src/App/Components/Elements/chart-loader.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Loading from '_common/components/loading'; type TChartLoader = { - is_dark: boolean; + is_dark?: boolean; is_visible: boolean; }; diff --git a/packages/trader/src/App/app.tsx b/packages/trader/src/App/app.tsx index faf20885c37d..87ce015eddc5 100644 --- a/packages/trader/src/App/app.tsx +++ b/packages/trader/src/App/app.tsx @@ -5,6 +5,7 @@ import TradeHeaderExtensions from 'App/Containers/trade-header-extensions'; import TradeFooterExtensions from 'App/Containers/trade-footer-extensions'; import TradeSettingsExtensions from 'App/Containers/trade-settings-extensions'; import { NetworkStatusToastErrorPopup } from 'Modules/Trading/Containers/toast-popup'; +import type { TWebSocket } from 'Types'; import initStore from './init-store'; import 'Sass/app.scss'; import type { TCoreStores } from '@deriv/stores/types'; @@ -13,7 +14,7 @@ import TraderProviders from '../trader-providers'; type Apptypes = { passthrough: { root_store: TCoreStores; - WS: unknown; + WS: TWebSocket; }; }; diff --git a/packages/trader/src/App/init-store.js b/packages/trader/src/App/init-store.ts similarity index 53% rename from packages/trader/src/App/init-store.js rename to packages/trader/src/App/init-store.ts index f6ad9f411595..6c751fc675eb 100644 --- a/packages/trader/src/App/init-store.js +++ b/packages/trader/src/App/init-store.ts @@ -1,18 +1,20 @@ import { configure } from 'mobx'; -import RootStore from 'Stores'; import { setWebsocket } from '@deriv/shared'; import ServerTime from '_common/base/server_time'; +import { TCoreStores } from '@deriv/stores/types'; +import type { TWebSocket } from 'Types'; +import RootStore from '../Stores'; configure({ enforceActions: 'observed' }); -let root_store; +let root_store: TCoreStores; -const initStore = (core_store, websocket) => { +const initStore = (core_store: TCoreStores, websocket: TWebSocket) => { if (root_store) return root_store; ServerTime.init(core_store.common); setWebsocket(websocket); - root_store = new RootStore(core_store); + root_store = new RootStore(core_store) as unknown as TCoreStores; return root_store; }; diff --git a/packages/trader/src/Documents/STYLE-GUIDE.MD b/packages/trader/src/Documents/STYLE-GUIDE.MD index 4b2a7fc1850f..1f7278bf8314 100644 --- a/packages/trader/src/Documents/STYLE-GUIDE.MD +++ b/packages/trader/src/Documents/STYLE-GUIDE.MD @@ -2,12 +2,13 @@ ## FILE STRUCTURE -In this project, it is tried to keep relevant files in 7 major directories -which are called `App`, `Constants`, `Documents`, `Modules`, `Services`, +In this project, it is tried to keep relevant files in 7 major directories +which are called `App`, `Constants`, `Documents`, `Modules`, `Services`, `Stores` and `Utils`. We will describe them more in next sections. Below is a sample file structure of the project. + ``` app ├── App/ @@ -27,27 +28,25 @@ app │ ├── Trading/ │ │ ├── Components/ │ │ │ ├── Elements/ - │ │ │ │ ├── full-screen-dialog.jsx │ │ │ │ └── mobile-widget.jsx │ │ │ └── Form/ │ │ │ ├── ContractType/ │ │ │ ├── Purchase/ │ │ │ ├── TradeParams/ - │ │ │ ├── form-layout.jsx - │ │ │ ├── screen-large.jsx - │ │ │ └── screen-small.jsx + │ │ │ ├── form-layout.tsx + │ │ │ ├── screen-large.tsx + │ │ │ └── screen-small.tsx │ │ ├── Containers/ - │ │ │ ├── contract-type.jsx - │ │ │ ├── purchase.jsx - │ │ │ ├── trade.jsx - │ │ │ └── trade-params.jsx + │ │ │ ├── contract-type.tsx + │ │ │ ├── purchase.tsx + │ │ │ ├── trade.tsx + │ │ │ └── trade-params.tsx │ │ ├── Helpers/ - │ │ └── index.js - │ └── Statement/ + │ │ └── index.ts + │ └── Contract/ │ ├── Components/ │ ├── Containers/ - │ ├── Helpers/ - │ └── index.js + │ └── index.ts ├── Services/ ├── Stores/ ├── Utils/ @@ -56,8 +55,8 @@ app └── index.js ``` -This guide is based on presentational and container components. -If you're not sure of the distinction between them, we recommend you read the +This guide is based on presentational and container components. +If you're not sure of the distinction between them, we recommend you read the [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) article first. @@ -77,144 +76,143 @@ This directory contains all documentations of the project (e.g. STYLE-GUIDE.MD). ## MODULES -The predominant part of the project is the `Modules` directory. -All the presentational and container components are classified by functionality -here. Regarding the picture above, each functionality in the project has a -directory in `Modules` directory (e.g. `Trading`) that contains two major subdirectories -`Components` and `Containers`. We will explain more about `Components` and +The predominant part of the project is the `Modules` directory. +All the presentational and container components are classified by functionality +here. Regarding the picture above, each functionality in the project has a +directory in `Modules` directory (e.g. `Trading`) that contains two major subdirectories +`Components` and `Containers`. We will explain more about `Components` and `Containers` directories in further sections. - ### COMPONENTS -This directory contains React components, of either stateless or stateful varieties +This directory contains React components, of either stateless or stateful varieties (e.g. `function` or `class` components). -The optimal structure for components is to place files relevant to a component inside +The optimal structure for components is to place files relevant to a component inside its own subdirectory, e.g.: ``` Components/ ├── Elements/ -│ ├── full-screen-dialog.jsx │ └── mobile-widget.jsx └── Form/ ├── ContractType/ ├── Purchase/ ├── TradeParams/ - ├── form-layout.jsx - ├── screen-large.jsx - └── screen-small.jsx + ├── form-layout.tsx + ├── screen-large.tsx + └── screen-small.tsx ``` The presentational components: -* Are concerned with how things look. -* May contain both presentational and container components inside, -and usually, have some DOM markup and styles of their own. -* Often allow containment via this.props.children. -* Have no dependencies on the rest of the app, such as Flux actions or stores. -* Don’t specify how the data is loaded or mutated. -* Receive data and callbacks exclusively via props. -* Rarely have their own state (when they do, it’s UI state rather than data). -* Are written as functional components unless they need state, lifecycle hooks, -or performance optimizations. -* Examples: Balance, AccountList, UserInfo, ... +- Are concerned with how things look. +- May contain both presentational and container components inside, + and usually, have some DOM markup and styles of their own. +- Often allow containment via this.props.children. +- Have no dependencies on the rest of the app, such as Flux actions or stores. +- Don’t specify how the data is loaded or mutated. +- Receive data and callbacks exclusively via props. +- Rarely have their own state (when they do, it’s UI state rather than data). +- Are written as functional components unless they need state, lifecycle hooks, + or performance optimizations. +- Examples: Balance, AccountList, UserInfo, ... #### Do: -* Keep all files immediately relevant to a component inside the given component directory. - -* Add a basic `index.js` to import the component and export it -to make it dead simple to import the component from elsewhere. -* Feel free to create subdirectories for relevant utils or helpers -if it helps to keep things tidy -* Keep components small, focused and easy to test, breaking up complex -components into smaller components -* Try to create new, reusable components instead of "sub-components" -(i.e. prefer not to create sub-directories that contain more components) -* Connect all components (no matter how small) with `mobx-react` observer -if they use props that come from the store for the best performance -(e.g. the parent component passing store props into the child component) -* Always use `function` to define functional components instead of using ES6 arrow function.[?](https://medium.com/@stevemao/do-not-use-anonymous-functions-to-construct-react-functional-components-c5408ec8f4c7) -* __Be consistent__ — however, you choose to lay things out +- Keep all files immediately relevant to a component inside the given component directory. + +- Add a basic `index.ts` to import the component and export it + to make it dead simple to import the component from elsewhere. +- Feel free to create subdirectories for relevant utils or helpers + if it helps to keep things tidy +- Keep components small, focused and easy to test, breaking up complex + components into smaller components +- Try to create new, reusable components instead of "sub-components" + (i.e. prefer not to create sub-directories that contain more components) +- Connect all components (no matter how small) with `mobx-react` observer + if they use props that come from the store for the best performance + (e.g. the parent component passing store props into the child component) +- Always use `function` to define functional components instead of using ES6 arrow function.[?](https://medium.com/@stevemao/do-not-use-anonymous-functions-to-construct-react-functional-components-c5408ec8f4c7) +- **Be consistent** — however, you choose to lay things out #### Don't: -* Mix concerns (files, modules) that should really belong to other components -* Forget to write tests... :wink: +- Mix concerns (files, modules) that should really belong to other components +- Forget to write tests... :wink: ### CONTAINERS -This directory contains React container components. Container components are a -concept borrowed from typical React-Redux projects. Containers are simply a -bridge between application stores and UI components, making a connection to -the relevant stores and passing down as props the properties and methods +This directory contains React container components. Container components are a +concept borrowed from typical React-Redux projects. Containers are simply a +bridge between application stores and UI components, making a connection to +the relevant stores and passing down as props the properties and methods provided by the connected store(s). -It is easy to know if you should create a component or a container: -if you need use `connect` function to connect a store, use containers otherwise +It is easy to know if you should create a component or a container: +if you need use `connect` function to connect a store, use containers otherwise use presentational components. -The optimal structure for container components is to place files relevant to a +The optimal structure for container components is to place files relevant to a container inside its own subdirectory, e.g.: ``` Containers/ -├── contract-type.jsx -├── purchase.jsx -├── trade.jsx -└── trade-params.jsx +├── contract-type.tsx +├── purchase.tsx +├── trade.tsx +└── trade-params.tsx ``` Containers should follow rules: -* Are concerned with how things work -* May contain both presentational and container components inside but usually -__don’t have any DOM markup of their own__ except for some wrapping divs and -never have any styles. -* Provide the data and behavior to presentational or other container components. -* Call Flux actions and provide these as callbacks to the presentational components. -* Are often stateful, as they tend to serve as data sources. -* Are usually generated using higher order components such as connect() from React -Redux, createContainer() from Relay, or Container.create() from Flux Utils, -rather than written by hand. -* Examples: Trade, Statement, Portfolio, Cashier, ... +- Are concerned with how things work +- May contain both presentational and container components inside but usually + **don’t have any DOM markup of their own** except for some wrapping divs and + never have any styles. +- Provide the data and behavior to presentational or other container components. +- Call Flux actions and provide these as callbacks to the presentational components. +- Are often stateful, as they tend to serve as data sources. +- Are usually generated using higher order components such as connect() from React + Redux, createContainer() from Relay, or Container.create() from Flux Utils, + rather than written by hand. +- Examples: Trade, Statement, Portfolio, Cashier, ... #### Do: -* Connect your container components to the MobX stores -* Maintain a strict policy of giving container components little to no -responsibilities relating to the UI: they ideally will not require styling -because their child components should be the ones with styling applied. -* Keep all files immediately relevant to a container inside the given -container directory. -* Add a basic index.js to import the container and export it to make it dead -simple to import the container from elsewhere. -* __Be consistent__— however, you choose to lay things out. +- Connect your container components to the MobX stores +- Maintain a strict policy of giving container components little to no + responsibilities relating to the UI: they ideally will not require styling + because their child components should be the ones with styling applied. +- Keep all files immediately relevant to a container inside the given + container directory. +- Add a basic index.ts to import the container and export it to make it dead + simple to import the container from elsewhere. +- **Be consistent**— however, you choose to lay things out. #### Don't: -* Overcomplicate containers, they're not supposed to contain a lot of business -logic. Usually, if you're creating methods on the container for complex logic, -you might want to consider hoisting that logic up into the store it's -connected to, or down into one of the components it's rendering. -* Mix concerns (files, modules) that should really belong to child components -or provided stores. -* Forget to write tests... :wink: +- Overcomplicate containers, they're not supposed to contain a lot of business + logic. Usually, if you're creating methods on the container for complex logic, + you might want to consider hoisting that logic up into the store it's + connected to, or down into one of the components it's rendering. +- Mix concerns (files, modules) that should really belong to child components + or provided stores. +- Forget to write tests... :wink: ## SERVICES -All javascript files which are related to sending, fetching and handling WebSocket +All javascript files which are related to sending, fetching and handling WebSocket requests and responses, are kept in `Services` directory. ## STORES -___The structure of this section is tentative.___ -This directory contains everything that is directly related to MobX stores, +**_The structure of this section is tentative._** + +This directory contains everything that is directly related to MobX stores, with each store having its own class definition. -The optimal structure for stores is to place files relevant to a store +The optimal structure for stores is to place files relevant to a store inside its own subdirectory, e.g.: ``` @@ -224,7 +222,7 @@ Stores/ │ │ ├── Actions/ │ │ ├── Constants/ │ │ ├── Helpers/ -│ │ └── trade-store.js +│ │ └── trade-store.ts │ ├── index.js │ └── statement-store.js ├── client-store.js @@ -235,22 +233,24 @@ Stores/ ``` ### Do: -* Keep all files immediately relevant to a store inside the given store directory -* Add a basic index.js to import the store and export it to make it dead simple to -import the store from elsewhere -* Use MobX's @action for action methods, rather than implicit set methods for clarity -* __Be consistent__— however, you choose to lay things out + +- Keep all files immediately relevant to a store inside the given store directory +- Add a basic index.js to import the store and export it to make it dead simple to + import the store from elsewhere +- Use MobX's @action for action methods, rather than implicit set methods for clarity +- **Be consistent**— however, you choose to lay things out ### Don't: -* Place anything React-y inside stores— MobX is not reliant on React and uses -no JSX, only basic ES6 classes and @ decorators. -* Instantiate stores (i.e. use new keyword) inside store definition files— -stores are instantiated in the createStore module to avoid stores becoming singletons. -* Be afraid to use side-effects in actions, such as API calls -* Forget to write tests... :wink: + +- Place anything React-y inside stores— MobX is not reliant on React and uses + no JSX, only basic ES6 classes and @ decorators. +- Instantiate stores (i.e. use new keyword) inside store definition files— + stores are instantiated in the createStore module to avoid stores becoming singletons. +- Be afraid to use side-effects in actions, such as API calls +- Forget to write tests... :wink: ## UTILS The `Utils` directory contains all javascript files that are used on other parts -of the project to help and simplify some routine processes +of the project to help and simplify some routine processes (e.g. Date - to handle date formatting and conversion). diff --git a/packages/trader/src/Modules/Contract/Components/Digits/digits.tsx b/packages/trader/src/Modules/Contract/Components/Digits/digits.tsx index cc5fd26df063..c860281a7f92 100644 --- a/packages/trader/src/Modules/Contract/Components/Digits/digits.tsx +++ b/packages/trader/src/Modules/Contract/Components/Digits/digits.tsx @@ -2,14 +2,8 @@ import classNames from 'classnames'; import React from 'react'; import { toJS } from 'mobx'; import { DesktopWrapper, MobileWrapper, Popover, Text } from '@deriv/components'; -import { - getMarketNamesMap, - isMobile, - useIsMounted, - isContractElapsed, - TContractStore, - TTickSpotData, -} from '@deriv/shared'; +import { TickSpotData } from '@deriv/api-types'; +import { getMarketNamesMap, isMobile, useIsMounted, isContractElapsed, TContractStore } from '@deriv/shared'; import { Localize } from '@deriv/translations'; import { Bounce, SlideIn } from 'App/Components/Animations'; import { DigitSpot, LastDigitPrediction } from '../LastDigitPrediction'; @@ -38,12 +32,12 @@ type TDigits = Pick & { onDigitChange?: TTraderStore['onChange']; selected_digit?: TTraderStore['last_digit']; trade_type?: TTraderStore['contract_type']; - tick?: TTickSpotData; + tick?: TickSpotData | null; underlying?: TTraderStore['symbol']; }; type TTickStream = NonNullable[number]; type TTickData = - | TTickSpotData + | TickSpotData | null | undefined | { diff --git a/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx b/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx index 3f09a3395f62..19ff8f6633c1 100644 --- a/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx +++ b/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx @@ -82,6 +82,7 @@ const ContractReplay = observer(({ contract_id }: { contract_id: number }) => { setIsVisible(false); const is_from_table_row = !isEmptyObject(state) ? state.from_table_row : false; return is_from_table_row ? history.goBack() : routeBackInApp(history); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [history, routeBackInApp]); React.useEffect(() => { diff --git a/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx b/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx index 161dd25a7d71..5488eaa6dfdb 100644 --- a/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx +++ b/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { usePrevious } from '@deriv/components'; import { getDurationPeriod, getDurationUnitText, getEndTime, getPlatformRedirect, isDesktop } from '@deriv/shared'; -import SmartChartSwitcher from '../../Trading/Containers/smart-chart-switcher.jsx'; -import { ChartBottomWidgets, ChartTopWidgets } from './contract-replay-widget'; import ChartMarker from 'Modules/SmartChart/Components/Markers/marker'; import DelayedAccuBarriersMarker from 'Modules/SmartChart/Components/Markers/delayed-accu-barriers-marker'; import allMarkers from 'Modules/SmartChart/Components/all-markers.jsx'; import ChartMarkerBeta from 'Modules/SmartChartBeta/Components/Markers/marker.jsx'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; +import { ChartBottomWidgets, ChartTopWidgets } from './contract-replay-widget'; +import SmartChartSwitcher from '../../Trading/Containers/smart-chart-switcher'; const ReplayChart = observer( ({ @@ -90,11 +90,11 @@ const ReplayChart = observer( id='replay' is_beta={is_beta_chart} barriers={barriers_array} - bottomWidgets={isBottomWidgetVisible() ? ChartBottomWidgets : null} + bottomWidgets={isBottomWidgetVisible() ? ChartBottomWidgets : undefined} chartControlsWidgets={null} chartType={chart_type} endEpoch={end_epoch} - margin={margin || null} + margin={margin} isMobile={is_mobile} enabledNavigationWidget={isDesktop()} enabledChartFooter={false} @@ -115,7 +115,7 @@ const ReplayChart = observer( isConnectionOpened={is_socket_opened} isStaticChart={ // forcing chart reload when start_epoch changes to an earlier epoch for ACCU closed contract: - is_accumulator_contract && end_epoch && Number(start_epoch) < Number(prev_start_epoch) + !!is_accumulator_contract && !!end_epoch && Number(start_epoch) < Number(prev_start_epoch) } shouldFetchTradingTimes={false} should_zoom_out_on_yaxis={is_accumulator_contract} diff --git a/packages/trader/src/Modules/SmartChart/Components/Markers/__tests__/accumulators-chart-elements.spec.tsx b/packages/trader/src/Modules/SmartChart/Components/Markers/__tests__/accumulators-chart-elements.spec.tsx index 16b7198b48a5..19d8f6578a28 100644 --- a/packages/trader/src/Modules/SmartChart/Components/Markers/__tests__/accumulators-chart-elements.spec.tsx +++ b/packages/trader/src/Modules/SmartChart/Components/Markers/__tests__/accumulators-chart-elements.spec.tsx @@ -31,7 +31,7 @@ describe('AccumulatorsChartElements', () => { profit: 120, }, }, - ], + ] as React.ComponentProps['all_positions'], current_spot: 9478.34, current_spot_time: 1234567890, has_crossed_accu_barriers: false, diff --git a/packages/trader/src/Modules/SmartChart/Components/Markers/accumulators-chart-elements.tsx b/packages/trader/src/Modules/SmartChart/Components/Markers/accumulators-chart-elements.tsx index 600f479e76ea..74c9d6b528a2 100644 --- a/packages/trader/src/Modules/SmartChart/Components/Markers/accumulators-chart-elements.tsx +++ b/packages/trader/src/Modules/SmartChart/Components/Markers/accumulators-chart-elements.tsx @@ -1,26 +1,21 @@ import { filterByContractType } from 'App/Components/Elements/PositionsDrawer/helpers/positions-helper'; import React from 'react'; +import { useStore } from '@deriv/stores'; import AccumulatorsProfitLossTooltip from './accumulators-profit-loss-tooltip'; -import { ProposalOpenContract } from '@deriv/api-types'; import ChartMarkerBeta from 'Modules/SmartChartBeta/Components/Markers/marker.jsx'; import ChartMarker from './marker'; -type TPositions = { - contract_info: Omit< - React.ComponentProps, - 'className' | 'alignment' | 'should_show_profit_text' - > & - Required>; -}; +type TPortfolioStore = ReturnType['portfolio']; type TAccumulatorsChartElements = { - all_positions: TPositions[]; - current_spot: number; - current_spot_time: number; + all_positions: TPortfolioStore['all_positions']; + current_spot?: number; + current_spot_time?: number; has_crossed_accu_barriers: boolean; should_show_profit_text: React.ComponentProps['should_show_profit_text']; symbol: string; is_beta_chart?: boolean; + is_mobile?: boolean; }; const AccumulatorsChartElements = ({ @@ -31,6 +26,7 @@ const AccumulatorsChartElements = ({ should_show_profit_text, symbol, is_beta_chart, + is_mobile, }: TAccumulatorsChartElements) => { const accumulators_positions = all_positions.filter( ({ contract_info }) => @@ -48,9 +44,10 @@ const AccumulatorsChartElements = ({ {...contract_info} should_show_profit_text={should_show_profit_text} is_beta_chart={is_beta_chart} + is_mobile={is_mobile} /> ))} - {has_crossed_accu_barriers && !!current_spot_time && ( + {has_crossed_accu_barriers && !!current_spot_time && !!current_spot && ( ; - -type TAccumulatorsProfitLossText = React.ComponentProps; +type TContractInfo = ReturnType['portfolio']['all_positions'][number]['contract_info']; type TAccumulatorsProfitLossTooltip = { alignment?: string; + className?: string; should_show_profit_text?: boolean; is_beta_chart?: boolean; -} & TPickProposalOpenContract & - TAccumulatorsProfitLossText; + is_mobile?: boolean; +} & TContractInfo; export type TRef = { setPosition: (position: { epoch: number | null; price: number | null }) => void; @@ -40,9 +35,10 @@ const AccumulatorsProfitLossTooltip = ({ profit, should_show_profit_text, is_beta_chart, + is_mobile, }: TAccumulatorsProfitLossTooltip) => { const [is_tooltip_open, setIsTooltipOpen] = React.useState(false); - const won = profit >= 0; + const won = Number(profit) >= 0; const tooltip_timeout = React.useRef>(); React.useEffect(() => { @@ -122,10 +118,10 @@ const AccumulatorsProfitLossTooltip = ({ classNames={`${className}__content`} >
- + {localize('Total profit/loss:')} - +
diff --git a/packages/trader/src/Modules/SmartChart/Components/toolbar-widgets.tsx b/packages/trader/src/Modules/SmartChart/Components/toolbar-widgets.tsx index be43d14d0704..24265e4bf825 100644 --- a/packages/trader/src/Modules/SmartChart/Components/toolbar-widgets.tsx +++ b/packages/trader/src/Modules/SmartChart/Components/toolbar-widgets.tsx @@ -2,14 +2,14 @@ import React from 'react'; import { isDesktop } from '@deriv/shared'; import { ChartMode, DrawTools, Share, StudyLegend, Views, ToolbarWidget } from 'Modules/SmartChart'; -type TToolbarWidgets = { +type TToolbarWidgetsProps = { is_mobile?: boolean; position?: string; updateChartType: (type: string) => void; updateGranularity: (granularity: number) => void; }; -const ToolbarWidgets = ({ is_mobile, position, updateChartType, updateGranularity }: TToolbarWidgets) => { +const ToolbarWidgets = ({ is_mobile, position, updateChartType, updateGranularity }: TToolbarWidgetsProps) => { return ( diff --git a/packages/trader/src/Modules/SmartChart/Components/top-widgets.tsx b/packages/trader/src/Modules/SmartChart/Components/top-widgets.tsx index 767bbe38166f..642d04fce575 100644 --- a/packages/trader/src/Modules/SmartChart/Components/top-widgets.tsx +++ b/packages/trader/src/Modules/SmartChart/Components/top-widgets.tsx @@ -18,9 +18,9 @@ type TTopWidgets = { onSymbolChange?: ReturnType['onChange']; open?: boolean; open_market?: { - category: string | null; - subcategory?: string | null; - }; + category?: string; + subcategory?: string; + } | null; theme?: string; y_axis_width?: number; }; diff --git a/packages/trader/src/Modules/SmartChartBeta/Components/toolbar-widgets.jsx b/packages/trader/src/Modules/SmartChartBeta/Components/toolbar-widgets.tsx similarity index 64% rename from packages/trader/src/Modules/SmartChartBeta/Components/toolbar-widgets.jsx rename to packages/trader/src/Modules/SmartChartBeta/Components/toolbar-widgets.tsx index 098a135f3acd..f628b3425a5e 100644 --- a/packages/trader/src/Modules/SmartChartBeta/Components/toolbar-widgets.jsx +++ b/packages/trader/src/Modules/SmartChartBeta/Components/toolbar-widgets.tsx @@ -1,6 +1,5 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import { isDesktop, isMobile } from '@deriv/shared'; +import { isDesktop } from '@deriv/shared'; import { ChartModeBeta, DrawToolsBeta, @@ -10,9 +9,16 @@ import { ToolbarWidgetBeta, } from 'Modules/SmartChartBeta'; -const ToolbarWidgetsBeta = ({ position, updateChartType, updateGranularity }) => { +type TToolbarWidgetsBetaProps = { + is_mobile?: boolean; + position?: string; + updateChartType: (type: string) => void; + updateGranularity: (granularity: number) => void; +}; + +const ToolbarWidgetsBeta = ({ is_mobile, position, updateChartType, updateGranularity }: TToolbarWidgetsBetaProps) => { return ( - + {isDesktop() && } {isDesktop() && } @@ -22,10 +28,4 @@ const ToolbarWidgetsBeta = ({ position, updateChartType, updateGranularity }) => ); }; -ToolbarWidgetsBeta.propTypes = { - position: PropTypes.string, - updateChartType: PropTypes.func, - updateGranularity: PropTypes.func, -}; - export default React.memo(ToolbarWidgetsBeta); diff --git a/packages/trader/src/Modules/Trading/Containers/chart-widgets.tsx b/packages/trader/src/Modules/Trading/Containers/chart-widgets.tsx index ff46bc45f217..4e16fe69ab24 100644 --- a/packages/trader/src/Modules/Trading/Containers/chart-widgets.tsx +++ b/packages/trader/src/Modules/Trading/Containers/chart-widgets.tsx @@ -9,7 +9,6 @@ import { observer, useStore } from '@deriv/stores'; type TDigits = React.ComponentProps; type TChartTopWidgets = { - charts_ref?: { chart: { yAxiswidth: number } } | null; open_market: React.ComponentProps['open_market']; open: React.ComponentProps['open']; }; @@ -48,16 +47,13 @@ export const DigitsWidget = observer(({ digits, tick }: { digits: TDigits['digit }); // Chart widgets passed into SmartCharts -export const ChartTopWidgets = observer(({ charts_ref, open_market, open }: TChartTopWidgets) => { +export const ChartTopWidgets = observer(({ open_market, open }: TChartTopWidgets) => { const { client, ui } = useStore(); const { is_digits_widget_active, onChange: onSymbolChange } = useTraderStore(); const { is_beta_chart } = client; const { is_dark_mode_on, is_mobile } = ui; const theme = is_dark_mode_on ? 'dark' : 'light'; let yAxiswidth; - if (charts_ref?.chart) { - yAxiswidth = charts_ref.chart.yAxiswidth; - } return ( { - const Chart = is_beta ? SmartChartBeta : SmartChart; - return ; -}; - -export default SmartChartSwitcher; diff --git a/packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.tsx b/packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.tsx new file mode 100644 index 000000000000..d91046676b80 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { SmartChart } from 'Modules/SmartChart'; +import { SmartChartBeta } from 'Modules/SmartChartBeta'; +import { + ActiveSymbols, + ActiveSymbolsRequest, + AuditDetailsForExpiredContract, + ProposalOpenContract, + ServerTimeRequest, + TickSpotData, + TicksHistoryRequest, + TicksHistoryResponse, + TicksStreamResponse, + TradingTimes, + TradingTimesRequest, +} from '@deriv/api-types'; +import { useStore } from '@deriv/stores'; +import type { TBottomWidgetsParams } from './trade'; +import type { TChartLayout, TChartStateChangeOption } from 'Stores/Modules/Trading/trade-store'; + +type TMarkersArray = ReturnType['contract_trade']['markers_array']; +type TGetContractsArray = ReturnType['contract_replay']['contract_store']['getContractsArray']; +type TBarriers = ReturnType['portfolio']['barriers']; + +type TContractsArray = TMarkersArray | ReturnType; + +type TGetIndicatorHeightRatio = (chart_height: number, indicator_count: number) => TRatio; + +type TInitialChartData = { + activeSymbols?: ActiveSymbols; + masterData?: TQuote[]; + tradingTimes?: TradingTimes; +}; + +type TLanguage = { + icon: JSX.Element; + key: string; + name: string; +}; + +type TNotification = { + category: string; + text: string; + type?: 'info' | 'warning' | 'success' | 'error' | 'warning'; +}; + +type TOHLCData = { + close: string; + epoch: number; + granularity: TicksHistoryRequest['granularity']; + high: string; + id: string; + low: string; + open: string; + open_time: number; + symbol: string; +}; + +type TSettings = { + activeLanguages?: Array | null; + countdown?: boolean; + enabledNavigationWidget?: boolean; + historical?: boolean; + isAutoScale?: boolean; + isHighestLowestMarkerEnabled?: boolean; + lang?: string; + language?: string; + minimumLeftBars?: number; + position?: string; + theme?: string; + whitespace?: number; +}; + +type TQuote = { + Close: number; + Date: string; + DT?: Date; + High?: number; + ohlc?: TOHLCData; + Open?: number; + Low?: number; + tick?: TickSpotData; + prevClose?: number; + Volume?: number; +}; + +type TRatio = { + height: number; + percent: number; +}; + +type TSmartChartProps = React.PropsWithChildren<{ + allowTickChartTypeOnly?: boolean; + allTicks?: NonNullable['all_ticks']; + anchorChartToLeft?: boolean; + barriers?: TBarriers; + bottomWidgets?: (props: TBottomWidgetsParams) => React.ReactElement; + chartControlsWidgets?: ((props: { isMobile?: boolean }) => React.ReactElement) | null; + chartData?: TInitialChartData; + chartStatusListener?: (isChartReady: boolean) => void; + chartType?: string; + clearChart?: boolean; + contractInfo?: ProposalOpenContract; + contracts_array: TContractsArray; + crosshair?: number; + crosshairState?: number | null; + crosshairTooltipLeftAllow?: number | null; + enabledChartFooter?: boolean; + enabledNavigationWidget?: boolean; + enableRouting?: boolean; + enableScroll?: boolean | null; + enableZoom?: boolean | null; + endEpoch?: number; + feedCall?: { activeSymbols?: boolean; tradingTimes?: boolean }; + getIndicatorHeightRatio?: TGetIndicatorHeightRatio; + getMarketsOrder?: (active_symbols: ActiveSymbols) => string[]; + granularity?: number; + hasAlternativeSource?: boolean; + historical?: boolean; + id?: string; + importedLayout?: TChartLayout | null; + initialData?: TInitialChartData; + isAnimationEnabled?: boolean; + isConnectionOpened?: boolean; + isLive?: boolean; + isMobile?: boolean; + isStaticChart?: boolean; + leftMargin?: number; + margin?: number; + maxTick?: number | null; + networkStatus?: { + class: string; + tooltip: string; + }; + onCrosshairChange?: (state?: number | null) => void | null; + onExportLayout?: (currentLayout: TChartLayout) => void; + onMessage?: (message: TNotification) => void; + onSettingsChange?: (newSettings: Omit) => void; + refreshActiveSymbols?: boolean; + requestAPI: (req: TradingTimesRequest | ActiveSymbolsRequest | ServerTimeRequest) => void; + requestForget: (req: TicksHistoryRequest) => void; + requestForgetStream?: (stream_id: string) => void; + requestSubscribe: ( + req: TicksHistoryRequest, + callback: (response: TicksHistoryResponse | TicksStreamResponse) => void + ) => void; + scrollToEpoch?: number | null; + settings?: TSettings; + shouldDrawTicksFromContractInfo?: boolean; + shouldFetchTickHistory?: boolean; + shouldFetchTradingTimes?: boolean; + should_show_eu_content?: boolean; + should_zoom_out_on_yaxis?: boolean; + showLastDigitStats?: boolean; + startEpoch?: number; + startWithDataFitMode?: boolean; + stateChangeListener?: (state: string, option?: TChartStateChangeOption) => void; + symbol?: string; + toolbarWidget?: () => React.ReactElement; + topWidgets?: () => React.ReactElement; + yAxisMargin?: { bottom?: number; top: number }; + zoom?: number; +}>; + +type TSmartChartSwitcherProps = TSmartChartProps & { + is_beta: boolean; +}; + +const SmartChartSwitcher = ({ is_beta, ...props }: TSmartChartSwitcherProps) => { + const Chart = is_beta ? SmartChartBeta : SmartChart; + return ; +}; + +export default SmartChartSwitcher; diff --git a/packages/trader/src/Modules/Trading/Containers/test.jsx b/packages/trader/src/Modules/Trading/Containers/test.jsx deleted file mode 100644 index 18354bc3ae0e..000000000000 --- a/packages/trader/src/Modules/Trading/Containers/test.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import { toJS } from 'mobx'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { useTraderStore } from 'Stores/useTraderStores'; -import { observer, useStore } from '@deriv/stores'; - -const styles = { - container: { - fontSize: '10px', - lineHeight: '15px', - position: 'absolute', - zIndex: 1, - background: 'rgba(0, 0, 0, 0.8)', - color: '#ccc', - padding: '10px', - marginTop: '-10px', - display: 'none', - overflowY: 'auto', - height: '100%', - width: '100%', - }, - prop_name: { - color: 'yellowgreen', - }, - tabs: { display: 'flex', textAlign: 'center', marginBottom: '10px' }, - tab: { - fontSize: '18px', - border: '1px solid grey', - width: '100%', - padding: '10px', - }, -}; - -const Test = observer(() => { - const stores = useStore(); - const trade_store = useTraderStore(); - const test_stores = { - trade: Object.entries(trade_store), - client: Object.entries(stores.client), - ui: Object.entries(stores.ui), - portfolio: Object.entries(stores.portfolio), - }; - - const [is_visible, setIsVisible] = React.useState(false); - const [store, setStore] = React.useState('trade'); - - React.useEffect(() => { - document.addEventListener('keyup', stateVisibility, false); - return () => { - document.removeEventListener('keyup', stateVisibility); - }; - }); - - const stateVisibility = e => { - // Ctrl + s - if (e.ctrlKey && e.keyCode === 83) setIsVisible(!is_visible); - }; - - const renderStoreContent = ([k, v]) => { - return ( - k !== 'root_store' && - typeof v !== 'function' && ( -
- {k}:{' '} - {v && typeof v === 'object' ? JSON.stringify(toJS(v), null, 1) : v} -
- ) - ); - }; - - const { container, tab, tabs } = styles; - - return ( - -
- {Object.keys(test_stores).map(storage => ( -

setStore(storage)} - style={{ ...tab, fontWeight: storage === store && 'bold' }} - > - {storage} -

- ))} -
- {test_stores[store].sort().map(renderStoreContent)} -
- ); -}); - -Test.propTypes = { - entries: PropTypes.array, -}; - -export default Test; diff --git a/packages/trader/src/Modules/Trading/Containers/trade-chart.tsx b/packages/trader/src/Modules/Trading/Containers/trade-chart.tsx new file mode 100644 index 000000000000..bd683e6c99fb --- /dev/null +++ b/packages/trader/src/Modules/Trading/Containers/trade-chart.tsx @@ -0,0 +1,199 @@ +import React from 'react'; +import { ActiveSymbols } from '@deriv/api-types'; +import { getDecimalPlaces, isDesktop } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { useTraderStore } from 'Stores/useTraderStores'; +import { ChartBottomWidgets } from './chart-widgets'; +import SmartChartSwitcher from './smart-chart-switcher'; +import AccumulatorsChartElements from '../../SmartChart/Components/Markers/accumulators-chart-elements'; +import ToolbarWidgets from '../../SmartChart/Components/toolbar-widgets'; +import ToolbarWidgetsBeta from '../../SmartChartBeta/Components/toolbar-widgets'; +import AllMarkers from '../../SmartChart/Components/all-markers.jsx'; +import type { TBottomWidgetsParams } from './trade'; + +type TTradeChartProps = { + bottomWidgets?: (props: TBottomWidgetsParams) => React.ReactElement; + is_accumulator: boolean; + topWidgets: () => React.ReactElement; +}; + +const TradeChart = observer((props: TTradeChartProps) => { + const { is_accumulator, topWidgets } = props; + const { client, ui, common, contract_trade, portfolio } = useStore(); + const { + accumulator_barriers_data, + accumulator_contract_barriers_data, + chart_type, + granularity, + has_crossed_accu_barriers, + markers_array, + updateChartType, + updateGranularity, + } = contract_trade; + const { all_positions } = portfolio; + const { is_chart_countdown_visible, is_chart_layout_default, is_dark_mode_on, is_mobile, is_positions_drawer_on } = + ui; + const { current_language, is_socket_opened } = common; + const { currency, is_beta_chart, should_show_eu_content } = client; + const { + active_symbols, + barriers_flattened: extra_barriers, + chartStateChange, + chart_layout, + exportLayout, + has_alternative_source, + is_trade_enabled, + main_barrier_flattened: main_barrier, + setChartStatus, + show_digits_stats, + symbol, + wsForget, + wsForgetStream, + wsSendRequest, + wsSubscribe, + } = useTraderStore(); + + const settings = { + countdown: is_chart_countdown_visible, + isHighestLowestMarkerEnabled: false, // TODO: Pending UI, + language: current_language.toLowerCase(), + position: is_chart_layout_default ? 'bottom' : 'left', + theme: is_dark_mode_on ? 'dark' : 'light', + ...(is_accumulator ? { whitespace: 190, minimumLeftBars: is_mobile ? 3 : undefined } : {}), + }; + + const { current_spot, current_spot_time } = accumulator_barriers_data || {}; + + const getBottomWidgets = React.useCallback( + ({ digits, tick }: TBottomWidgetsParams) => ( + + ), + [is_accumulator] + ); + + const getMarketsOrder = (active_symbols: ActiveSymbols): string[] => { + const synthetic_index = 'synthetic_index'; + const has_synthetic_index = active_symbols.some(s => s.market === synthetic_index); + return active_symbols + .slice() + .sort((a, b) => (a.display_name < b.display_name ? -1 : 1)) + .map(s => s.market) + .reduce( + (arr, market) => { + if (arr.indexOf(market) === -1) arr.push(market); + return arr; + }, + has_synthetic_index ? [synthetic_index] : [] + ); + }; + + const barriers = main_barrier ? [main_barrier, ...extra_barriers] : extra_barriers; + + // max ticks to display for mobile view for tick chart + const max_ticks = granularity === 0 ? 8 : 24; + + if (!symbol || !active_symbols.length) return null; + + return ( + setChartStatus(!v)} + chartType={chart_type} + clearChart={false} + contracts_array={markers_array} + crosshair={is_mobile ? 0 : undefined} + crosshairTooltipLeftAllow={560} + feedCall={{ + activeSymbols: false, + }} + enabledNavigationWidget={isDesktop()} + enabledChartFooter={false} + getMarketsOrder={getMarketsOrder} + granularity={show_digits_stats || is_accumulator ? 0 : Number(granularity)} + hasAlternativeSource={has_alternative_source} + id='trade' + importedLayout={chart_layout} + initialData={{ + activeSymbols: JSON.parse(JSON.stringify(active_symbols)), + }} + isConnectionOpened={is_socket_opened} + isLive + isMobile={is_mobile} + is_beta={is_beta_chart} + leftMargin={isDesktop() && is_positions_drawer_on ? 328 : 80} + maxTick={is_mobile ? max_ticks : undefined} + onExportLayout={exportLayout} + requestAPI={wsSendRequest} + requestForget={wsForget} + requestForgetStream={wsForgetStream} + requestSubscribe={wsSubscribe} + settings={settings} + shouldFetchTradingTimes + should_show_eu_content={should_show_eu_content} + should_zoom_out_on_yaxis={is_accumulator} + showLastDigitStats={isDesktop() ? show_digits_stats : false} + stateChangeListener={chartStateChange} + symbol={symbol} + toolbarWidget={() => { + if (is_beta_chart) { + return ( + + ); + } + return ( + + ); + }} + topWidgets={is_trade_enabled ? topWidgets : undefined} + yAxisMargin={{ + top: is_mobile ? 76 : 106, + }} + > + {!is_beta_chart && + markers_array.map(marker => { + const Marker = AllMarkers[marker.type as keyof typeof AllMarkers]; + return ( + + ); + })} + {is_accumulator && ( + + )} + + ); +}); +export default TradeChart; diff --git a/packages/trader/src/Modules/Trading/Containers/trade.jsx b/packages/trader/src/Modules/Trading/Containers/trade.jsx deleted file mode 100644 index c0ee53d9cc2e..000000000000 --- a/packages/trader/src/Modules/Trading/Containers/trade.jsx +++ /dev/null @@ -1,437 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { DesktopWrapper, Div100vhContainer, MobileWrapper, SwipeableWrapper } from '@deriv/components'; -import { getDecimalPlaces, isDesktop } from '@deriv/shared'; -import ChartLoader from 'App/Components/Elements/chart-loader'; -import PositionsDrawer from 'App/Components/Elements/PositionsDrawer'; -import MarketIsClosedOverlay from 'App/Components/Elements/market-is-closed-overlay'; -import Test from './test.jsx'; -import { ChartBottomWidgets, ChartTopWidgets, DigitsWidget } from './chart-widgets'; -import FormLayout from '../Components/Form/form-layout'; -import AllMarkers from '../../SmartChart/Components/all-markers.jsx'; -import AccumulatorsChartElements from '../../SmartChart/Components/Markers/accumulators-chart-elements'; -import ToolbarWidgets from '../../SmartChart/Components/toolbar-widgets'; -import ToolbarWidgetsBeta from '../../SmartChartBeta/Components/toolbar-widgets.jsx'; -import { useTraderStore } from 'Stores/useTraderStores'; -import { observer, useStore } from '@deriv/stores'; - -const BottomWidgetsMobile = ({ tick, digits, setTick, setDigits }) => { - React.useEffect(() => { - setTick(tick); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tick]); - - React.useEffect(() => { - setDigits(digits); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [digits]); - - // render nothing for bottom widgets on chart in mobile - return null; -}; - -const Trade = observer(() => { - const { client, common, ui } = useStore(); - const { - form_components, - getFirstOpenMarket, - should_show_active_symbols_loading, - is_chart_loading, - is_market_closed, - is_trade_enabled, - onChange, - onMount, - onUnmount, - prepareTradeStore, - setContractTypes, - setMobileDigitView, - setIsDigitsWidgetActive, - show_digits_stats, - is_accumulator, - symbol, - is_synthetics_available, - is_synthetics_trading_market_available, - is_turbos, - is_vanilla, - } = useTraderStore(); - const { - notification_messages_ui: NotificationMessages, - has_only_forward_starting_contracts: is_market_unavailable_visible, - should_show_multipliers_onboarding, - is_dark_mode_on: is_dark_theme, - is_mobile, - } = ui; - const { is_eu } = client; - const { network_status } = common; - - const [digits, setDigits] = React.useState([]); - const [tick, setTick] = React.useState({}); - const [try_synthetic_indices, setTrySyntheticIndices] = React.useState(false); - const [try_open_markets, setTryOpenMarkets] = React.useState(false); - const [category, setCategory] = React.useState(null); - const [subcategory, setSubcategory] = React.useState(null); - const [swipe_index, setSwipeIndex] = React.useState(0); - const charts_ref = React.useRef(); - - const open_market = React.useMemo(() => { - if (try_synthetic_indices) { - return { category: 'synthetics' }; - } else if (try_open_markets && category) { - return { category, subcategory }; - } - return null; - }, [try_synthetic_indices, try_open_markets, category, subcategory]); - - React.useEffect(() => { - onMount(); - if (!is_synthetics_available) { - const setMarket = async () => { - const markets_to_search = ['forex', 'indices', 'commodities']; // none-synthetic - const { category: market_cat, subcategory: market_subcat } = - (await getFirstOpenMarket(markets_to_search)) ?? {}; - if (market_cat) { - setCategory(market_cat); - setSubcategory(market_subcat); - } - }; - - setMarket(); - } - return () => onUnmount(); - }, [onMount, onUnmount, getFirstOpenMarket, is_synthetics_available]); - - React.useEffect(() => { - if (is_mobile) { - setDigits([]); - } - setTrySyntheticIndices(false); - setTryOpenMarkets(false); - }, [is_mobile, symbol, setDigits, setTrySyntheticIndices, is_synthetics_available]); - - React.useEffect(() => { - const selectMultipliers = async () => { - await setContractTypes(); - - onChange({ target: { name: 'contract_type', value: 'multiplier' } }); - }; - if (should_show_multipliers_onboarding && !is_chart_loading && (is_synthetics_available || !is_market_closed)) { - selectMultipliers(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [should_show_multipliers_onboarding, is_chart_loading]); - - const bottomWidgets = React.useCallback(({ digits: d, tick: t }) => { - return ; - }, []); - - const onChangeSwipeableIndex = index => { - setMobileDigitView(index === 0); - setIsDigitsWidgetActive(index === 0); - setSwipeIndex(index); - }; - - const onTryOtherMarkets = async () => { - if (!is_synthetics_available) { - setTryOpenMarkets(true); - setTimeout(() => setTryOpenMarkets(false)); - } else { - setTrySyntheticIndices(true); - setTimeout(() => setTrySyntheticIndices(false)); - } - }; - - const topWidgets = React.useCallback( - ({ ...params }) => ( - - ), - [open_market, try_synthetic_indices, try_open_markets] - ); - - const form_wrapper_class = is_mobile ? 'mobile-wrapper' : 'sidebar__container desktop-only'; - const chart_height_offset = React.useMemo(() => { - if (is_accumulator) return '295px'; - if (is_turbos) return '300px'; - return '259px'; - }, [is_turbos, is_accumulator]); - - return ( -
- - - - {/* Div100vhContainer is workaround for browsers on devices - with toolbars covering screen height, - using css vh is not returning correct screen height */} - - - } - > - -
- - -
-
- - - - {show_digits_stats && } - - - -
- - {/* Remove Test component for debugging below for production release */} - -
-
- {is_market_closed && !is_market_unavailable_visible && ( - - )} - 0 && network_status.class === 'online' - } - /> -
-
- ); -}); - -export default Trade; - -// CHART (ChartTrade)-------------------------------------------------------- - -/* eslint-disable */ -import SmartChartSwitcher from './smart-chart-switcher.jsx'; - -const SmartChartWithRef = React.forwardRef((props, ref) => ); - -// ChartMarkers -------------------------- -const ChartMarkers = observer(config => { - const { ui, client, contract_trade } = useStore(); - const { markers_array, granularity } = contract_trade; - const { is_dark_mode_on: is_dark_theme } = ui; - const { currency } = client; - return markers_array.map(marker => { - const Marker = AllMarkers[marker.type]; - return ( - - ); - }); -}); - -const ChartTrade = observer(props => { - const { is_accumulator, end_epoch, topWidgets, charts_ref } = props; - const { client, ui, common, contract_trade, portfolio } = useStore(); - const { - accumulator_barriers_data, - accumulator_contract_barriers_data, - chart_type, - granularity, - markers_array, - has_crossed_accu_barriers, - updateGranularity, - updateChartType, - } = contract_trade; - const { all_positions } = portfolio; - const { is_chart_layout_default, is_chart_countdown_visible, is_dark_mode_on, is_positions_drawer_on, is_mobile } = - ui; - const { is_socket_opened, current_language } = common; - const { currency, is_beta_chart, should_show_eu_content } = client; - const { - chartStateChange, - is_trade_enabled, - main_barrier_flattened: main_barrier, - barriers_flattened: extra_barriers, - show_digits_stats, - symbol, - exportLayout, - setChartStatus, - chart_layout, - wsForget, - wsForgetStream, - wsSendRequest, - wsSubscribe, - active_symbols, - has_alternative_source, - } = useTraderStore(); - - const settings = { - assetInformation: false, // ui.is_chart_asset_info_visible, - countdown: is_chart_countdown_visible, - isHighestLowestMarkerEnabled: false, // TODO: Pending UI, - language: current_language.toLowerCase(), - position: is_chart_layout_default ? 'bottom' : 'left', - theme: is_dark_mode_on ? 'dark' : 'light', - ...(is_accumulator ? { whitespace: 190, minimumLeftBars: is_mobile ? 3 : undefined } : {}), - }; - - const { current_spot, current_spot_time } = accumulator_barriers_data || {}; - - const bottomWidgets = React.useCallback( - ({ digits, tick }) => ( - - ), - [is_accumulator] - ); - - const getMarketsOrder = active_symbols => { - const synthetic_index = 'synthetic_index'; - - const has_synthetic_index = !!active_symbols.find(s => s.market === synthetic_index); - return active_symbols - .slice() - .sort((a, b) => (a.display_name < b.display_name ? -1 : 1)) - .map(s => s.market) - .reduce( - (arr, market) => { - if (arr.indexOf(market) === -1) arr.push(market); - return arr; - }, - has_synthetic_index ? [synthetic_index] : [] - ); - }; - - const barriers = main_barrier ? [main_barrier, ...extra_barriers] : extra_barriers; - - // max ticks to display for mobile view for tick chart - const max_ticks = granularity === 0 ? 8 : 24; - - if (!symbol || active_symbols.length === 0) return null; - - return ( - setChartStatus(!v)} - chartType={chart_type} - initialData={{ - activeSymbols: JSON.parse(JSON.stringify(active_symbols)), - }} - chartData={{ - activeSymbols: JSON.parse(JSON.stringify(active_symbols)), - }} - feedCall={{ - activeSymbols: false, - }} - enabledNavigationWidget={isDesktop()} - enabledChartFooter={false} - id='trade' - isMobile={is_mobile} - maxTick={is_mobile ? max_ticks : undefined} - granularity={show_digits_stats || is_accumulator ? 0 : granularity} - requestAPI={wsSendRequest} - requestForget={wsForget} - requestForgetStream={wsForgetStream} - requestSubscribe={wsSubscribe} - settings={settings} - should_show_eu_content={should_show_eu_content} - allowTickChartTypeOnly={show_digits_stats || is_accumulator} - stateChangeListener={chartStateChange} - symbol={symbol} - topWidgets={is_trade_enabled ? topWidgets : null} - isConnectionOpened={is_socket_opened} - clearChart={false} - toolbarWidget={() => { - if (is_beta_chart) { - return ( - - ); - } else - return ( - - ); - }} - importedLayout={chart_layout} - onExportLayout={exportLayout} - shouldFetchTradingTimes={!end_epoch} - hasAlternativeSource={has_alternative_source} - getMarketsOrder={getMarketsOrder} - should_zoom_out_on_yaxis={is_accumulator} - yAxisMargin={{ - top: is_mobile ? 76 : 106, - }} - isLive={true} - leftMargin={isDesktop() && is_positions_drawer_on ? 328 : 80} - is_beta={is_beta_chart} - > - {!is_beta_chart && } - {is_accumulator && ( - - )} - - ); -}); diff --git a/packages/trader/src/Modules/Trading/Containers/trade.tsx b/packages/trader/src/Modules/Trading/Containers/trade.tsx new file mode 100644 index 000000000000..fbcba9bd8a5c --- /dev/null +++ b/packages/trader/src/Modules/Trading/Containers/trade.tsx @@ -0,0 +1,235 @@ +import React from 'react'; +import classNames from 'classnames'; +import { DesktopWrapper, Div100vhContainer, MobileWrapper, SwipeableWrapper } from '@deriv/components'; +import { TickSpotData } from '@deriv/api-types'; +import { isDesktop } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { useTraderStore } from 'Stores/useTraderStores'; +import ChartLoader from 'App/Components/Elements/chart-loader'; +import PositionsDrawer from 'App/Components/Elements/PositionsDrawer'; +import MarketIsClosedOverlay from 'App/Components/Elements/market-is-closed-overlay'; +import { ChartTopWidgets, DigitsWidget } from './chart-widgets'; +import FormLayout from '../Components/Form/form-layout'; +import TradeChart from './trade-chart'; + +export type TBottomWidgetsParams = { + digits: number[]; + tick: TickSpotData | null; +}; +type TBottomWidgetsMobile = TBottomWidgetsParams & { + setTick: (tick: TickSpotData | null) => void; + setDigits: (digits: number[]) => void; +}; + +const BottomWidgetsMobile = ({ tick, digits, setTick, setDigits }: TBottomWidgetsMobile) => { + React.useEffect(() => { + setTick(tick); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tick]); + + React.useEffect(() => { + setDigits(digits); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [digits]); + + // render nothing for bottom widgets on chart in mobile + return null; +}; + +const Trade = observer(() => { + const { client, common, ui } = useStore(); + const { + form_components, + getFirstOpenMarket, + is_accumulator, + is_chart_loading, + is_market_closed, + is_trade_enabled, + is_turbos, + is_synthetics_available, + is_synthetics_trading_market_available, + is_vanilla, + onChange, + onMount, + onUnmount, + prepareTradeStore, + setContractTypes, + setIsDigitsWidgetActive, + setMobileDigitView, + should_show_active_symbols_loading, + show_digits_stats, + symbol, + } = useTraderStore(); + const { + has_only_forward_starting_contracts: is_market_unavailable_visible, + is_dark_mode_on: is_dark_theme, + is_mobile, + notification_messages_ui: NotificationMessages, + should_show_multipliers_onboarding, + } = ui; + const { is_eu } = client; + const { network_status } = common; + + const [digits, setDigits] = React.useState([]); + const [tick, setTick] = React.useState(null); + const [try_synthetic_indices, setTrySyntheticIndices] = React.useState(false); + const [try_open_markets, setTryOpenMarkets] = React.useState(false); + const [category, setCategory] = React.useState(); + const [subcategory, setSubcategory] = React.useState(); + const [swipe_index, setSwipeIndex] = React.useState(0); + + const open_market = React.useMemo(() => { + if (try_synthetic_indices) { + return { category: 'synthetics' }; + } else if (try_open_markets && category) { + return { category, subcategory }; + } + return null; + }, [try_synthetic_indices, try_open_markets, category, subcategory]); + + React.useEffect(() => { + onMount(); + if (!is_synthetics_available) { + const setMarket = async () => { + const markets_to_search = ['forex', 'indices', 'commodities']; // none-synthetic + const { category: market_cat, subcategory: market_subcat } = + (await getFirstOpenMarket(markets_to_search)) ?? {}; + if (market_cat) { + setCategory(market_cat); + setSubcategory(market_subcat); + } + }; + + setMarket(); + } + return () => onUnmount(); + }, [onMount, onUnmount, getFirstOpenMarket, is_synthetics_available]); + + React.useEffect(() => { + if (is_mobile) { + setDigits([]); + } + setTrySyntheticIndices(false); + setTryOpenMarkets(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [symbol, setDigits, setTrySyntheticIndices, is_synthetics_available]); + + React.useEffect(() => { + const selectMultipliers = async () => { + await setContractTypes(); + + onChange({ target: { name: 'contract_type', value: 'multiplier' } }); + }; + if (should_show_multipliers_onboarding && !is_chart_loading && (is_synthetics_available || !is_market_closed)) { + selectMultipliers(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [should_show_multipliers_onboarding, is_chart_loading]); + + const bottomWidgets = React.useCallback(({ digits: d, tick: t }: TBottomWidgetsParams) => { + return ; + }, []); + + const onChangeSwipeableIndex = (index?: number) => { + setMobileDigitView(index === 0); + setIsDigitsWidgetActive(index === 0); + setSwipeIndex(index); + }; + + const onTryOtherMarkets = async () => { + if (!is_synthetics_available) { + setTryOpenMarkets(true); + setTimeout(() => setTryOpenMarkets(false)); + } else { + setTrySyntheticIndices(true); + setTimeout(() => setTrySyntheticIndices(false)); + } + }; + + const topWidgets = React.useCallback( + () => , + [open_market, try_synthetic_indices, try_open_markets] + ); + + const form_wrapper_class = is_mobile ? 'mobile-wrapper' : 'sidebar__container desktop-only'; + const chart_height_offset = React.useMemo(() => { + if (is_accumulator) return '295px'; + if (is_turbos) return '300px'; + return '259px'; + }, [is_turbos, is_accumulator]); + + return ( +
+ + + + {/* Div100vhContainer is workaround for browsers on devices + with toolbars covering screen height, + using css vh is not returning correct screen height */} + + + } + > + +
+ + +
+
+ + + + {show_digits_stats && } + + + +
+
+
+ {is_market_closed && !is_market_unavailable_visible && ( + + )} + 0 && network_status.class === 'online' + } + /> +
+
+ ); +}); + +export default Trade; diff --git a/packages/trader/src/Modules/Trading/index.js b/packages/trader/src/Modules/Trading/index.js deleted file mode 100644 index 09744c03b3ee..000000000000 --- a/packages/trader/src/Modules/Trading/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Trade from './Containers/trade.jsx'; - -export default Trade; diff --git a/packages/trader/src/Modules/Trading/index.ts b/packages/trader/src/Modules/Trading/index.ts new file mode 100644 index 000000000000..e53600e6e273 --- /dev/null +++ b/packages/trader/src/Modules/Trading/index.ts @@ -0,0 +1,3 @@ +import Trade from './Containers/trade'; + +export default Trade; diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.ts b/packages/trader/src/Stores/Modules/Trading/trade-store.ts index 4963ec408900..d000dd74d052 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.ts +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.ts @@ -73,7 +73,7 @@ type TBarriers = Array< isSingleBarrier?: boolean; } >; -type TChartLayout = { +export type TChartLayout = { adj: boolean; aggregationType: string; animation?: boolean; @@ -118,7 +118,19 @@ type TChartLayout = { timeUnit: string; volumeUnderlay: boolean; }; -type TChartStateChangeOption = { symbol: string | undefined; isClosed: boolean }; +export type TChartStateChangeOption = { + indicator_type_name?: string; + indicators_category_name?: string; + isClosed?: boolean; + is_favorite?: boolean; + is_info_open?: boolean; + is_open?: boolean; + chart_type_name?: string; + search_string?: string; + symbol?: string; + symbol_category?: string; + time_interval_name?: string; +}; type TContractDataForGTM = Omit, 'cancellation' | 'limit_order'> & ReturnType & { buy_price: number; diff --git a/packages/trader/src/Types/common-prop.type.ts b/packages/trader/src/Types/common-prop.type.ts index 34b089269291..7b04be8a62dd 100644 --- a/packages/trader/src/Types/common-prop.type.ts +++ b/packages/trader/src/Types/common-prop.type.ts @@ -1,16 +1,50 @@ +import { + ActiveSymbolsResponse, + BuyContractResponse, + BuyContractRequest, + ContractsForSymbolResponse, + ForgetAllResponse, + ForgetResponse, + PriceProposalOpenContractsResponse, + PriceProposalOpenContractsRequest, + PriceProposalResponse, + PriceProposalRequest, + ServerTimeResponse, + TicksHistoryResponse, + TicksStreamResponse, + TicksStreamRequest, + TradingTimesResponse, + UpdateContractHistoryResponse, + UpdateContractResponse, + UpdateContractRequest, +} from '@deriv/api-types'; import { TCoreStores } from '@deriv/stores/types'; import { useTraderStore } from 'Stores/useTraderStores'; import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { TSocketEndpointNames, TSocketResponse } from '../../../api/types'; export type TBinaryRoutesProps = { is_logged_in: boolean; is_logging_in: boolean; passthrough?: { root_store: TCoreStores; - WS: unknown; + WS: TWebSocket; }; }; +type TBuyRequest = { + proposal_id: string; + price: string | number; + passthrough?: BuyContractRequest['passthrough']; +}; + +export type TServerError = { + code: string; + message: string; + details?: { [key: string]: string }; + fields?: string[]; +}; + export type TTextValueStrings = { text: string; value: string; @@ -48,3 +82,45 @@ export type TRouteConfig = TRoute & { }; export type TTradeStore = ReturnType; + +type TWebSocketCall = { + activeSymbols: (mode?: 'string') => Promise; + send?: (req?: Record) => Promise<{ error?: TServerError & Record }>; + subscribeProposalOpenContract: ( + contract_id: PriceProposalOpenContractsRequest['contract_id'], + callback: (response: PriceProposalOpenContractsResponse) => void + ) => void; +}; + +type TWebSocketSend = (req?: Record) => Promise<{ error?: TServerError & Record }>; + +export type TWebSocket = { + activeSymbols: (mode?: 'string') => Promise; + authorized: TWebSocketCall; + buy: (req: TBuyRequest) => Promise; + contractUpdate: ( + contract_id: UpdateContractRequest['contract_id'], + limit_order: UpdateContractRequest['limit_order'] + ) => Promise<{ error?: TServerError } & UpdateContractResponse>; + contractUpdateHistory: (contract_id?: number) => Promise; + forget: (id: string) => Promise; + forgetAll: (value: T) => Promise; + forgetStream: (stream_id: string) => void; + send?: TWebSocketSend; + subscribeProposal: ( + req: Partial, + callback: (response: Promise) => void + ) => Promise; + subscribeTicks: (symbol: string, callback: (response: TicksStreamResponse) => void) => void; + subscribeTicksHistory: ( + req: TicksStreamRequest, + callback: (response: TicksHistoryResponse | TicksStreamResponse) => void + ) => Promise<{ error?: TServerError } & (TicksHistoryResponse | TicksStreamResponse)>; + storage: { + contractsFor: (symbol: string) => Promise; + send?: TWebSocketSend; + }; + time: () => Promise; + tradingTimes: (date: string) => Promise; + wait: (value: T) => Promise>; +};