diff --git a/packages/components/src/components/toast/toast.scss b/packages/components/src/components/toast/toast.scss index 8f60715265d5..04205cba254f 100644 --- a/packages/components/src/components/toast/toast.scss +++ b/packages/components/src/components/toast/toast.scss @@ -33,6 +33,12 @@ } } } + &__notification { + .dc-toast__message { + background: var(--general-active); + padding: 0.9rem 1.2rem; + } + } &--enter, &--exit { transform: scale(1, 0); diff --git a/packages/components/src/components/toast/toast.tsx b/packages/components/src/components/toast/toast.tsx index f430cf2fe767..a5000b05fa18 100644 --- a/packages/components/src/components/toast/toast.tsx +++ b/packages/components/src/components/toast/toast.tsx @@ -7,7 +7,7 @@ type TToast = { is_open?: boolean; onClick?: React.MouseEventHandler; onClose?: () => void; - type?: 'error' | 'info'; + type?: 'error' | 'info' | 'notification'; timeout?: number; }; diff --git a/packages/core/src/sass/app/_common/layout/app-layouts.scss b/packages/core/src/sass/app/_common/layout/app-layouts.scss index 0a736bc68167..029269bf47c4 100644 --- a/packages/core/src/sass/app/_common/layout/app-layouts.scss +++ b/packages/core/src/sass/app/_common/layout/app-layouts.scss @@ -69,6 +69,15 @@ position: absolute; right: 1.6rem; } + .dc-toast-popup-mobile { + display: inherit; + max-width: calc(100vw - 1.6rem); + position: absolute; + width: calc(100vw - 1.2rem); + top: 0.4rem; + left: 1.2rem; + right: 1.2rem; + } } } diff --git a/packages/trader/@deriv-stores.d.ts b/packages/trader/@deriv-stores.d.ts new file mode 100644 index 000000000000..4be7c2be78e2 --- /dev/null +++ b/packages/trader/@deriv-stores.d.ts @@ -0,0 +1,33 @@ +import type { TRootStore } from '@deriv/stores/types'; +import type TradeStore from './src/Stores/Modules/Trading/trade-store'; + +type TToastBoxListItem = { + contract_category: string; + contract_types: [ + { + text: string; + value: string; + } + ]; +}; + +type TToastBoxObject = { + key?: boolean; + buy_price?: number; + currency?: string; + contract_type?: string; + list?: TToastBoxListItem[]; +}; + +type TOverrideTradeStore = Omit & { + clearContractPurchaseToastBox: () => void; + contract_purchase_toast_box: TToastBoxObject; +}; + +declare module '@deriv/stores' { + export function useStore(): TRootStore & { + modules: { + trade: TOverrideTradeStore; + }; + }; +} diff --git a/packages/trader/build/loaders-config.js b/packages/trader/build/loaders-config.js index 244d0aa2d1b8..4024a6b51072 100644 --- a/packages/trader/build/loaders-config.js +++ b/packages/trader/build/loaders-config.js @@ -2,9 +2,6 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const path = require('path'); const js_loaders = [ - { - loader: '@deriv/shared/src/loaders/react-import-loader.js', - }, { loader: '@deriv/shared/src/loaders/deriv-account-loader.js', }, diff --git a/packages/trader/package.json b/packages/trader/package.json index 3f33bce0bed8..9748f72772f2 100644 --- a/packages/trader/package.json +++ b/packages/trader/package.json @@ -85,6 +85,7 @@ "@deriv/deriv-charts": "1.1.8", "@deriv/reports": "^1.0.0", "@deriv/shared": "^1.0.0", + "@deriv/stores": "^1.0.0", "@deriv/translations": "^1.0.0", "@types/classnames": "^2.2.11", "@types/react-loadable": "^5.5.6", diff --git a/packages/trader/src/App/app.tsx b/packages/trader/src/App/app.tsx index 83cae85459eb..5b091e7c2283 100644 --- a/packages/trader/src/App/app.tsx +++ b/packages/trader/src/App/app.tsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Loadable from 'react-loadable'; +import { StoreProvider } from '@deriv/stores'; import Routes from 'App/Containers/Routes/routes.jsx'; import TradeHeaderExtensions from 'App/Containers/trade-header-extensions.jsx'; import TradeFooterExtensions from 'App/Containers/trade-footer-extensions.jsx'; @@ -30,14 +31,14 @@ const App = ({ passthrough }: Apptypes) => { return ( - + - + ); }; diff --git a/packages/trader/src/Modules/SmartChart/Components/BuyToastNotification/__tests__/buy-toast-notification.spec.tsx b/packages/trader/src/Modules/SmartChart/Components/BuyToastNotification/__tests__/buy-toast-notification.spec.tsx new file mode 100644 index 000000000000..24cf64d60400 --- /dev/null +++ b/packages/trader/src/Modules/SmartChart/Components/BuyToastNotification/__tests__/buy-toast-notification.spec.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import BuyToastNotification from '../../buy-toast-notification'; +import { mockStore, StoreProvider } from '@deriv/stores'; + +describe('BuyToastNotification component', () => { + const mockActionToastbox = { + buy_price: '100', + currency: 'USD', + contract_type: 'rise_fall', + list: [ + { contract_category: 'up_down', contract_types: [{ text: 'Rise/Fall', value: 'rise_fall' }] }, + { contract_category: 'high_low', contract_types: [{ text: 'Higher/Lower', value: 'higher_lower' }] }, + ], + key: true, + }; + + const mock = mockStore({ + modules: { + trade: { + contract_purchase_toast_box: mockActionToastbox, + clearContractPurchaseToastBox: jest.fn(), + }, + }, + }); + + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'popup_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should render the notification ', () => { + render(, { + wrapper: ({ children }) => {children}, + }); + + expect(modal_root_el).toBeInTheDocument(); + }); + + it('should call clearContractPurchaseToastBox after 4 seconds', () => { + render(, { + wrapper: ({ children }) => {children}, + }); + + jest.advanceTimersByTime(4000); + expect(mock.modules.trade.clearContractPurchaseToastBox).toHaveBeenCalled(); + }); +}); diff --git a/packages/trader/src/Modules/SmartChart/Components/buy-toast-notification.tsx b/packages/trader/src/Modules/SmartChart/Components/buy-toast-notification.tsx new file mode 100644 index 000000000000..dae3f11dfd2f --- /dev/null +++ b/packages/trader/src/Modules/SmartChart/Components/buy-toast-notification.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { MobileWrapper, Toast, Text } from '@deriv/components'; +import { Localize } from '@deriv/translations'; +import { findContractCategory } from 'Modules/Trading/Helpers/contract-type'; +import { observer, useStore } from '@deriv/stores'; + +type TContractType = { + text: string; + value: string; +}; + +const BuyToastNotification = observer(() => { + const portal = document.getElementById('popup_root'); + const { modules } = useStore(); + const { trade } = modules; + const { contract_purchase_toast_box, clearContractPurchaseToastBox } = trade; + + React.useEffect(() => { + const timeout = setTimeout(() => { + clearContractPurchaseToastBox(); + }, 4000); + + return () => { + clearTimeout(timeout); + }; + }, [clearContractPurchaseToastBox, contract_purchase_toast_box]); + + if (!portal || !contract_purchase_toast_box) return ; + + const { buy_price, currency, contract_type, list } = contract_purchase_toast_box; + const active_trade_type = { value: contract_type }; + + const trade_type_name = findContractCategory(list, active_trade_type)?.contract_types?.find( + (item: TContractType) => item.value === contract_type + ).text; + + return ReactDOM.createPortal( + + + + ]} + values={{ trade_type_name, buy_price, currency }} + shouldUnescape + /> + + + , + portal + ); +}); + +export default BuyToastNotification; diff --git a/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx b/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx index 863f4410a743..9a7840505bab 100644 --- a/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx +++ b/packages/trader/src/Modules/SmartChart/Components/top-widgets.jsx @@ -6,6 +6,7 @@ import { localize } from '@deriv/translations'; import { isEnded, isAccumulatorContract, isDigitContract } from '@deriv/shared'; import { connect } from 'Stores/connect'; import { ChartTitle } from 'Modules/SmartChart'; +import BuyToastNotification from './buy-toast-notification'; const TradeInfo = ({ markers_array, granularity }) => { const latest_tick_contract = markers_array[markers_array.length - 1]; @@ -64,6 +65,7 @@ const TopWidgets = ({ width: `calc(100% - ${y_axis_width ? y_axis_width + 5 : 0}px)`, }} > + {is_mobile && } {ChartTitleLocal} {!is_digits_widget_active && } diff --git a/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-widget.jsx b/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-widget.jsx index f3abf466aa11..37efe808c046 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-widget.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-widget.jsx @@ -168,6 +168,7 @@ const ContractTypeWidget = ({ is_equal, name, value, list, onChange, languageCha .filter(type => type.value !== 'rise_fall_equal') .findIndex(type => type.value === item?.value); }; + return (
null; debouncedProposal = debounce(this.requestProposal, 500); proposal_requests = {}; @@ -324,6 +330,9 @@ export default class TradeStore extends BaseStore { updateLimitOrderBarriers: action.bound, updateStore: action.bound, updateSymbol: action.bound, + contract_purchase_toast_box: observable, + clearContractPurchaseToastBox: action.bound, + setContractPurchaseToastbox: action.bound, }); // Adds intercept to change min_max value of duration validation @@ -837,6 +846,7 @@ export default class TradeStore extends BaseStore { this.debouncedProposal(); this.clearLimitOrderBarriers(); this.pushPurchaseDataToGtm(contract_data); + this.setContractPurchaseToastbox(response.buy); this.is_purchasing_contract = false; return; } @@ -1560,6 +1570,22 @@ export default class TradeStore extends BaseStore { return this.contract_type === 'vanilla'; } + setContractPurchaseToastbox(response) { + const list = getAvailableContractTypes(this.contract_types_list, unsupported_contract_types_list); + + return (this.contract_purchase_toast_box = { + key: true, + buy_price: formatMoney(this.root_store.client.currency, response.buy_price, true, 0, 0), + contract_type: this.contract_type, + currency: getCurrencyDisplayCode(this.root_store.client.currency), + list, + }); + } + + clearContractPurchaseToastBox() { + this.contract_purchase_toast_box = undefined; + } + async getFirstOpenMarket(markets_to_search) { if (this.active_symbols?.length) { return findFirstOpenMarket(this.active_symbols, markets_to_search); diff --git a/packages/trader/tsconfig.json b/packages/trader/tsconfig.json index 197b6642c58e..f980e4bcac9b 100644 --- a/packages/trader/tsconfig.json +++ b/packages/trader/tsconfig.json @@ -19,5 +19,5 @@ "outDir": "./dist", "baseUrl": "./" }, - "include": ["src", "../../utils.d.ts"] + "include": ["src", "../../globals.d.ts", "../../utils.d.ts", "@deriv-stores.d.ts"] } diff --git a/packages/translations/src/components/localize.jsx b/packages/translations/src/components/localize.jsx index f11016a5e391..80496f2d19f9 100644 --- a/packages/translations/src/components/localize.jsx +++ b/packages/translations/src/components/localize.jsx @@ -2,8 +2,15 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Trans } from 'react-i18next'; -const Localize = ({ i18n_default_text, values, components, options, i18n }) => ( - +const Localize = ({ i18n_default_text, values, components, options, i18n, shouldUnescape }) => ( + ); // Trans needs to have the i18n instance in scope @@ -16,6 +23,7 @@ Localize.propTypes = { i18n_default_text: PropTypes.string, options: PropTypes.object, values: PropTypes.object, + shouldUnescape: PropTypes.bool, }; export default withI18n;