diff --git a/packages/components/src/components/numpad/numpad.tsx b/packages/components/src/components/numpad/numpad.tsx index e7f6c2e4dfd5..f7895a0e3ead 100644 --- a/packages/components/src/components/numpad/numpad.tsx +++ b/packages/components/src/components/numpad/numpad.tsx @@ -8,7 +8,7 @@ import { useLongPress } from '../../hooks'; type TNumpad = { className?: string; - currency: string; + currency?: string; is_regular?: boolean; is_currency?: boolean; is_submit_disabled?: boolean; diff --git a/packages/components/src/components/numpad/step-input.tsx b/packages/components/src/components/numpad/step-input.tsx index 5ddd8e242e75..09f29f6f8e26 100644 --- a/packages/components/src/components/numpad/step-input.tsx +++ b/packages/components/src/components/numpad/step-input.tsx @@ -12,7 +12,7 @@ type TStepInput = { onChange: (increment_value: string | number, pip_size: number) => void; render?: (props: { value: string; className: string }) => React.ReactNode; pip_size: number; - currency: string; + currency?: string; label?: string; }; diff --git a/packages/components/src/components/tick-picker/tick-picker.tsx b/packages/components/src/components/tick-picker/tick-picker.tsx index f035d7fa4710..0c8e29b16102 100644 --- a/packages/components/src/components/tick-picker/tick-picker.tsx +++ b/packages/components/src/components/tick-picker/tick-picker.tsx @@ -14,7 +14,7 @@ type TTickPicker = { min_value: number; max_value: number; onSubmit: (props: { target: { value: number; name: string } }) => void; - submit_label: React.ReactElement; + submit_label: React.ReactElement | string; singular_label: string; plural_label: string; onValueChange: (tick_value: number) => void; 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/details.ts b/packages/shared/src/utils/helpers/details.ts index 43e24c105335..7379ff7ab907 100644 --- a/packages/shared/src/utils/helpers/details.ts +++ b/packages/shared/src/utils/helpers/details.ts @@ -3,6 +3,12 @@ import { localize } from '@deriv/translations'; import moment from 'moment'; import { TContractInfo } from '../contract'; +type TUnitMap = { + name_plural?: string; + name_singular?: string; + name?: string; +}; + export const getDurationUnitValue = (obj_duration: moment.Duration) => { const duration_ms = obj_duration.asMilliseconds() / 1000; // Check with isEndTime to find out if value of duration has decimals @@ -40,7 +46,7 @@ export const getUnitMap = () => { m: { name_plural: localize('minutes'), name_singular: localize('minute') }, s: { name: localize('seconds') }, t: { name_plural: localize('ticks'), name_singular: localize('tick') }, - }; + } as { [key: string]: TUnitMap }; }; const TIME = { 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 7d9b4ef9cf4d..ba30b167a88f 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -225,6 +225,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: [], @@ -326,6 +327,7 @@ const mock = (): TStores & { is_mock: boolean } => { is_cashier_visible: false, is_wallet_modal_visible: false, is_chart_layout_default: false, + is_chart_countdown_visible: false, is_closing_create_real_account_modal: false, is_dark_mode_on: false, is_language_settings_modal_on: false, @@ -361,9 +363,9 @@ const mock = (): TStores & { is_mock: boolean } => { }, resetRealAccountSignupParams: jest.fn(), notification_messages_ui: jest.fn(), + onChangeUiStore: jest.fn(), openPositionsDrawer: jest.fn(), openRealAccountSignup: jest.fn(), - onChangeUiStore: jest.fn(), setChartCountdown: jest.fn(), setIsWalletModalVisible: jest.fn(), setHasOnlyForwardingContracts: jest.fn(), @@ -374,6 +376,8 @@ const mock = (): TStores & { is_mock: boolean } => { setPurchaseState: jest.fn(), setAppContentsScrollRef: jest.fn(), shouldNavigateAfterChooseCrypto: jest.fn(), + simple_duration_unit: 't', + toggleHistoryTab: jest.fn(), toggleLanguageSettingsModal: jest.fn(), togglePositionsDrawer: jest.fn(), toggleLinkExpiredModal: jest.fn(), @@ -420,9 +424,7 @@ const mock = (): TStores & { is_mock: boolean } => { setMT5MigrationModalEnabled: jest.fn(), toggleMT5MigrationModal: jest.fn(), vanilla_trade_type: 'VANILLALONGCALL', - is_chart_countdown_visible: false, is_additional_kyc_info_modal_open: false, - simple_duration_unit: 't', toggleAdditionalKycInfoModal: jest.fn(), is_kyc_information_submitted_modal_open: false, toggleKycInformationSubmittedModal: jest.fn(), @@ -694,16 +696,42 @@ const mock = (): TStores & { is_mock: boolean } => { pushwoosh: {}, contract_replay: { contract_store: { + accumulator_previous_spot_time: null, + barriers_array: [], + contract_config: {}, contract_info: {}, + contract_update: {}, + contract_update_history: [], digits_info: {}, display_status: '', + getContractsArray: jest.fn(), is_digit_contract: false, is_ended: false, + marker: { + contract_info: {}, + epoch_array: [], + key: '', + price_array: [], + type: '', + }, + markers_array: [], }, + chart_state: '', + chartStateChange: jest.fn(), + has_error: false, + is_chart_loading: true, + is_forward_starting: false, + is_market_closed: false, + is_sell_requested: false, + onClickCancel: jest.fn(), + onClickSell: jest.fn(), + onMount: jest.fn(), + onUnmount: jest.fn(), removeErrorMessage: jest.fn(), - error_message: '', + 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 b8eb57d8601d..bce3067fd9ad 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -263,6 +263,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; @@ -467,6 +481,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; @@ -603,11 +618,11 @@ type TUiStore = { is_positions_drawer_on: boolean; is_services_error_visible: boolean; is_unsupported_contract_modal_visible: boolean; + onChangeUiStore: ({ name, value }: { name: string; value: unknown }) => void; openPositionsDrawer: () => void; openRealAccountSignup: ( value: 'maltainvest' | 'svg' | 'add_crypto' | 'choose' | 'add_fiat' | 'set_currency' | 'manage' ) => void; - onChangeUiStore: ({ name, value }: { name: string; value: number | null }) => void; notification_messages_ui: React.ElementType; setChartCountdown: (value: boolean) => void; populateFooterExtensions: ( @@ -643,6 +658,7 @@ type TUiStore = { toggleAccountsDialog: () => void; toggleAccountSettings: (props?: boolean) => void; toggleCashier: () => void; + toggleHistoryTab: (state_change?: boolean) => void; toggleLanguageSettingsModal: () => void; toggleLinkExpiredModal: (state_change: boolean) => void; togglePositionsDrawer: () => void; @@ -815,7 +831,7 @@ type TContractTradeStore = { underlying, }: Partial) => void; updateChartType: (type: string) => void; - updateGranularity: (granularity: number) => void; + updateGranularity: (granularity: number | null) => void; updateProposal: (response: ProposalOpenContract) => void; }; @@ -968,14 +984,72 @@ type TTradersHubStore = { type TContractReplay = { contract_store: { + accumulator_previous_spot_time: number | null; + barriers_array: Array | []; + contract_config: + | Record + | { + chart_type: string; + granularity?: number; + end_epoch?: number; + start_epoch: number; + scroll_to_epoch: number; + } + | null; contract_info: TPortfolioPosition['contract_info']; + contract_update: ProposalOpenContract['limit_order']; + contract_update_history: TContractStore['contract_update_history']; digits_info: { [key: number]: { digit: number; spot: string } }; display_status: string; + getContractsArray: () => { + type: string; + markers: Array<{ + color: string; + epoch: number; + quote?: number; + text?: string; + type: string; + }>; + props: { + hasPersistentBorders: boolean; + }; + }[]; is_digit_contract: boolean; is_ended: boolean; + marker: { + contract_info: TPortfolioPosition['contract_info']; + epoch_array: Array | []; + key: string; + price_array: Array | []; + type: string; + }; + markers_array: + | [] + | Array<{ + content_config: { className: string }; + marker_config: { ContentComponent: 'div'; x: string | number; y: string | number }; + react_key: string; + type: string; + }>; }; + chart_state: string; + chartStateChange: (state: string, option?: TChartStateChangeOption) => void; + error_code?: string; + error_message?: string; + has_error: boolean; + indicative_status?: string; + is_chart_loading: boolean; + is_forward_starting: boolean; + is_market_closed: boolean; + is_sell_requested: boolean; + margin?: number; + onClickCancel: (contract_id?: number) => void; + onClickSell: (contract_id?: number) => void; + onMount: (contract_id?: number) => void; + onUnmount: () => void; removeErrorMessage: () => void; - error_message: string; + removeAccountSwitcherListener: () => void; + setAccountSwitcherListener: (contract_id: string | number, history: Array) => void; }; type TGtmStore = { is_gtm_applicable: boolean; @@ -1014,7 +1088,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/ContractDrawer/contract-drawer.tsx b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.tsx index aefdf90b8b83..e1916a40abf0 100644 --- a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.tsx +++ b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.tsx @@ -74,7 +74,7 @@ const ContractDrawer = observer( contract_end_time={getEndTime(contract_info)} contract_info={contract_info} contract_update_history={contract_update_history} - duration_unit={getDurationUnitText(getDurationPeriod(contract_info))} + duration_unit={getDurationUnitText(getDurationPeriod(contract_info)) ?? ''} duration={getDurationTime(contract_info)} exit_spot={exit_spot} has_result={ 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..59d88b07f25b 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, useIsMounted, isContractElapsed, TContractStore } from '@deriv/shared'; import { Localize } from '@deriv/translations'; import { Bounce, SlideIn } from 'App/Components/Animations'; import { DigitSpot, LastDigitPrediction } from '../LastDigitPrediction'; @@ -35,15 +29,16 @@ type TDigits = Pick & { is_digit_contract?: TContractStore['is_digit_contract']; is_ended?: TContractStore['is_ended']; is_trade_page?: boolean; + is_mobile: boolean; 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 | { @@ -62,6 +57,7 @@ const DigitsWrapper = ({ is_digit_contract, is_ended, is_trade_page, + is_mobile, onDigitChange, selected_digit, trade_type, @@ -102,7 +98,7 @@ const DigitsWrapper = ({ { - const { contract_replay } = useStore(); + const { contract_replay, ui } = useStore(); + const { is_mobile } = ui; const { contract_store } = contract_replay; const { contract_info, digits_info, display_status, is_digit_contract, is_ended } = contract_store; @@ -14,6 +15,7 @@ export const DigitsWidget = observer(() => { { - const { common, contract_replay, ui } = useStore(); - const [swipe_index, setSwipeIndex] = React.useState(0); - const { contract_store } = contract_replay; - const { - is_market_closed, - is_sell_requested, - is_valid_to_cancel, - onClickCancel, - onClickSell, - onMount, - onUnmount, - indicative_status, - is_chart_loading, - is_forward_starting, - } = contract_replay; - const { contract_info, contract_update, contract_update_history, is_digit_contract } = contract_store; - const { routeBackInApp } = common; - const { is_dark_mode_on: is_dark_theme, notification_messages_ui: NotificationMessages, toggleHistoryTab } = ui; - const trade_type_feature_flag = - contract_info.shortcode && getContractTypeFeatureFlag(contract_info.contract_type, isHighLow(contract_info)); - const is_trade_type_disabled = useFeatureFlags()[`is_${trade_type_feature_flag}_enabled`] === false; - const [is_visible, setIsVisible] = React.useState(false); - const history = useHistory(); - - React.useEffect(() => { - const url_contract_id = +/[^/]*$/.exec(location.pathname)[0]; - onMount(contract_id || url_contract_id); - setIsVisible(true); - - return () => { - setIsVisible(false); - onUnmount(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [contract_id, location, onMount, onUnmount]); - - const onClickClose = React.useCallback(() => { - setIsVisible(false); - const is_from_table_row = !isEmptyObject(location.state) ? location.state.from_table_row : false; - return is_from_table_row ? history.goBack() : routeBackInApp(history); - }, [history, routeBackInApp]); - - React.useEffect(() => { - // don't open Contract details page for trade types with disabled feature flag: - if (is_trade_type_disabled && is_visible) { - onClickClose(); - } - }, [is_trade_type_disabled, is_visible, onClickClose]); - - const onChangeSwipeableIndex = index => { - setSwipeIndex(index); - }; - - if (!contract_info.underlying) return null; - - const is_accumulator = isAccumulatorContract(contract_info.contract_type); - const is_multiplier = isMultiplierContract(contract_info.contract_type); - const is_turbos = isTurbosContract(contract_info.contract_type); - const is_vanilla = isVanillaContract(contract_info.contract_type); - const is_smarttrader_contract = isSmartTraderContract(contract_info.contract_type); - - const contract_drawer_el = ( - - ); - - const unsupportedContractOnConfirm = () => { - history.goBack(); - }; - - const unsupportedContractOnClose = () => { - const statementws_url = urlFor('user/statementws', { legacy: true }); - window.open(statementws_url, '_blank'); - }; - - return ( - - - - - - - - {contract_drawer_el} - -
- {contract_drawer_el} -
-
- }> -
- - - - - - - - - {is_digit_contract ? ( - - - - - - - - ) : ( - - )} - -
-
-
-
-
- ); -}); - -ContractReplay.propTypes = { - contract_id: PropTypes.number, -}; - -export default ContractReplay; - -// CHART ----------------------------------------- - -const ReplayChart = observer(({ is_accumulator_contract }) => { - const trade = useTraderStore(); - const { contract_replay, client, common, ui } = useStore(); - const { contract_store, chart_state, chartStateChange, margin } = contract_replay; - const { - accumulator_previous_spot_time, - contract_config, - marker: accumulators_barriers_marker, - is_digit_contract, - barriers_array, - getContractsArray, - markers_array, - contract_info, - } = contract_store; - const { underlying: symbol, audit_details } = contract_info; - const allow_scroll_to_epoch = chart_state === 'READY' || chart_state === 'SCROLL_TO_LEFT'; - const { app_routing_history, current_language, is_socket_opened } = common; - const { is_dark_mode_on: is_dark_theme, is_chart_layout_default, is_chart_countdown_visible } = ui; - const { end_epoch, chart_type, start_epoch, granularity } = contract_config; - /** - * TODO: remove forcing light theme once DBot supports dark theme - * DBot does not support for dark theme since till now, - * as a result, if any user come to report detail pages - * from DBot, we should force it to have light theme - */ - const from_platform = getPlatformRedirect(app_routing_history); - const should_force_light_theme = from_platform.name === 'DBot'; - const settings = { - language: current_language.toLowerCase(), - theme: is_dark_theme && !should_force_light_theme ? 'dark' : 'light', - position: is_chart_layout_default ? 'bottom' : 'left', - countdown: is_chart_countdown_visible, - assetInformation: false, // ui.is_chart_asset_info_visible, - isHighestLowestMarkerEnabled: false, // TODO: Pending UI - }; - const scroll_to_epoch = allow_scroll_to_epoch ? contract_config.scroll_to_epoch : undefined; - const all_ticks = audit_details ? audit_details.all_ticks : []; - const { wsForget, wsSubscribe, wsSendRequest, wsForgetStream } = trade; - const { is_beta_chart } = client; - - const accu_barriers_marker_component = !is_beta_chart ? allMarkers[accumulators_barriers_marker?.type] : undefined; - - const isBottomWidgetVisible = () => { - return isDesktop() && is_digit_contract; - }; - - const getChartYAxisMargin = () => { - const chart_margin = { - top: isMobile() ? 96 : 148, - bottom: isBottomWidgetVisible() ? 128 : 112, - }; - - if (isMobile()) { - if (is_beta_chart) { - chart_margin.top = 48; - } - chart_margin.bottom = 48; - } - - return chart_margin; - }; - const prev_start_epoch = usePrevious(start_epoch); - - const has_ended = !!getEndTime(contract_info); - - return ( - - {is_beta_chart && - markers_array.map(({ content_config, marker_config, react_key }) => ( - - ))} - {!is_beta_chart && - markers_array.map(({ content_config, marker_config, react_key }) => ( - - ))} - {!is_beta_chart && is_accumulator_contract && !!markers_array && ( - - )} - - ); -}); - -ReplayChart.propTypes = { - is_accumulator_contract: PropTypes.bool, -}; diff --git a/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx b/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx new file mode 100644 index 000000000000..c5de42de3c16 --- /dev/null +++ b/packages/trader/src/Modules/Contract/Containers/contract-replay.tsx @@ -0,0 +1,215 @@ +import classNames from 'classnames'; +import React from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { + DesktopWrapper, + Div100vhContainer, + MobileWrapper, + PageOverlay, + SwipeableWrapper, + FadeWrapper, +} from '@deriv/components'; +import { + getContractTypeFeatureFlag, + isAccumulatorContract, + isDesktop, + isEmptyObject, + isHighLow, + isMultiplierContract, + isTurbosContract, + isVanillaContract, + isSmartTraderContract, + urlFor, +} from '@deriv/shared'; +import { Localize } from '@deriv/translations'; +import { useFeatureFlags } from '@deriv/hooks'; +import ChartLoader from 'App/Components/Elements/chart-loader'; +import ContractDrawer from 'App/Components/Elements/ContractDrawer'; +import UnsupportedContractModal from 'App/Components/Elements/Modals/UnsupportedContractModal'; +import { DigitsWidget, InfoBoxWidget } from './contract-replay-widget'; +import ReplayChart from './replay-chart'; +import { observer, useStore } from '@deriv/stores'; + +type TLocationState = { from_table_row: boolean }; + +const ContractReplay = observer(({ contract_id }: { contract_id: number }) => { + const { state } = useLocation(); + const { common, contract_replay, ui } = useStore(); + const [swipe_index, setSwipeIndex] = React.useState(0); + const { contract_store } = contract_replay; + const { + is_market_closed, + is_sell_requested, + onClickCancel, + onClickSell, + onMount, + onUnmount, + indicative_status, + is_chart_loading, + is_forward_starting, + } = contract_replay; + const { contract_info, contract_update, contract_update_history, is_digit_contract } = contract_store; + const { routeBackInApp } = common; + const { + is_dark_mode_on: is_dark_theme, + is_mobile, + notification_messages_ui: NotificationMessages, + toggleHistoryTab, + } = ui; + const trade_type_feature_flag = + contract_info.shortcode && + getContractTypeFeatureFlag(contract_info.contract_type ?? '', isHighLow(contract_info)); + const is_trade_type_disabled = + useFeatureFlags()[`is_${trade_type_feature_flag}_enabled` as keyof ReturnType] === + false; + const [is_visible, setIsVisible] = React.useState(false); + const history = useHistory(); + + React.useEffect(() => { + const url_array = /[^/]*$/.exec(location.pathname); + const url_contract_id = url_array ? +url_array[0] : undefined; + onMount(contract_id || url_contract_id); + setIsVisible(true); + + return () => { + setIsVisible(false); + onUnmount(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contract_id, location, onMount, onUnmount]); + + const onClickClose = React.useCallback(() => { + 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(() => { + // don't open Contract details page for trade types with disabled feature flag: + if (is_trade_type_disabled && is_visible) { + onClickClose(); + } + }, [is_trade_type_disabled, is_visible, onClickClose]); + + const onChangeSwipeableIndex = (index: number) => { + setSwipeIndex(index); + }; + + if (!contract_info.underlying) return null; + + const is_accumulator = isAccumulatorContract(contract_info.contract_type); + const is_multiplier = isMultiplierContract(contract_info.contract_type); + const is_turbos = isTurbosContract(contract_info.contract_type); + const is_vanilla = isVanillaContract(contract_info.contract_type); + const is_smarttrader_contract = isSmartTraderContract(contract_info.contract_type); + + const contract_drawer_el = ( + + ); + + const unsupportedContractOnConfirm = () => { + history.goBack(); + }; + + const unsupportedContractOnClose = () => { + const statementws_url = urlFor('user/statementws', { legacy: true }); + window.open(statementws_url, '_blank'); + }; + + return ( + + + + + + } + onClickClose={onClickClose} + > + + {contract_drawer_el} + +
+ {contract_drawer_el} +
+
+ }> +
+ + + + + + + + + {is_digit_contract ? ( + + + ['onChange'] + } + > + + + + + ) : ( + + )} + +
+
+
+
+
+ ); +}); + +export default ContractReplay; diff --git a/packages/trader/src/Modules/Contract/Containers/contract.jsx b/packages/trader/src/Modules/Contract/Containers/contract.tsx similarity index 81% rename from packages/trader/src/Modules/Contract/Containers/contract.jsx rename to packages/trader/src/Modules/Contract/Containers/contract.tsx index 420cfe0d21d2..734e1da1ff86 100644 --- a/packages/trader/src/Modules/Contract/Containers/contract.jsx +++ b/packages/trader/src/Modules/Contract/Containers/contract.tsx @@ -1,16 +1,19 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { Redirect, withRouter } from 'react-router'; +import { RouteComponentProps } from 'react-router-dom'; import { CSSTransition } from 'react-transition-group'; import { routes } from '@deriv/shared'; import ErrorComponent from 'App/Components/Elements/Errors'; import { localize } from '@deriv/translations'; -import ContractReplay from './contract-replay.jsx'; +import ContractReplay from './contract-replay'; import { observer, useStore } from '@deriv/stores'; +type TContractParams = { contract_id: string }; +type TContract = RouteComponentProps; + const dialog_errors = ['GetProposalFailure', 'ContractValidationError']; -const Contract = observer(({ match, history }) => { +const Contract = observer(({ match, history }: TContract) => { const { contract_replay } = useStore(); const { removeErrorMessage, @@ -29,7 +32,7 @@ const Contract = observer(({ match, history }) => { }; }, [onMount, onUnmount, removeErrorMessage, history, match.params.contract_id]); - if (isNaN(match.params.contract_id)) { + if (isNaN(Number(match.params.contract_id))) { return ; } @@ -38,9 +41,9 @@ const Contract = observer(({ match, history }) => { {has_error ? ( history.push(routes.trade)} should_show_refresh={false} @@ -63,9 +66,4 @@ const Contract = observer(({ match, history }) => { ); }); -Contract.propTypes = { - history: PropTypes.object, - match: PropTypes.object, -}; - export default withRouter(Contract); diff --git a/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx b/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx new file mode 100644 index 000000000000..5488eaa6dfdb --- /dev/null +++ b/packages/trader/src/Modules/Contract/Containers/replay-chart.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { usePrevious } from '@deriv/components'; +import { getDurationPeriod, getDurationUnitText, getEndTime, getPlatformRedirect, isDesktop } from '@deriv/shared'; +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( + ({ + is_dark_theme_prop, + is_accumulator_contract, + }: { + is_dark_theme_prop?: boolean; + is_accumulator_contract?: boolean; + }) => { + const trade = useTraderStore(); + const { contract_replay, client, common, ui } = useStore(); + const { contract_store, chart_state, chartStateChange, margin } = contract_replay; + const { + accumulator_previous_spot_time, + contract_config, + marker: accumulators_barriers_marker, + is_digit_contract, + barriers_array, + getContractsArray, + markers_array, + contract_info, + } = contract_store; + const { underlying: symbol, audit_details } = contract_info; + const allow_scroll_to_epoch = chart_state === 'READY' || chart_state === 'SCROLL_TO_LEFT'; + const { app_routing_history, current_language, is_socket_opened } = common; + const { is_chart_layout_default, is_chart_countdown_visible, is_mobile } = ui; + const { end_epoch, chart_type, start_epoch, granularity } = contract_config || {}; + const is_dark_theme = is_dark_theme_prop || ui.is_dark_mode_on; + + /** + * TODO: remove forcing light theme once DBot supports dark theme + * DBot does not support for dark theme since till now, + * as a result, if any user come to report detail pages + * from DBot, we should force it to have light theme + */ + const from_platform = getPlatformRedirect(app_routing_history); + const should_force_light_theme = from_platform.name === 'DBot'; + const settings = { + language: current_language.toLowerCase(), + theme: is_dark_theme && !should_force_light_theme ? 'dark' : 'light', + position: is_chart_layout_default ? 'bottom' : 'left', + countdown: is_chart_countdown_visible, + assetInformation: false, // ui.is_chart_asset_info_visible, + isHighestLowestMarkerEnabled: false, // TODO: Pending UI + }; + const scroll_to_epoch = allow_scroll_to_epoch && contract_config ? contract_config.scroll_to_epoch : undefined; + const all_ticks = audit_details ? audit_details.all_ticks : []; + const { wsForget, wsSubscribe, wsSendRequest, wsForgetStream } = trade; + const { is_beta_chart } = client; + + const accu_barriers_marker_component = is_beta_chart + ? undefined + : allMarkers[accumulators_barriers_marker?.type as keyof typeof allMarkers]; + const isBottomWidgetVisible = () => { + return isDesktop() && is_digit_contract; + }; + + const getChartYAxisMargin = () => { + const chart_margin = { + top: is_mobile ? 96 : 148, + bottom: isBottomWidgetVisible() ? 128 : 112, + }; + + if (is_mobile) { + if (is_beta_chart) { + chart_margin.top = 48; + } + chart_margin.bottom = 48; + } + + return chart_margin; + }; + const prev_start_epoch = usePrevious(start_epoch); + + const has_ended = !!getEndTime(contract_info); + + return ( + + {is_beta_chart && + markers_array.map(({ content_config, marker_config, react_key }) => ( + + ))} + {!is_beta_chart && + markers_array.map(({ content_config, marker_config, react_key }) => ( + + ))} + {!is_beta_chart && is_accumulator_contract && !!markers_array && ( + ['marker_component'] + } + is_dark_theme={is_dark_theme} + granularity={granularity} + is_in_contract_details + previous_spot_time={accumulator_previous_spot_time} + {...accumulators_barriers_marker} + /> + )} + + ); + } +); +export default ReplayChart; diff --git a/packages/trader/src/Modules/Contract/index.js b/packages/trader/src/Modules/Contract/index.js deleted file mode 100644 index a098260d4660..000000000000 --- a/packages/trader/src/Modules/Contract/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Contract from './Containers/contract.jsx'; - -export default Contract; diff --git a/packages/trader/src/Modules/Contract/index.ts b/packages/trader/src/Modules/Contract/index.ts new file mode 100644 index 000000000000..616fcf9793fb --- /dev/null +++ b/packages/trader/src/Modules/Contract/index.ts @@ -0,0 +1,3 @@ +import Contract from './Containers/contract'; + +export default 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 83e4fca77cda..dce2daf46577 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 @@ -32,7 +32,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 958441e6b851..ae6f2987af87 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,27 +1,22 @@ 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 { TRADE_TYPES } from '@deriv/shared'; 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 = ({ @@ -32,6 +27,7 @@ const AccumulatorsChartElements = ({ should_show_profit_text, symbol, is_beta_chart, + is_mobile, }: TAccumulatorsChartElements) => { const accumulators_positions = all_positions.filter( ({ contract_info }) => @@ -51,9 +47,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 && ( ['portfolio']['all_positions'][number]['contract_info']; @@ -16,6 +16,7 @@ type TAccumulatorsProfitLossTooltip = { className?: string; should_show_profit_text?: boolean; is_beta_chart?: boolean; + is_mobile?: boolean; } & TContractInfo; export type TRef = { @@ -36,6 +37,7 @@ const AccumulatorsProfitLossTooltip = ({ profit_percentage, should_show_profit_text, is_beta_chart, + is_mobile, }: TAccumulatorsProfitLossTooltip) => { const [is_tooltip_open, setIsTooltipOpen] = React.useState(false); const won = Number(profit) >= 0; @@ -120,10 +122,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 076f98449b5a..5479849117aa 100644 --- a/packages/trader/src/Modules/SmartChart/Components/top-widgets.tsx +++ b/packages/trader/src/Modules/SmartChart/Components/top-widgets.tsx @@ -15,9 +15,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/Components/Form/TradeParams/Duration/__tests__/duration-range-text.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-range-text.spec.tsx index 13ea85a7c844..b4980f4be027 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-range-text.spec.tsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/duration-range-text.spec.tsx @@ -3,8 +3,8 @@ import { render, screen } from '@testing-library/react'; import DurationRangeText from '../duration-range-text'; const mocked_props = { - min: '1', - max: '1440', + min: 1, + max: 1440, duration_unit_text: 'minutes', }; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/expiry-text.spec.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/expiry-text.spec.tsx similarity index 100% rename from packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/expiry-text.spec.jsx rename to packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/__tests__/expiry-text.spec.tsx 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 deleted file mode 100644 index 5dec629d54c9..000000000000 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx +++ /dev/null @@ -1,165 +0,0 @@ -import classNames from 'classnames'; -import { PropTypes as MobxPropTypes } from 'mobx-react'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { Dropdown, ButtonToggle, InputField } from '@deriv/components'; -import { getDurationMinMaxValues, getUnitMap, hasIntradayDurationUnit, toMoment } from '@deriv/shared'; -import RangeSlider from 'App/Components/Form/RangeSlider'; -import TradingDatePicker from '../../DatePicker'; -import TradingTimePicker from '../../TimePicker'; -import ExpiryText from './expiry-text.jsx'; -import DurationRangeText from './duration-range-text'; -import { observer, useStore } from '@deriv/stores'; -import { useTraderStore } from 'Stores/useTraderStores'; - -const AdvancedDuration = observer( - ({ - advanced_duration_unit, - advanced_expiry_type, - changeDurationUnit, - duration_t, - duration_units_list, - expiry_date, - expiry_epoch, - expiry_list, - expiry_type, - getDurationFromUnit, - number_input_props, - onChange, - onChangeUiStore, - server_time, - shared_input_props, - start_date, - }) => { - const { ui } = useStore(); - const { current_focus, setCurrentFocus } = ui; - const { contract_expiry_type, duration_min_max, is_vanilla, validation_errors } = useTraderStore(); - - const [min, max] = getDurationMinMaxValues(duration_min_max, contract_expiry_type, advanced_duration_unit); - let is_24_hours_contract = false; - - if (expiry_type === 'endtime') { - const has_intraday_duration_unit = hasIntradayDurationUnit(duration_units_list); - is_24_hours_contract = - (!!start_date || toMoment(expiry_date || server_time).isSame(toMoment(server_time), 'day')) && - has_intraday_duration_unit; - } - - const endtime_container_class = classNames('endtime-container', { - 'has-time': is_24_hours_contract, - }); - - const changeExpiry = ({ target }) => { - const { name, value } = target; - - onChange({ target: { name: 'expiry_type', value } }); - onChangeUiStore({ name, value }); - }; - - const has_error = !!validation_errors?.duration?.length; - - const { name_plural, name } = getUnitMap()[advanced_duration_unit]; - const duration_unit_text = name_plural ?? name; - - return ( - <> - {expiry_list.length > 1 && ( - - )} - {expiry_type === 'duration' ? ( - <> -
- {duration_units_list.length >= 1 && ( - - )} - {advanced_duration_unit === 't' && contract_expiry_type === 'tick' && ( - - )} - {advanced_duration_unit === 'd' && ( - - )} - {advanced_duration_unit !== 't' && advanced_duration_unit !== 'd' && ( - - )} - {is_vanilla && ( - - )} - {advanced_duration_unit === 'd' && ( - - )} -
- - ) : ( - <> -
- - { - is_24_hours_contract && - // validation_errors={validation_errors.end_time} TODO: add validation_errors for end time - } - {!is_24_hours_contract && } -
- - )} - - ); - } -); - -AdvancedDuration.propTypes = { - advanced_duration_unit: PropTypes.string, - advanced_expiry_type: PropTypes.string, - changeDurationUnit: PropTypes.func, - duration_t: PropTypes.number, - duration_units_list: MobxPropTypes.arrayOrObservableArray, - expiry_date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - expiry_list: PropTypes.array, - expiry_type: PropTypes.string, - getDurationFromUnit: PropTypes.func, - number_input_props: PropTypes.object, - onChange: PropTypes.func, - onChangeUiStore: PropTypes.func, - server_time: PropTypes.object, - shared_input_props: PropTypes.object, - start_date: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -export default AdvancedDuration; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx new file mode 100644 index 000000000000..6933dd5c51fe --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.tsx @@ -0,0 +1,181 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Dropdown, ButtonToggle, InputField } from '@deriv/components'; +import { getDurationMinMaxValues, getUnitMap, hasIntradayDurationUnit, toMoment } from '@deriv/shared'; +import RangeSlider from 'App/Components/Form/RangeSlider'; +import TradingDatePicker from '../../DatePicker'; +import TradingTimePicker from '../../TimePicker'; +import ExpiryText from './expiry-text'; +import DurationRangeText from './duration-range-text'; +import type { TDuration } from './duration'; +import { observer, useStore } from '@deriv/stores'; +import { useTraderStore } from 'Stores/useTraderStores'; + +type TAdvancedDuration = Pick< + TDuration, + | 'advanced_duration_unit' + | 'advanced_expiry_type' + | 'duration_t' + | 'duration_units_list' + | 'expiry_date' + | 'expiry_epoch' + | 'expiry_type' + | 'getDurationFromUnit' + | 'onChange' + | 'onChangeUiStore' + | 'server_time' + | 'start_date' + | 'market_open_times' +> & { + changeDurationUnit: ({ target }: { target: { name: string; value: string } }) => void; + expiry_list: { + text: string; + value: string; + }[]; + number_input_props: { + type: string; + is_incrementable: boolean; + }; + shared_input_props: { + is_hj_whitelisted: boolean; + onChange: ({ + target, + }: { + target: { + name: string; + value: string | number; + }; + }) => void; + max_value: number; + min_value: number; + }; +}; + +const AdvancedDuration = observer( + ({ + advanced_duration_unit, + advanced_expiry_type, + changeDurationUnit, + duration_t, + duration_units_list, + expiry_date, + expiry_epoch, + expiry_list, + expiry_type, + getDurationFromUnit, + number_input_props, + onChange, + onChangeUiStore, + server_time, + shared_input_props, + start_date, + }: TAdvancedDuration) => { + const { ui } = useStore(); + const { current_focus, setCurrentFocus } = ui; + const { contract_expiry_type, duration_min_max, is_vanilla, validation_errors } = useTraderStore(); + + const [min, max] = getDurationMinMaxValues(duration_min_max, contract_expiry_type, advanced_duration_unit); + let is_24_hours_contract = false; + + if (expiry_type === 'endtime') { + const has_intraday_duration_unit = hasIntradayDurationUnit(duration_units_list); + is_24_hours_contract = + (!!start_date || toMoment(expiry_date || server_time).isSame(toMoment(server_time), 'day')) && + has_intraday_duration_unit; + } + + const endtime_container_class = classNames('endtime-container', { + 'has-time': is_24_hours_contract, + }); + + const changeExpiry = ({ target }: { target: { name: string; value: unknown } }) => { + const { name, value } = target; + + onChange({ target: { name: 'expiry_type', value } }); + onChangeUiStore({ name, value }); + }; + + const has_error = !!validation_errors?.duration?.length; + + const { name_plural, name } = getUnitMap()[advanced_duration_unit]; + const duration_unit_text = name_plural ?? name; + + return ( + <> + {expiry_list.length > 1 && ( + + )} + {expiry_type === 'duration' ? ( +
+ {duration_units_list.length >= 1 && ( + + )} + {advanced_duration_unit === 't' && contract_expiry_type === 'tick' && ( + + )} + {advanced_duration_unit === 'd' && ( + + )} + {advanced_duration_unit !== 't' && advanced_duration_unit !== 'd' && ( + + )} + {is_vanilla && ( + + )} + {advanced_duration_unit === 'd' && ( + + )} +
+ ) : ( +
+ + { + is_24_hours_contract && + // validation_errors={validation_errors.end_time} TODO: add validation_errors for end time + } + {!is_24_hours_contract && } +
+ )} + + ); + } +); + +export default AdvancedDuration; 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.tsx similarity index 83% rename from packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx rename to packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.tsx index 65d2405e5f06..9687e01f9127 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.tsx @@ -3,15 +3,65 @@ import classNames from 'classnames'; import { Tabs, TickPicker, Numpad, RelativeDatepicker } from '@deriv/components'; import { isEmptyObject, addComma, getDurationMinMaxValues, getUnitMap } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; -import ExpiryText from './expiry-text.jsx'; -import DurationRangeText from './duration-range-text'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; import moment from 'moment'; +import ExpiryText from './expiry-text'; +import DurationRangeText from './duration-range-text'; +import type { TTradeParamsMobile } from 'Modules/Trading/Containers/trade-params-mobile'; + +type TDuration = Pick< + TTradeParamsMobile, + | 'amount_tab_idx' + | 'd_duration' + | 'duration_tab_idx' + | 'h_duration' + | 'm_duration' + | 's_duration' + | 't_duration' + | 'has_amount_error' + | 'payout_value' + | 'setDurationError' + | 'setDurationTabIdx' + | 'setSelectedDuration' + | 'stake_value' + | 'toggleModal' +> & { + expiry_epoch?: string | number; +}; + +type TNumber = Pick< + TDuration, + | 'expiry_epoch' + | 'has_amount_error' + | 'payout_value' + | 'setDurationError' + | 'setSelectedDuration' + | 'stake_value' + | 'toggleModal' +> & { + basis_option: string; + contract_expiry?: string; + duration_unit_option: ReturnType['duration_units_list'][0]; + duration_values?: Record; + selected_duration: number; + show_expiry?: boolean; +}; + +type TTicks = Omit; + +type TDurationUnit = 't' | 's' | 'm' | 'h' | 'd'; const submit_label = localize('OK'); -const updateAmountChanges = (obj, stake_value, payout_value, basis, trade_basis, trade_amount) => { +const updateAmountChanges = ( + obj: Record, + stake_value: number, + payout_value: number, + basis: string, + trade_basis: string, + trade_amount: number +) => { // TODO: Move onChangeMultiple outside of duration and amount // and unify all trade parameter components to use same onMultipleChange func onSubmit // Checks if Amount tab was changed to stake and stake value was updated @@ -39,7 +89,7 @@ const Ticks = observer( stake_value, selected_duration, setSelectedDuration, - }) => { + }: TTicks) => { const { duration_min_max, duration: trade_duration, @@ -55,9 +105,9 @@ const Ticks = observer( const [min_tick, max_tick] = getDurationMinMaxValues(duration_min_max, 'tick', 't'); - const setTickDuration = value => { + const setTickDuration = (value: { target: { value: number; name: string } }) => { const { value: duration } = value.target; - const on_change_obj = {}; + const on_change_obj: Record = {}; // check for any amount changes from Amount trade params tab before submitting onChange object if (!has_amount_error) @@ -72,15 +122,18 @@ const Ticks = observer( toggleModal(); }; - const onTickChange = tick => setSelectedDuration('t', tick); - const tick_duration = trade_duration < min_tick && selected_duration < min_tick ? min_tick : selected_duration; + const onTickChange = (tick: number) => setSelectedDuration('t', tick); + const tick_duration = + trade_duration < Number(min_tick) && selected_duration < Number(min_tick) + ? Number(min_tick) + : selected_duration; return (
{ + }: TNumber) => { const { ui } = useStore(); const { addToast } = ui; const { @@ -122,7 +175,7 @@ const Numbers = observer( const [min, max] = getDurationMinMaxValues(duration_min_max, contract_expiry, duration_unit); const [has_error, setHasError] = React.useState(false); - const validateDuration = value => { + const validateDuration = (value: number | string) => { const localized_message = ( ); - if (parseInt(value) < min || parseInt(selected_duration) > max) { + if (parseInt(value as string) < Number(min) || Math.trunc(selected_duration) > Number(max)) { addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 }); setDurationError(true); setHasError(true); return 'error'; - } else if (parseInt(value) > max) { + } else if (parseInt(value as string) > Number(max)) { addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 }); setHasError(true); return 'error'; @@ -153,14 +206,14 @@ const Numbers = observer( return true; }; - const setDuration = duration => { - const on_change_obj = {}; + const setDuration = (duration: string | number) => { + const on_change_obj: Record = {}; // check for any amount changes from Amount trade params tab before submitting onChange object if (!has_amount_error) updateAmountChanges(on_change_obj, stake_value, payout_value, basis_option, trade_basis, trade_amount); - if (trade_duration !== duration || trade_duration_unit !== duration_unit) { + if (trade_duration !== Number(duration) || trade_duration_unit !== duration_unit) { on_change_obj.duration_unit = duration_unit; on_change_obj.duration = duration; on_change_obj.expiry_type = 'duration'; @@ -170,13 +223,13 @@ const Numbers = observer( toggleModal(); }; - const setExpiryDate = (epoch, duration) => { + const setExpiryDate = (epoch: number, duration: string | number) => { if (trade_duration_unit !== 'd') { return moment.utc().add(Number(duration), 'days').format('D MMM YYYY, [23]:[59]:[59] [GMT +0]'); } let expiry_date = new Date((epoch - trade_duration * 24 * 60 * 60) * 1000); if (duration) { - expiry_date = new Date(expiry_date.getTime() + duration * 24 * 60 * 60 * 1000); + expiry_date = new Date(expiry_date.getTime() + Number(duration) * 24 * 60 * 60 * 1000); } return expiry_date @@ -186,12 +239,12 @@ const Numbers = observer( .replace(/(\d{2}) (\w{3} \d{4})/, '$1 $2,'); }; - const onNumberChange = num => { - setSelectedDuration(duration_unit, num); + const onNumberChange = (num: number | string) => { + setSelectedDuration(duration_unit, Number(num)); validateDuration(num); }; - const fixed_date = !has_error ? setExpiryDate(expiry_epoch, duration_values?.d_duration) : ''; + const fixed_date = !has_error ? setExpiryDate(Number(expiry_epoch), Number(duration_values?.d_duration)) : ''; const { name_plural, name } = getUnitMap()[duration_unit]; const duration_unit_text = name_plural ?? name; @@ -214,8 +267,8 @@ const Numbers = observer( }} pip_size={0} submit_label={submit_label} - min={min} - max={max} + min={Number(min)} + max={Number(max)} reset_press_interval={350} reset_value='' onValidate={validateDuration} @@ -243,7 +296,7 @@ const Duration = observer( stake_value, t_duration, toggleModal, - }) => { + }: TDuration) => { const { duration_units_list, duration_min_max, duration_unit, basis: trade_basis } = useTraderStore(); const duration_values = { t_duration, @@ -257,7 +310,7 @@ const Duration = observer( ? duration_tab_idx : duration_units_list.findIndex(d => d.value === duration_unit); const [min, max] = getDurationMinMaxValues(duration_min_max, 'daily', 'd'); - const handleRelativeChange = date => { + const handleRelativeChange = (date: number) => { setSelectedDuration('d', date); }; const selected_basis_option = () => { @@ -269,10 +322,10 @@ const Duration = observer( return trade_basis; }; - const onTabChange = index => { + const onTabChange = (index: number) => { setDurationTabIdx(index); const { value: unit } = duration_units_list[index]; - setSelectedDuration(unit, duration_values[`${unit}_duration`]); + setSelectedDuration(unit, duration_values[`${unit as TDurationUnit}_duration`]); }; return ( diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-range-text.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-range-text.tsx index c94ad35b38fe..215317dee928 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-range-text.tsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-range-text.tsx @@ -4,9 +4,9 @@ import { addComma } from '@deriv/shared'; import { Localize } from '@deriv/translations'; type TDurationRangeText = { - min: string; - max: string; - duration_unit_text: string; + min: number | null; + max: number | null; + duration_unit_text?: string; }; const DurationRangeText = ({ min, max, duration_unit_text }: TDurationRangeText) => ( diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-toggle.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-toggle.jsx deleted file mode 100644 index 427f59362c9d..000000000000 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-toggle.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { Icon } from '@deriv/components'; -import { localize } from '@deriv/translations'; - -const DurationToggle = ({ name, onChange, value }) => { - const toggle = () => { - onChange({ target: { value: !value, name } }); - }; - const icon_className = classNames('advanced-simple-toggle__icon', 'select-arrow', { - 'advanced-simple-toggle__icon--active': value, - }); - return ( - <> - - - ); -}; - -DurationToggle.propTypes = { - name: PropTypes.string, - onChange: PropTypes.func, - value: PropTypes.bool, -}; - -export default DurationToggle; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-toggle.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-toggle.tsx new file mode 100644 index 000000000000..cb68fb15b6e8 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-toggle.tsx @@ -0,0 +1,31 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Icon } from '@deriv/components'; +import { localize } from '@deriv/translations'; + +type TDurationToggle = { + name: string; + onChange: ({ target }: { target: { name: string; value: boolean } }) => void; + value: boolean; +}; + +const DurationToggle = ({ name, onChange, value }: TDurationToggle) => { + const toggle = () => { + onChange({ target: { value: !value, name } }); + }; + const icon_className = classNames('advanced-simple-toggle__icon', 'select-arrow', { + 'advanced-simple-toggle__icon--active': value, + }); + return ( + + ); +}; + +export default DurationToggle; 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.tsx similarity index 88% rename from packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.jsx rename to packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-wrapper.tsx index 43585bcb4265..1c60b473d909 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.tsx @@ -1,10 +1,13 @@ import React from 'react'; import { getDurationMinMaxValues } from '@deriv/shared'; -import Duration from './duration.jsx'; +import Duration from './duration'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; -const DurationWrapper = observer(() => { +type TDurationWrapper = { + is_minimized?: boolean; +}; +const DurationWrapper = observer(({ is_minimized }: TDurationWrapper) => { const { ui } = useStore(); const { advanced_expiry_type, @@ -47,6 +50,7 @@ const DurationWrapper = observer(() => { expiry_time, expiry_type, getDurationFromUnit, + is_minimized, is_advanced_duration, market_open_times, onChange, @@ -56,7 +60,7 @@ const DurationWrapper = observer(() => { start_date, }; - const hasDurationUnit = (duration_type, is_advanced) => { + const hasDurationUnit = (duration_type: string, is_advanced: boolean) => { let duration_list = [...duration_units_list]; if (duration_list.length > 1 && !is_advanced) { @@ -87,19 +91,23 @@ const DurationWrapper = observer(() => { }; const assertDurationIsWithinBoundary = React.useCallback( - current_duration => { + (current_duration: number) => { const [min_value, max_value] = getDurationMinMaxValues( duration_min_max, contract_expiry_type, duration_unit ); - if (contract_expiry_type === 'tick' && current_duration < min_value) { - onChangeUiStore({ name: `duration_${duration_unit}`, value: min_value }); + if (contract_expiry_type === 'tick' && current_duration < Number(min_value)) { + onChangeUiStore({ name: `duration_${duration_unit}`, value: Number(min_value) }); onChange({ target: { name: 'duration', value: min_value } }); } - if (!(current_duration < min_value) && current_duration > max_value && duration_unit !== 'd') { - onChangeUiStore({ name: `duration_${duration_unit}`, value: max_value }); + if ( + current_duration >= Number(min_value) && + current_duration > Number(max_value) && + duration_unit !== 'd' + ) { + onChangeUiStore({ name: `duration_${duration_unit}`, value: Number(max_value) }); onChange({ target: { name: 'duration', value: max_value } }); } }, @@ -129,7 +137,7 @@ const DurationWrapper = observer(() => { if (expiry_type === 'endtime') handleEndTime(); - assertDurationIsWithinBoundary(current_duration); + assertDurationIsWithinBoundary(Number(current_duration)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [has_missing_duration_unit, simple_is_missing_duration_unit]); @@ -155,11 +163,11 @@ const DurationWrapper = observer(() => { onChange({ target: { name: 'expiry_type', value: 'duration' } }); } - if (duration !== current_duration) { + if (duration !== Number(current_duration)) { onChangeUiStore({ name: `duration_${duration_unit}`, value: duration }); } - assertDurationIsWithinBoundary(current_duration); + assertDurationIsWithinBoundary(Number(current_duration)); }, [ duration_unit, is_advanced_duration, diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx similarity index 73% rename from packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx rename to packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx index e60335988681..9deb9ce9f69d 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.tsx @@ -1,15 +1,44 @@ import classNames from 'classnames'; -import { PropTypes as MobxPropTypes } from 'mobx-react'; -import PropTypes from 'prop-types'; import React from 'react'; import { localize } from '@deriv/translations'; import Fieldset from 'App/Components/Form/fieldset'; import RangeSlider from 'App/Components/Form/RangeSlider'; import { Dropdown } from '@deriv/components'; import { toMoment, isVanillaContract } from '@deriv/shared'; -import DurationToggle from './duration-toggle.jsx'; -import AdvancedDuration from './advanced-duration.jsx'; -import SimpleDuration from './simple-duration.jsx'; +import { useStore } from '@deriv/stores'; +import { useTraderStore } from 'Stores/useTraderStores'; +import DurationToggle from './duration-toggle'; +import AdvancedDuration from './advanced-duration'; +import SimpleDuration from './simple-duration'; + +type TUIStore = ReturnType['ui']; +type TTradeStore = ReturnType; +export type TDuration = { + advanced_duration_unit: TUIStore['advanced_duration_unit']; + advanced_expiry_type: TUIStore['advanced_expiry_type']; + contract_type: TTradeStore['contract_type']; + duration: TTradeStore['duration']; + duration_t: TUIStore['duration_t']; + duration_unit: TTradeStore['duration_unit']; + duration_units_list: TTradeStore['duration_units_list']; + expiry_date: TTradeStore['expiry_date']; + expiry_epoch: TTradeStore['expiry_epoch']; + expiry_time: TTradeStore['expiry_time']; + expiry_type: TTradeStore['expiry_type']; + getDurationFromUnit: TUIStore['getDurationFromUnit']; + hasDurationUnit: (duration_type: string, is_advanced: boolean) => boolean; + is_advanced_duration: TUIStore['is_advanced_duration']; + is_minimized?: boolean; + market_open_times: TTradeStore['market_open_times']; + max_value: number | null; + min_value: number | null; + onChange: TTradeStore['onChange']; + onChangeMultiple: TTradeStore['onChangeMultiple']; + onChangeUiStore: TUIStore['onChangeUiStore']; + server_time?: moment.MomentInput; + simple_duration_unit: TUIStore['simple_duration_unit']; + start_date: TTradeStore['start_date']; +}; const Duration = ({ advanced_duration_unit, @@ -36,7 +65,7 @@ const Duration = ({ server_time, simple_duration_unit, start_date, -}) => { +}: TDuration) => { React.useEffect(() => { if (isVanillaContract(contract_type)) { onToggleDurationType({ target: { value: true, name: 'is_advanced_duration' } }); @@ -56,7 +85,7 @@ const Duration = ({ if (is_minimized) { const moment_expiry = toMoment(expiry_date); - const duration_unit_text = (duration_units_list.find(o => o.value === duration_unit) || {}).text; + const duration_unit_text = duration_units_list.find(({ value }) => value === duration_unit)?.text ?? ''; return (
{expiry_type === 'duration' @@ -66,18 +95,18 @@ const Duration = ({ ); } - const changeDurationUnit = ({ target }) => { + const changeDurationUnit = ({ target }: { target: { name: string; value: string } }) => { const { name, value } = target; const duration_value = getDurationFromUnit(value); onChangeUiStore({ name, value }); onChangeMultiple({ duration_unit: value, - duration: duration_value, + duration: Number(duration_value), }); }; - const changeDurationValue = ({ target }) => { + const changeDurationValue = ({ target }: { target: { name: string; value: string | number } }) => { const { name, value } = target; const duration_name = `duration_${is_advanced_duration ? advanced_duration_unit : simple_duration_unit}`; @@ -86,7 +115,7 @@ const Duration = ({ onChange({ target: { name, value: +value } }); }; - const onToggleDurationType = ({ target }) => { + const onToggleDurationType = ({ target }: { target: { name: string; value: boolean } }) => { const { name, value: is_advanced } = target; onChangeUiStore({ name, value: is_advanced }); @@ -101,7 +130,7 @@ const Duration = ({ }); } - const new_trade_store_values = {}; + const new_trade_store_values: Record = {}; // simple only has expiry type of duration if (!is_advanced && expiry_type !== 'duration') { @@ -111,7 +140,7 @@ const Duration = ({ new_trade_store_values.expiry_type = advanced_expiry_type; } - const has_same_duration = current_duration_unit === duration_unit && duration_value === duration; + const has_same_duration = current_duration_unit === duration_unit && Number(duration_value) === duration; if (!has_same_duration) { new_trade_store_values.duration_unit = current_duration_unit; new_trade_store_values.duration = duration_value; @@ -123,12 +152,12 @@ const Duration = ({ } }; - const props = { + const passthrough_props = { shared_input: { is_hj_whitelisted: true, onChange: changeDurationValue, - max_value, - min_value, + max_value: Number(max_value), + min_value: Number(min_value), }, number_input: { type: 'number', @@ -149,7 +178,6 @@ const Duration = ({ )} - {!has_toggle && } + {!has_toggle && } {has_toggle && ( <> {is_advanced_duration && ( @@ -175,11 +203,11 @@ const Duration = ({ expiry_type={expiry_type} getDurationFromUnit={getDurationFromUnit} market_open_times={market_open_times} - number_input_props={props.number_input} + number_input_props={passthrough_props.number_input} onChange={onChange} onChangeUiStore={onChangeUiStore} server_time={server_time} - shared_input_props={props.shared_input} + shared_input_props={passthrough_props.shared_input} start_date={start_date} /> )} @@ -189,8 +217,8 @@ const Duration = ({ changeDurationUnit={changeDurationUnit} duration_t={duration_t} duration_units_list={duration_units_list} - number_input_props={props.number_input} - shared_input_props={props.shared_input} + number_input_props={passthrough_props.number_input} + shared_input_props={passthrough_props.shared_input} simple_duration_unit={simple_duration_unit} /> )} @@ -207,33 +235,4 @@ const Duration = ({ ); }; -Duration.propTypes = { - advanced_duration_unit: PropTypes.string, - advanced_expiry_type: PropTypes.string, - contract_type: PropTypes.string, - duration: PropTypes.number, - duration_t: PropTypes.number, - duration_unit: PropTypes.string, - duration_units_list: MobxPropTypes.arrayOrObservableArray, - expiry_date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - expiry_time: PropTypes.string, - expiry_type: PropTypes.string, - getDurationFromUnit: PropTypes.func, - hasDurationUnit: PropTypes.func, - is_advanced_duration: PropTypes.bool, - is_minimized: PropTypes.bool, - market_open_times: PropTypes.array, - max_value: PropTypes.number, - min_value: PropTypes.number, - number_input: PropTypes.object, - onChange: PropTypes.func, - onChangeMultiple: PropTypes.func, - onChangeUiStore: PropTypes.func, - server_time: PropTypes.object, - shared_input: PropTypes.object, - simple_duration_unit: PropTypes.string, - start_date: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - validation_errors: PropTypes.object, -}; - export default Duration; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/expiry-text.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/expiry-text.tsx similarity index 78% rename from packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/expiry-text.jsx rename to packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/expiry-text.tsx index ba94abb09533..dc6a7fc3de1b 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/expiry-text.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/expiry-text.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { Text } from '@deriv/components'; import { Localize } from '@deriv/translations'; -const ExpiryText = ({ expiry_epoch, has_error, fixed_date }) => { +type TExpiryText = { + expiry_epoch?: number; + has_error?: boolean; + fixed_date?: string; +}; +const ExpiryText = ({ expiry_epoch, has_error, fixed_date }: TExpiryText) => { const formatted_date = expiry_epoch && !has_error ? new Date(expiry_epoch * 1000) diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/index.js b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/index.js deleted file mode 100644 index f23d94adbd34..000000000000 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import DurationWrapper from './duration-wrapper.jsx'; - -export default DurationWrapper; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/index.ts b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/index.ts new file mode 100644 index 000000000000..f53057797d7f --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/index.ts @@ -0,0 +1,3 @@ +import DurationWrapper from './duration-wrapper'; + +export default DurationWrapper; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/simple-duration.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/simple-duration.tsx similarity index 71% rename from packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/simple-duration.jsx rename to packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/simple-duration.tsx index 813c364c942e..d7ba8bed9c8e 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/simple-duration.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/simple-duration.tsx @@ -1,5 +1,3 @@ -import { PropTypes as MobxPropTypes } from 'mobx-react'; -import PropTypes from 'prop-types'; import React from 'react'; import { ButtonToggle, InputField } from '@deriv/components'; import RangeSlider from 'App/Components/Form/RangeSlider'; @@ -7,6 +5,23 @@ import TradingDatePicker from '../../DatePicker'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; +type TSimpleDuration = { + changeDurationUnit: React.ComponentProps['onChange']; + duration_t: number; + duration_units_list: ReturnType['duration_units_list']; + getDurationFromUnit: ReturnType['ui']['getDurationFromUnit']; + number_input_props: { + type: string; + is_incrementable: boolean; + }; + shared_input_props: { + is_hj_whitelisted: boolean; + onChange: ({ target }: { target: { name: string; value: string | number } }) => void; + max_value: number; + min_value: number; + }; + simple_duration_unit: string; +}; const SimpleDuration = observer( ({ changeDurationUnit, @@ -15,12 +30,14 @@ const SimpleDuration = observer( getDurationFromUnit, number_input_props, shared_input_props, - }) => { + simple_duration_unit: simple_duration_unit_prop, + }: TSimpleDuration) => { const { ui } = useStore(); - const { current_focus, setCurrentFocus, simple_duration_unit } = ui; + const { current_focus, setCurrentFocus } = ui; const { contract_expiry_type, validation_errors } = useTraderStore(); - const filterMinutesAndTicks = arr => { + const simple_duration_unit = simple_duration_unit_prop || ui.simple_duration_unit; + const filterMinutesAndTicks = (arr: TSimpleDuration['duration_units_list']) => { const filtered_arr = arr.filter(du => du.value === 't' || du.value === 'm'); if (filtered_arr.length <= 1) return []; @@ -41,7 +58,7 @@ const SimpleDuration = observer( /> )} {simple_duration_unit === 't' && contract_expiry_type === 'tick' && ( - + )} {simple_duration_unit === 'd' && ( @@ -53,7 +70,7 @@ const SimpleDuration = observer( current_focus={current_focus} error_messages={validation_errors.duration} name='duration' - label={has_label ? duration_units_list[0]?.text : null} + label={has_label ? duration_units_list[0]?.text : undefined} setCurrentFocus={setCurrentFocus} value={getDurationFromUnit(simple_duration_unit)} {...number_input_props} @@ -65,13 +82,4 @@ const SimpleDuration = observer( } ); -SimpleDuration.propTypes = { - changeDurationUnit: PropTypes.func, - duration_t: PropTypes.number, - duration_units_list: MobxPropTypes.arrayOrObservableArray, - getDurationFromUnit: PropTypes.func, - number_input_props: PropTypes.object, - shared_input_props: PropTypes.object, -}; - export default SimpleDuration; diff --git a/packages/trader/src/Modules/Trading/Containers/chart-widgets.tsx b/packages/trader/src/Modules/Trading/Containers/chart-widgets.tsx index ff46bc45f217..3a4c0a883fed 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']; }; @@ -20,13 +19,14 @@ type TChartBottomWidgets = { }; export const DigitsWidget = observer(({ digits, tick }: { digits: TDigits['digits_array']; tick: TDigits['tick'] }) => { - const { contract_trade } = useStore(); + const { contract_trade, ui } = useStore(); const { onChange: onDigitChange, symbol: underlying, contract_type: trade_type, last_digit: selected_digit, } = useTraderStore(); + const { is_mobile } = ui; const { last_contract } = contract_trade; const { contract_info = {}, digits_info = {}, display_status, is_digit_contract, is_ended } = last_contract; return ( @@ -37,6 +37,7 @@ export const DigitsWidget = observer(({ digits, tick }: { digits: TDigits['digit display_status={display_status} is_digit_contract={is_digit_contract} is_ended={is_ended} + is_mobile={is_mobile} onDigitChange={onDigitChange} is_trade_page tick={tick} @@ -48,16 +49,12 @@ 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 ( ); diff --git a/packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.jsx b/packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.jsx deleted file mode 100644 index c1f97db13bc5..000000000000 --- a/packages/trader/src/Modules/Trading/Containers/smart-chart-switcher.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { SmartChart } from 'Modules/SmartChart'; -import { SmartChartBeta } from 'Modules/SmartChartBeta'; - -const SmartChartSwitcher = ({ is_beta, ...props }) => { - 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..7dcd26c81b0b --- /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'; +}; + +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..9564bcde11ea --- /dev/null +++ b/packages/trader/src/Modules/Trading/Containers/trade-chart.tsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { ActiveSymbols } from '@deriv/api-types'; +import { 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; + has_barrier?: boolean; + is_accumulator: boolean; + topWidgets: () => React.ReactElement; +}; + +const TradeChart = observer((props: TTradeChartProps) => { + const { has_barrier, 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 } : {}), + ...(has_barrier ? { whitespace: 110 } : {}), + }; + + 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-params-mobile.tsx b/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.tsx index 98e19d4b2286..98da355f5e03 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.tsx +++ b/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.tsx @@ -2,7 +2,7 @@ import 'Sass/app/modules/trading-mobile.scss'; import { Div100vhContainer, Modal, Money, Tabs, ThemedScrollbars, usePreventIOSZoom } from '@deriv/components'; import AmountMobile from 'Modules/Trading/Components/Form/TradeParams/amount-mobile'; import Barrier from 'Modules/Trading/Components/Form/TradeParams/barrier'; -import DurationMobile from 'Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx'; +import DurationMobile from 'Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile'; import LastDigit from 'Modules/Trading/Components/Form/TradeParams/last-digit'; import { TTextValueStrings } from 'Types'; import { observer, useStore } from '@deriv/stores'; @@ -17,7 +17,7 @@ type TTradeParamsModal = { toggleModal: () => void; }; -type TTradeParamsMobile = { +export type TTradeParamsMobile = { currency: string; toggleModal: () => void; isVisible: (component_key: string) => boolean; @@ -283,7 +283,6 @@ const TradeParamsMobile = observer( {isVisible('duration') ? (
is migrated to TS toggleModal={toggleModal} amount_tab_idx={amount_tab_idx} duration_tab_idx={duration_tab_idx} diff --git a/packages/trader/src/Modules/Trading/Containers/trade-params.tsx b/packages/trader/src/Modules/Trading/Containers/trade-params.tsx index 1bec30adf6eb..d554bde58613 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade-params.tsx +++ b/packages/trader/src/Modules/Trading/Containers/trade-params.tsx @@ -29,10 +29,7 @@ const TradeParams = observer(({ is_minimized = false }: TTradeParams) => { return ( - {isVisible('duration') && ( - // @ts-expect-error: TODO: check if TS error is gone after is migrated to TS - - )} + {isVisible('duration') && } {isVisible('barrier') && } {isVisible('last_digit') && } {isVisible('accumulator') && } 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 1e22a86f62bc..000000000000 --- a/packages/trader/src/Modules/Trading/Containers/trade.jsx +++ /dev/null @@ -1,422 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { DesktopWrapper, Div100vhContainer, MobileWrapper, SwipeableWrapper } from '@deriv/components'; -import { 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, - has_barrier, - is_accumulator, - is_chart_loading, - is_market_closed, - is_synthetics_available, - is_synthetics_trading_market_available, - is_trade_enabled, - is_turbos, - is_vanilla, - onMount, - onUnmount, - prepareTradeStore, - setIsDigitsWidgetActive, - setMobileDigitView, - should_show_active_symbols_loading, - show_digits_stats, - symbol, - } = useTraderStore(); - const { - notification_messages_ui: NotificationMessages, - has_only_forward_starting_contracts: is_market_unavailable_visible, - 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]); - - 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, has_barrier, 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 } : {}), - ...(has_barrier ? { whitespace: 110 } : {}), - }; - - 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..84d82ef83d65 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Containers/trade.tsx @@ -0,0 +1,222 @@ +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, + has_barrier, + is_accumulator, + is_chart_loading, + is_market_closed, + is_synthetics_available, + is_synthetics_trading_market_available, + is_trade_enabled, + is_turbos, + is_vanilla, + onMount, + onUnmount, + prepareTradeStore, + 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, + } = 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]); + + 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 f297fb4c0a3b..a408cb1c1fcc 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,7 @@ type TChartLayout = { timeUnit: string; volumeUnderlay: boolean; }; -type TChartStateChangeOption = { +export type TChartStateChangeOption = { indicator_type_name?: string; indicators_category_name?: string; isClosed?: boolean; 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>; +};