diff --git a/packages/components/src/components/collapsible/collapsible.scss b/packages/components/src/components/collapsible/collapsible.scss index fc02a5bfa3c7..57f0c5ef3ed0 100644 --- a/packages/components/src/components/collapsible/collapsible.scss +++ b/packages/components/src/components/collapsible/collapsible.scss @@ -77,7 +77,6 @@ .trade-container { &__fieldset { flex: 1; - margin-left: 0.4rem; .dc-button-menu__wrapper { height: 4rem; diff --git a/packages/components/src/components/contract-card/contract-card-items/__tests__/turbos-card-body.spec.tsx b/packages/components/src/components/contract-card/contract-card-items/__tests__/turbos-card-body.spec.tsx new file mode 100644 index 000000000000..3f4a2f4b0680 --- /dev/null +++ b/packages/components/src/components/contract-card/contract-card-items/__tests__/turbos-card-body.spec.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { render, screen } from '@testing-library/react'; +import { TContractInfo } from '@deriv/shared/src/utils/contract/contract-types'; +import TurbosCardBody from '../turbos-card-body'; + +const contract_info: TContractInfo = { + contract_id: 1, + bid_price: 1044.02, + buy_price: 1044.0, + profit: 50, + barrier: '10904.80', + entry_spot_display_value: '1046.80', + sell_price: 1046.8, +}; + +const mockCardLabels = () => ({ + BARRIER: 'Barrier', + CONTRACT_VALUE: 'Contract value', + ENTRY_SPOT: 'Entry spot', + TAKE_PROFIT: 'Take profit', + TOTAL_PROFIT_LOSS: 'Total profit/loss', + PURCHASE_PRICE: 'Buy price', +}); + +describe('TurbosCardBody', () => { + const mock_props = { + addToast: jest.fn(), + connectWithContractUpdate: jest.fn(), + contract_info, + contract_update: { + take_profit: { + display_name: 'Take profit', + order_amount: 0, + order_date: 1678948046, + }, + }, + currency: 'USD', + current_focus: null, + error_message_alignment: 'left', + getCardLabels: mockCardLabels, + getContractById: jest.fn(), + is_sold: false, + onMouseLeave: jest.fn(), + removeToast: jest.fn(), + setCurrentFocus: jest.fn(), + status: 'profit', + progress_slider_mobile_el: false, + }; + beforeAll(() => { + (ReactDOM.createPortal as jest.Mock) = jest.fn(component => { + return component; + }); + }); + + it('renders header and values correctly', () => { + render(); + const buy_price_header = screen.getByText(mockCardLabels().PURCHASE_PRICE); + expect(buy_price_header).toBeInTheDocument(); + const buy_price_amount = screen.getByText('1,044.00'); + expect(buy_price_amount).toBeInTheDocument(); + + const entry_spot_header = screen.getByText(mockCardLabels().ENTRY_SPOT); + expect(entry_spot_header).toBeInTheDocument(); + const entry_spot_amount = screen.getByText('1,046.80'); + expect(entry_spot_amount).toBeInTheDocument(); + + const barrier_header = screen.getByText(mockCardLabels().BARRIER); + expect(barrier_header).toBeInTheDocument(); + const barrier_level = screen.getByText('10,904.80'); + expect(barrier_level).toBeInTheDocument(); + + const take_profit_header = screen.getByText(mockCardLabels().TAKE_PROFIT); + expect(take_profit_header).toBeInTheDocument(); + const take_profit_amount = screen.getByText('-'); + expect(take_profit_amount).toBeInTheDocument(); + + const total_profit_loss_header = screen.getByText(mockCardLabels().TOTAL_PROFIT_LOSS); + expect(total_profit_loss_header).toBeInTheDocument(); + const total_profit_loss_amount = screen.getByText('50.00'); + expect(total_profit_loss_amount).toBeInTheDocument(); + }); +}); diff --git a/packages/components/src/components/contract-card/contract-card-items/contract-card-body.tsx b/packages/components/src/components/contract-card/contract-card-items/contract-card-body.tsx index 5155ec8e6265..f9430a530f10 100644 --- a/packages/components/src/components/contract-card/contract-card-items/contract-card-body.tsx +++ b/packages/components/src/components/contract-card/contract-card-items/contract-card-body.tsx @@ -11,16 +11,16 @@ import { ResultStatusIcon } from '../result-overlay/result-overlay'; import ProgressSliderMobile from '../../progress-slider-mobile'; import AccumulatorCardBody from './accumulator-card-body'; import MultiplierCardBody from './multiplier-card-body'; +import TurbosCardBody from './turbos-card-body'; import VanillaOptionsCardBody from './vanilla-options-card-body'; import { TContractInfo, TContractStore } from '@deriv/shared/src/utils/contract/contract-types'; -import { ContractUpdate } from '@deriv/api-types'; import { TToastConfig } from '../../types/contract.types'; import { TGetCardLables } from '../../types/common.types'; export type TGeneralContractCardBodyProps = { addToast: (toast_config: TToastConfig) => void; contract_info: TContractInfo; - contract_update: ContractUpdate; + contract_update: TContractInfo['contract_update']; connectWithContractUpdate?: (contract_update_form: React.ElementType) => React.ElementType; currency: string; current_focus?: string; @@ -34,16 +34,17 @@ export type TGeneralContractCardBodyProps = { onMouseLeave: () => void; removeToast: (toast_id: string) => void; setCurrentFocus: (name: string) => void; - status: string; + status?: string; toggleCancellationWarning: () => void; progress_slider?: React.ReactNode; - is_accumulator?: boolean; is_positions?: boolean; }; export type TContractCardBodyProps = { + is_accumulator?: boolean; is_multiplier: boolean; server_time: moment.Moment; + is_turbos?: boolean; is_vanilla?: boolean; } & TGeneralContractCardBodyProps; @@ -63,6 +64,7 @@ const ContractCardBody = ({ is_multiplier, is_positions, is_sold, + is_turbos, is_vanilla, onMouseLeave, removeToast, @@ -90,51 +92,61 @@ const ContractCardBody = ({ /> ); + const toggle_card_dialog_props = { + addToast, + connectWithContractUpdate, + current_focus, + error_message_alignment, + getContractById, + onMouseLeave, + removeToast, + setCurrentFocus, + }; + let card_body; if (is_multiplier) { card_body = ( ); } else if (is_accumulator) { card_body = ( + ); + } else if (is_turbos) { + card_body = ( + ); } else if (is_vanilla) { @@ -209,7 +221,7 @@ const ContractCardBody = ({
{card_body} diff --git a/packages/components/src/components/contract-card/contract-card-items/contract-card-footer.tsx b/packages/components/src/components/contract-card/contract-card-items/contract-card-footer.tsx index 0e036df03929..d599eb7b1081 100644 --- a/packages/components/src/components/contract-card/contract-card-items/contract-card-footer.tsx +++ b/packages/components/src/components/contract-card/contract-card-items/contract-card-footer.tsx @@ -15,7 +15,7 @@ export type TCardFooterPropTypes = { is_sell_requested: boolean; onClickCancel: (contract_id?: number) => void; onClickSell: (contract_id?: number) => void; - onFooterEntered: () => void; + onFooterEntered?: () => void; server_time: moment.Moment; should_show_transition: boolean; }; diff --git a/packages/components/src/components/contract-card/contract-card-items/contract-card-header.tsx b/packages/components/src/components/contract-card/contract-card-items/contract-card-header.tsx index 80295e90933f..0e79629122eb 100644 --- a/packages/components/src/components/contract-card/contract-card-items/contract-card-header.tsx +++ b/packages/components/src/components/contract-card/contract-card-items/contract-card-header.tsx @@ -1,14 +1,17 @@ import React from 'react'; import classNames from 'classnames'; import { CSSTransition } from 'react-transition-group'; +import { localize } from '@deriv/translations'; import { isHighLow, getCurrentTick, getGrowthRatePercentage, - isBot, + getContractSubtype, isAccumulatorContract, + isBot, isOnlyUpsDownsContract, isMobile, + isTurbosContract, } from '@deriv/shared'; import ContractTypeCell from './contract-type-cell'; import Button from '../../button'; @@ -66,28 +69,42 @@ const ContractCardHeader = ({ const is_accumulator = isAccumulatorContract(contract_type); const is_only_ups_downs = isOnlyUpsDownsContract(contract_type); const is_mobile = isMobile(); - const contract_type_list_info = [ - { - is_param_displayed: multiplier, - displayed_param: `x${multiplier}`, - }, - { - is_param_displayed: is_accumulator, - displayed_param: `${getGrowthRatePercentage(growth_rate || 0)}%`, - }, - ]; + const is_turbos = isTurbosContract(contract_type); + + const contract_type_list_info = React.useMemo( + () => [ + { + is_param_displayed: multiplier, + displayed_param: `x${multiplier}`, + }, + { + is_param_displayed: is_accumulator, + displayed_param: `${getGrowthRatePercentage(growth_rate || 0)}%`, + }, + { + is_param_displayed: is_turbos, + displayed_param: + getContractSubtype(contract_type || '') === 'Long' ? localize('Long') : localize('Short'), + }, + ], + [multiplier, growth_rate, is_accumulator, is_turbos, contract_type] + ); + const displayed_trade_param = contract_type_list_info.find(contract_type_item_info => contract_type_item_info.is_param_displayed) ?.displayed_param || ''; return ( - <> +
)} - + ); }; diff --git a/packages/components/src/components/contract-card/contract-card-items/contract-card-sell.tsx b/packages/components/src/components/contract-card/contract-card-items/contract-card-sell.tsx index 1ac287669b84..c8b063294808 100644 --- a/packages/components/src/components/contract-card/contract-card-items/contract-card-sell.tsx +++ b/packages/components/src/components/contract-card/contract-card-items/contract-card-sell.tsx @@ -9,7 +9,7 @@ export type TContractCardSellProps = { contract_info: TContractInfo; getCardLabels: TGetCardLables; is_sell_requested: boolean; - onClickSell: (contract_id?: number) => void; + onClickSell?: (contract_id?: number) => void; }; const ContractCardSell = ({ contract_info, getCardLabels, is_sell_requested, onClickSell }: TContractCardSellProps) => { @@ -17,7 +17,7 @@ const ContractCardSell = ({ contract_info, getCardLabels, is_sell_requested, onC const should_show_sell = hasContractEntered(contract_info) && isOpen(contract_info); const onClick = (ev: React.MouseEvent) => { - onClickSell(contract_info.contract_id); + onClickSell?.(contract_info.contract_id); ev.stopPropagation(); ev.preventDefault(); }; diff --git a/packages/components/src/components/contract-card/contract-card-items/contract-update-form.tsx b/packages/components/src/components/contract-card/contract-card-items/contract-update-form.tsx index 5b23fea516f7..c423695f5972 100644 --- a/packages/components/src/components/contract-card/contract-card-items/contract-update-form.tsx +++ b/packages/components/src/components/contract-card/contract-card-items/contract-update-form.tsx @@ -6,6 +6,7 @@ import { getLimitOrderAmount, isCryptocurrency, isDeepEqual, + isMultiplierContract, pick, getTotalProfit, } from '@deriv/shared'; @@ -14,11 +15,21 @@ import Icon from '../../icon'; import MobileWrapper from '../../mobile-wrapper'; import Money from '../../money'; import InputWithCheckbox from '../../input-wth-checkbox'; -import { TGetCardLables, TToastConfig } from '../../types'; import { TContractStore } from '@deriv/shared/src/utils/contract/contract-types'; - -export type TContractUpdateFormProps = { - addToast: (toast_config: TToastConfig) => void; +import { TGeneralContractCardBodyProps } from './contract-card-body'; +import { TGetCardLables } from '../../types'; + +export type TContractUpdateFormProps = Pick< + TGeneralContractCardBodyProps, + | 'addToast' + | 'current_focus' + | 'error_message_alignment' + | 'getCardLabels' + | 'onMouseLeave' + | 'removeToast' + | 'setCurrentFocus' + | 'status' +> & { contract: TContractStore; current_focus?: string; error_message_alignment: string; @@ -30,6 +41,7 @@ export type TContractUpdateFormProps = { toggleDialog: (e: React.MouseEvent) => void; getContractById: (contract_id: number) => TContractStore; is_accumulator?: boolean; + is_turbos?: boolean; }; const ContractUpdateForm = (props: TContractUpdateFormProps) => { @@ -39,6 +51,7 @@ const ContractUpdateForm = (props: TContractUpdateFormProps) => { current_focus, error_message_alignment, getCardLabels, + is_turbos, is_accumulator, onMouseLeave, removeToast, @@ -79,14 +92,14 @@ const ContractUpdateForm = (props: TContractUpdateFormProps) => { const isValid = (val?: number | null) => !(val === undefined || val === null); const is_take_profit_valid = has_contract_update_take_profit - ? Number(contract_update_take_profit) > 0 + ? +contract_update_take_profit > 0 : isValid(stop_loss); - const is_stop_loss_valid = has_contract_update_stop_loss - ? Number(contract_update_stop_loss) > 0 - : isValid(take_profit); - const is_valid_accu_contract_update = is_accumulator && !!is_take_profit_valid; - const is_valid_contract_update = - is_valid_accu_contract_update || (is_valid_to_cancel ? false : !!(is_take_profit_valid || is_stop_loss_valid)); + const is_stop_loss_valid = has_contract_update_stop_loss ? +contract_update_stop_loss > 0 : isValid(take_profit); + const is_multiplier = isMultiplierContract(contract_info.contract_type || ''); + const is_valid_multiplier_contract_update = is_valid_to_cancel + ? false + : !!(is_take_profit_valid || is_stop_loss_valid); + const is_valid_contract_update = is_multiplier ? is_valid_multiplier_contract_update : !!is_take_profit_valid; const getStateToCompare = ( _state: Partial & TContractUpdateFormProps> @@ -142,7 +155,7 @@ const ContractUpdateForm = (props: TContractUpdateFormProps) => { onChange={onChange} error_message_alignment={error_message_alignment || 'right'} value={contract_profit_or_loss.contract_update_take_profit} - is_disabled={!is_accumulator && !!is_valid_to_cancel} + is_disabled={is_multiplier && !!is_valid_to_cancel} setCurrentFocus={setCurrentFocus} /> ); @@ -201,11 +214,11 @@ const ContractUpdateForm = (props: TContractUpdateFormProps) => {
{take_profit_input}
- {!is_accumulator &&
{stop_loss_input}
} + {is_multiplier &&
{stop_loss_input}
}
diff --git a/packages/components/src/components/data-list/data-list.scss b/packages/components/src/components/data-list/data-list.scss index 6695d3069c10..7de0e18b45b2 100644 --- a/packages/components/src/components/data-list/data-list.scss +++ b/packages/components/src/components/data-list/data-list.scss @@ -79,7 +79,7 @@ &--wrapper:not(.data-list__item--dynamic-height-wrapper) { height: 100%; } - &--vanilla { + &--timer { flex: none; } } diff --git a/packages/components/src/components/data-list/data-list.tsx b/packages/components/src/components/data-list/data-list.tsx index 204874cad948..79c606efbc8e 100644 --- a/packages/components/src/components/data-list/data-list.tsx +++ b/packages/components/src/components/data-list/data-list.tsx @@ -13,25 +13,32 @@ import { IndexRange, } from 'react-virtualized'; import { isMobile, isDesktop } from '@deriv/shared'; -import DataListCell from './data-list-cell'; +import DataListCell, { TColIndex, TDataListCell } from './data-list-cell'; import DataListRow from './data-list-row'; import ThemedScrollbars from '../themed-scrollbars'; import { MeasuredCellParent } from 'react-virtualized/dist/es/CellMeasurer'; +type TMobileRowRenderer = { + row?: TRow; + is_footer?: boolean; + columns_map?: Record; + server_time?: moment.Moment; + onClickCancel: (contract_id?: number) => void; + onClickSell: (contract_id?: number) => void; + measure?: () => void; +}; const List = _List as unknown as React.FC; const AutoSizer = _AutoSizer as unknown as React.FC; const CellMeasurer = _CellMeasurer as unknown as React.FC; -export type TRowRenderer = (params: { row: any; is_footer?: boolean; measure?: () => void }) => React.ReactNode; +export type TRowRenderer = (params: Partial) => React.ReactNode; export type TPassThrough = { isTopUp: (item: TRow) => boolean }; -export type TRow = { - [key: string]: string; -}; +export type TRow = { [key: string]: any }; -type TDataList = { +export type TDataList = { className?: string; data_source: TRow[]; - footer?: React.ReactNode; - getRowAction?: (row: TRow) => string; + footer?: TRow; + getRowAction?: (row: TRow) => { component: JSX.Element } | string; getRowSize?: (params: { index: number }) => number; keyMapper?: (row: TRow) => number | string; onRowsRendered?: (params: IndexRange) => void; @@ -139,7 +146,7 @@ const DataList = React.memo( ); }; - const handleScroll: React.UIEventHandler = ev => { + const handleScroll = (ev: Partial>) => { let timeout; clearTimeout(timeout); @@ -154,7 +161,7 @@ const DataList = React.memo( setScrollTop((ev.target as HTMLElement).scrollTop); if (typeof onScroll === 'function') { - onScroll(ev); + onScroll(ev as React.UIEvent); } }; @@ -198,7 +205,12 @@ const DataList = React.memo( width={width} {...(isDesktop() ? { scrollTop: scroll_top, autoHeight: true } - : { onScroll: target => handleScroll({ target } as any) })} + : { + onScroll: target => + handleScroll({ target } as unknown as Partial< + React.UIEvent + >), + })} /> @@ -219,9 +231,9 @@ const DataList = React.memo(
); } -); +) as React.MemoExoticComponent<(props: TDataList) => JSX.Element> & { Cell: typeof DataListCell }; DataList.displayName = 'DataList'; -(DataList as any).Cell = DataListCell; +DataList.Cell = DataListCell; export default DataList; diff --git a/packages/components/src/components/data-table/data-table.tsx b/packages/components/src/components/data-table/data-table.tsx index a92b9e0ba9d6..e37e1dd6acf7 100644 --- a/packages/components/src/components/data-table/data-table.tsx +++ b/packages/components/src/components/data-table/data-table.tsx @@ -22,7 +22,7 @@ const AutoSizer = _AutoSizer as unknown as React.FC; const CellMeasurer = _CellMeasurer as unknown as React.FC; export type TSource = { - [key: string]: string; + [key: string]: unknown; }; type TMeasure = { @@ -40,19 +40,19 @@ type TDataTable = { className: string; content_loader: React.ElementType; columns: TSource[]; - contract_id: number; - getActionColumns: (params: { row_obj?: TSource; is_header?: boolean; is_footer: boolean }) => TTableRowItem[]; + contract_id?: number; + getActionColumns?: (params: { row_obj?: TSource; is_header?: boolean; is_footer: boolean }) => TTableRowItem[]; getRowSize?: ((params: { index: number }) => number) | number; - measure: () => void; - getRowAction?: (item: TSource) => TTableRowItem; - onScroll: React.UIEventHandler; - id: number; - passthrough: (item: TSource) => boolean; + measure?: () => void; + getRowAction?: (row: Record) => { component: JSX.Element } | string; + onScroll?: React.UIEventHandler; + id?: number; + passthrough?: (item: TSource) => boolean; autoHide?: boolean; - footer: boolean; + footer: Record; preloaderCheck: (param: TSource) => boolean; data_source: TSource[]; - keyMapper: (row: TSource) => number | string; + keyMapper?: (row: TSource) => number | string; }; const DataTable = ({ @@ -113,7 +113,7 @@ const DataTable = ({ columns={columns} content_loader={content_loader} getActionColumns={getActionColumns} - id={contract_id} + id={contract_id as string} key={id} measure={measure} passthrough={passthrough} @@ -128,7 +128,13 @@ const DataTable = ({ ); return is_dynamic_height ? ( - + {({ measure }) =>
{getContent({ measure })}
}
) : ( diff --git a/packages/components/src/components/icon-trade-types/icon-trade-types.tsx b/packages/components/src/components/icon-trade-types/icon-trade-types.tsx index f869f55537ad..b2e32776c1c4 100644 --- a/packages/components/src/components/icon-trade-types/icon-trade-types.tsx +++ b/packages/components/src/components/icon-trade-types/icon-trade-types.tsx @@ -76,6 +76,10 @@ const IconTradeTypes = ({ type, className, ...props }: TIconTradeTypes) => { return ; case 'ticklow': return ; + case 'turboslong': + return ; + case 'turbosshort': + return ; case 'upordown': return ; case 'vanillalongcall': diff --git a/packages/components/src/components/icon/common/ic-cat-turbos.svg b/packages/components/src/components/icon/common/ic-cat-turbos.svg new file mode 100644 index 000000000000..38afd4991365 --- /dev/null +++ b/packages/components/src/components/icon/common/ic-cat-turbos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/icon/icons.js b/packages/components/src/components/icon/icons.js index 592fb2fcb33a..b8bedd13878e 100644 --- a/packages/components/src/components/icon/icons.js +++ b/packages/components/src/components/icon/icons.js @@ -302,6 +302,7 @@ import './common/ic-cat-accumulator.svg'; import './common/ic-cat-all.svg'; import './common/ic-cat-multiplier.svg'; import './common/ic-cat-options.svg'; +import './common/ic-cat-turbos.svg'; import './common/ic-chart.svg'; import './common/ic-charts-tab-dbot.svg'; import './common/ic-chat.svg'; @@ -813,6 +814,8 @@ import './tradetype/ic-tradetype-runhigh.svg'; import './tradetype/ic-tradetype-runlow.svg'; import './tradetype/ic-tradetype-tickhigh.svg'; import './tradetype/ic-tradetype-ticklow.svg'; +import './tradetype/ic-tradetype-turboslong.svg'; +import './tradetype/ic-tradetype-turbosshort.svg'; import './tradetype/ic-tradetype-upordown.svg'; import './tradetype/ic-tradetype-vanilla-long-call.svg'; import './tradetype/ic-tradetype-vanilla-long-put.svg'; diff --git a/packages/components/src/components/icon/tradetype/ic-tradetype-turboslong.svg b/packages/components/src/components/icon/tradetype/ic-tradetype-turboslong.svg new file mode 100644 index 000000000000..4b22f1517365 --- /dev/null +++ b/packages/components/src/components/icon/tradetype/ic-tradetype-turboslong.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/icon/tradetype/ic-tradetype-turbosshort.svg b/packages/components/src/components/icon/tradetype/ic-tradetype-turbosshort.svg new file mode 100644 index 000000000000..a870ac0ff5f8 --- /dev/null +++ b/packages/components/src/components/icon/tradetype/ic-tradetype-turbosshort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/mobile-dialog/mobile-dialog.tsx b/packages/components/src/components/mobile-dialog/mobile-dialog.tsx index 4407f7942071..13355bb5d346 100644 --- a/packages/components/src/components/mobile-dialog/mobile-dialog.tsx +++ b/packages/components/src/components/mobile-dialog/mobile-dialog.tsx @@ -12,7 +12,7 @@ type TMobileDialog = { has_content_scroll?: boolean; portal_element_id: string; renderTitle?: () => string; - title?: string; + title?: React.ReactNode; visible?: boolean; wrapper_classname?: string; header_classname?: string; diff --git a/packages/components/src/components/positions-drawer-card/positions-drawer-card.tsx b/packages/components/src/components/positions-drawer-card/positions-drawer-card.tsx index c68fd8834018..a929e246887a 100644 --- a/packages/components/src/components/positions-drawer-card/positions-drawer-card.tsx +++ b/packages/components/src/components/positions-drawer-card/positions-drawer-card.tsx @@ -1,13 +1,13 @@ import React from 'react'; import classNames from 'classnames'; import { NavLink } from 'react-router-dom'; -import type { ContractUpdate } from '@deriv/api-types'; import ContractCard from '../contract-card'; import { getContractPath, isAccumulatorContract, isCryptoContract, isMultiplierContract, + isTurbosContract, getCardLabels, getContractTypeDisplay, getEndTime, @@ -17,42 +17,35 @@ import { import { TContractInfo, TContractStore } from '@deriv/shared/src/utils/contract/contract-types'; import { TToastConfig } from '../types/contract.types'; -type TGetEndTime = Pick< - TContractInfo, - 'is_expired' | 'sell_time' | 'status' | 'tick_count' | 'underlying' | 'contract_id' -> & - Required>; - type TPositionsDrawerCardProps = { addToast: (toast_config: TToastConfig) => void; className?: string; - contract_info: TGetEndTime; - contract_update: ContractUpdate; + contract_info: TContractInfo; + contract_update?: TContractInfo['contract_update']; currency: string; current_focus: string; - display_name: string; + display_name?: string; getContractById: (contract_id?: number) => TContractStore; - is_mobile: boolean; - is_sell_requested: boolean; - is_unsupported: boolean; + is_mobile?: boolean; + is_sell_requested?: boolean; + is_unsupported?: boolean; is_link_disabled: boolean; - profit_loss: number; + profit_loss?: number; onClickCancel: (contract_id?: number) => void; onClickSell: (contract_id?: number) => void; onClickRemove: (contract_id?: number) => void; - onFooterEntered: () => void; + onFooterEntered?: () => void; onMouseEnter?: () => void; onMouseLeave?: () => void; removeToast: (key: string) => void; - result: string; + result?: string; setCurrentFocus: (value: string) => void; server_time: moment.Moment; - should_show_transition: boolean; + should_show_transition?: boolean; should_show_cancellation_warning: boolean; status: string; toggleCancellationWarning: () => void; toggleUnsupportedContractModal: (value: boolean) => void; - measure?: () => void; }; const PositionsDrawerCard = ({ @@ -67,7 +60,6 @@ const PositionsDrawerCard = ({ is_sell_requested, is_unsupported, is_link_disabled, - measure, profit_loss, onClickCancel, onClickSell, @@ -85,16 +77,17 @@ const PositionsDrawerCard = ({ toggleCancellationWarning, toggleUnsupportedContractModal, }: TPositionsDrawerCardProps) => { - const is_accumulator = isAccumulatorContract(contract_info.contract_type || ''); + const is_accumulator = isAccumulatorContract(contract_info.contract_type); const is_multiplier = isMultiplierContract(contract_info.contract_type || ''); - const is_vanilla = isVanillaContract(contract_info.contract_type || ''); + const is_turbos = isTurbosContract(contract_info.contract_type); + const is_vanilla = isVanillaContract(contract_info.contract_type); const is_crypto = isCryptoContract(contract_info.underlying || ''); const has_progress_slider = !is_multiplier || (is_crypto && is_multiplier); const has_ended = !!getEndTime(contract_info); const is_mobile = isMobile(); const contract_card_classname = classNames('dc-contract-card', { - 'dc-contract-card--green': profit_loss > 0 && !result, - 'dc-contract-card--red': profit_loss < 0 && !result, + 'dc-contract-card--green': Number(profit_loss) > 0 && !result, + 'dc-contract-card--red': Number(profit_loss) < 0 && !result, }); const loader_el = ( @@ -102,16 +95,15 @@ const PositionsDrawerCard = ({
); - const card_header = ( @@ -121,7 +113,7 @@ const PositionsDrawerCard = ({ ); @@ -193,10 +186,10 @@ const PositionsDrawerCard = ({ getContractPath={getContractPath} is_multiplier={is_multiplier} is_positions - is_unsupported={is_unsupported} + is_unsupported={!!is_unsupported} onClickRemove={onClickRemove} - profit_loss={profit_loss} - result={result} + profit_loss={Number(profit_loss)} + result={result ?? ''} should_show_result_overlay={true} toggleUnsupportedContractModal={toggleUnsupportedContractModal} > diff --git a/packages/components/src/components/select-native/select-native.tsx b/packages/components/src/components/select-native/select-native.tsx index cb3bed57d512..3954e5a5af90 100644 --- a/packages/components/src/components/select-native/select-native.tsx +++ b/packages/components/src/components/select-native/select-native.tsx @@ -10,7 +10,7 @@ type TSelectNative = { classNameHint?: string; error?: string; hint?: string; - label: string; + label?: string; placeholder?: string; should_show_empty_option?: boolean; suffix_icon?: string; diff --git a/packages/components/stories/contract-card/statics/contract.js b/packages/components/stories/contract-card/statics/contract.js index cdd2501ea056..a775c2250d7b 100644 --- a/packages/components/stories/contract-card/statics/contract.js +++ b/packages/components/stories/contract-card/statics/contract.js @@ -2,6 +2,7 @@ import { localize } from '@deriv/translations'; export const getCardLabels = () => ({ APPLY: 'Apply', + BARRIER: 'Barrier:', STAKE: 'Stake:', CLOSE: 'Close', CANCEL: 'Cancel', @@ -235,6 +236,16 @@ export const getSupportedContracts = is_high_low => ({ name: 'Down', position: 'bottom', }, + TURBOSLONG: { + button_name: 'Long', + name: 'Turbos', + position: 'top', + }, + TURBOSSHORT: { + button_name: 'Short', + name: 'Turbos', + position: 'bottom', + }, RUNHIGH: { name: 'Only Ups', position: 'top', diff --git a/packages/components/stories/icon/icons.js b/packages/components/stories/icon/icons.js index 9a4f544a2c02..cd7aee024c02 100644 --- a/packages/components/stories/icon/icons.js +++ b/packages/components/stories/icon/icons.js @@ -311,6 +311,7 @@ export const icons = 'IcCatAll', 'IcCatMultiplier', 'IcCatOptions', + 'IcCatTurbos', 'IcChart', 'IcChartsTabDbot', 'IcChat', @@ -842,6 +843,8 @@ export const icons = 'IcTradetypeRunlow', 'IcTradetypeTickhigh', 'IcTradetypeTicklow', + 'IcTradetypeTurboslong', + 'IcTradetypeTurbosshort', 'IcTradetypeUpordown', 'IcTradetypeVanillaLongCall', 'IcTradetypeVanillaLongPut', diff --git a/packages/core/src/Constants/contract.js b/packages/core/src/Constants/contract.js index f5873f7c3a9d..de3a6f62b81a 100644 --- a/packages/core/src/Constants/contract.js +++ b/packages/core/src/Constants/contract.js @@ -214,6 +214,16 @@ export const getSupportedContracts = is_high_low => ({ name: localize('No Touch'), position: 'bottom', }, + TURBOSLONG: { + button_name: localize('Long'), + name: 'Turbos', + position: 'top', + }, + TURBOSSHORT: { + button_name: localize('Short'), + name: 'Turbos', + position: 'bottom', + }, RUNHIGH: { name: localize('Only Ups'), position: 'top', diff --git a/packages/core/src/Stores/contract-store.js b/packages/core/src/Stores/contract-store.js index 820289eee5cd..b16b80effdb3 100644 --- a/packages/core/src/Stores/contract-store.js +++ b/packages/core/src/Stores/contract-store.js @@ -1,10 +1,12 @@ import { action, extendObservable, observable, toJS, makeObservable, runInAction } from 'mobx'; import { + isAccumulatorContract, + isDigitContract, isEnded, isEqualObject, - isAccumulatorContract, isMultiplierContract, - isDigitContract, + isOpen, + isTurbosContract, getDigitInfo, getDisplayStatus, WS, @@ -17,7 +19,6 @@ import { getAccuBarriersDefaultTimeout, getAccuBarriersForContractDetails, getEndTime, - isOpen, } from '@deriv/shared'; import { getChartConfig } from './Helpers/logic'; import { setLimitOrderBarriers, getLimitOrder } from './Helpers/limit-orders'; @@ -139,8 +140,9 @@ export default class ContractStore extends BaseStore { const is_multiplier = isMultiplierContract(this.contract_info.contract_type); const is_accumulator = isAccumulatorContract(this.contract_info.contract_type); + const is_turbos = isTurbosContract(this.contract_info.contract_type); - if ((is_accumulator || is_multiplier) && contract_info.contract_id && contract_info.limit_order) { + if ((is_accumulator || is_multiplier || is_turbos) && contract_info.contract_id && contract_info.limit_order) { this.populateContractUpdateConfig(this.contract_info); } } @@ -302,9 +304,11 @@ export default class ContractStore extends BaseStore { } updateLimitOrder() { - const limit_order = isAccumulatorContract(this.contract_info.contract_type) - ? { take_profit: getLimitOrder(this).take_profit } - : getLimitOrder(this); + const limit_order = + isAccumulatorContract(this.contract_info.contract_type) || + isTurbosContract(this.contract_info.contract_type) + ? { take_profit: getLimitOrder(this).take_profit } + : getLimitOrder(this); WS.contractUpdate(this.contract_id, limit_order).then(response => { if (response.error) { diff --git a/packages/core/src/Stores/contract-trade-store.js b/packages/core/src/Stores/contract-trade-store.js index 81b6ffe8512f..2896871f9bbd 100644 --- a/packages/core/src/Stores/contract-trade-store.js +++ b/packages/core/src/Stores/contract-trade-store.js @@ -9,6 +9,7 @@ import { isEnded, isMobile, isMultiplierContract, + isTurbosContract, LocalStore, switch_to_tick_chart, } from '@deriv/shared'; @@ -217,7 +218,11 @@ export default class ContractTradeStore extends BaseStore { if (is_call_put) { // treat CALLE/PUTE and CALL/PUT the same trade_types = ['CALLE', 'PUTE', 'CALL', 'PUT']; + } else if (isTurbosContract(trade_type)) { + //to show both Long and Short recent contracts on DTrader chart + trade_types = ['TURBOSLONG', 'TURBOSSHORT']; } + return this.contracts .filter(c => c.contract_info.underlying === underlying) .filter(c => { diff --git a/packages/core/src/Stores/portfolio-store.js b/packages/core/src/Stores/portfolio-store.js index b04b0ac1779f..bea44e255fbe 100644 --- a/packages/core/src/Stores/portfolio-store.js +++ b/packages/core/src/Stores/portfolio-store.js @@ -18,6 +18,7 @@ import { getDurationUnitText, getEndTime, removeBarrier, + TURBOS, } from '@deriv/shared'; import { Money } from '@deriv/components'; import { ChartBarrierStore } from './chart-barrier-store'; @@ -86,6 +87,7 @@ export default class PortfolioStore extends BaseStore { setContractType: action, is_accumulator: computed, is_multiplier: computed, + is_turbos: computed, }); this.root_store = root_store; @@ -587,4 +589,8 @@ export default class PortfolioStore extends BaseStore { get is_multiplier() { return this.contract_type === 'multiplier'; } + + get is_turbos() { + return this.contract_type === TURBOS.LONG || this.contract_type === TURBOS.SHORT; + } } diff --git a/packages/core/src/Stores/ui-store.js b/packages/core/src/Stores/ui-store.js index f25716243967..36f3eefc2ed9 100644 --- a/packages/core/src/Stores/ui-store.js +++ b/packages/core/src/Stores/ui-store.js @@ -69,9 +69,6 @@ export default class UIStore extends BaseStore { duration_h = 1; duration_d = 1; - // vanilla trade type selection - vanilla_trade_type = 'VANILLALONGCALL'; - // purchase button states purchase_states = [false, false]; @@ -303,7 +300,6 @@ export default class UIStore extends BaseStore { show_positions_toggle: observable, simple_duration_unit: observable, toasts: observable.shallow, - vanilla_trade_type: observable, addToast: action.bound, closeAccountNeededModal: action.bound, closeRealAccountSignup: action.bound, diff --git a/packages/reports/src/Components/market-symbol-icon-row.tsx b/packages/reports/src/Components/market-symbol-icon-row.tsx index 4776b53ee7ac..cd409deafb19 100644 --- a/packages/reports/src/Components/market-symbol-icon-row.tsx +++ b/packages/reports/src/Components/market-symbol-icon-row.tsx @@ -5,39 +5,39 @@ import { getMarketName, getTradeTypeName } from '../Helpers/market-underlying'; import classNames from 'classnames'; type TMarketSymbolIconRow = { + has_full_contract_title?: boolean; icon?: string | null; payload: { shortcode: string; display_name: string; action_type: string; }; - show_description?: boolean; should_show_multiplier?: boolean; should_show_accumulator?: boolean; - is_vanilla?: boolean; }; const MarketSymbolIconRow = ({ + has_full_contract_title, icon, payload, - show_description, should_show_accumulator = true, should_show_multiplier = true, - is_vanilla, }: TMarketSymbolIconRow) => { const should_show_category_icon = typeof payload.shortcode === 'string'; const info_from_shortcode = extractInfoFromShortcode(payload.shortcode); const is_high_low = isHighLow({ shortcode_info: info_from_shortcode }); - - // We need the condition to update the label for vanilla trade type since the label doesn't match with the trade type key unlike other contracts - const category_label = is_vanilla - ? (info_from_shortcode.category as string).replace('Vanillalong', '').charAt(0).toUpperCase() + - (info_from_shortcode.category as string).replace('Vanillalong', '').slice(1) - : info_from_shortcode.category; - + const category_label = getTradeTypeName( + info_from_shortcode.category as string, + is_high_low, + has_full_contract_title + ); if (should_show_category_icon && info_from_shortcode) { return ( -
+
- {show_description && payload.display_name} + {has_full_contract_title && payload.display_name}
@@ -64,7 +64,7 @@ const MarketSymbolIconRow = ({ classNameTarget='category-type-icon__popover' classNameBubble='category-type-icon__popover-bubble' alignment='top' - message={getTradeTypeName(info_from_shortcode.category as string, is_high_low)} + message={category_label} is_bubble_hover_enabled disable_target_icon > @@ -77,15 +77,13 @@ const MarketSymbolIconRow = ({ color='brand' /> - {show_description && category_label} + {has_full_contract_title && category_label}
{should_show_multiplier && info_from_shortcode.multiplier && (
x{info_from_shortcode.multiplier}
)} {should_show_accumulator && info_from_shortcode.growth_rate && ( -
- {(info_from_shortcode.growth_rate as number) * 100}% -
+
{+info_from_shortcode.growth_rate * 100}%
)}
); diff --git a/packages/reports/src/Constants/data-table-constants.tsx b/packages/reports/src/Constants/data-table-constants.tsx index 1577cfb117b9..5bba1b5f59ee 100644 --- a/packages/reports/src/Constants/data-table-constants.tsx +++ b/packages/reports/src/Constants/data-table-constants.tsx @@ -17,7 +17,9 @@ import IndicativeCell from '../Components/indicative-cell'; import MarketSymbolIconRow from '../Components/market-symbol-icon-row'; import ProfitLossCell from '../Components/profit_loss_cell'; import CurrencyWrapper from '../Components/currency-wrapper'; -import { ITransformer } from 'mobx-utils'; +import { useStore } from '@deriv/stores'; + +type TPortfolioStore = ReturnType['portfolio']; const map = { buy: 'success', @@ -35,11 +37,15 @@ export type TKeys = keyof typeof map; const getModeFromValue = (key: TKeys) => map[key] || map.default; -type TMultiplierOpenPositionstemplateProps = { +type TAccumulatorOpenPositionstemplateProps = Omit< + TMultiplierOpenPositionstemplateProps, + 'onClickCancel' | 'server_time' +>; +type TMultiplierOpenPositionstemplateProps = Pick< + TPortfolioStore, + 'getPositionById' | 'onClickCancel' | 'onClickSell' +> & { currency: string; - onClickCancel: () => void; - onClickSell: () => void; - getPositionById: (id: string) => ITransformer; server_time: moment.Moment; }; @@ -179,15 +185,14 @@ export const getOpenPositionsColumnsTemplate = (currency: string) => [ key: 'icon', title: isMobile() ? '' : localize('Type'), col_index: 'type', - renderCellContent: ({ row_obj, is_footer, is_vanilla }: TCellContentProps) => { + renderCellContent: ({ row_obj, is_footer, is_vanilla, is_turbos }: TCellContentProps) => { if (is_footer) return localize('Total'); return ( ); }, @@ -436,7 +441,7 @@ export const getAccumulatorOpenPositionsColumnsTemplate = ({ currency, onClickSell, getPositionById, -}: TMultiplierOpenPositionstemplateProps) => [ +}: TAccumulatorOpenPositionstemplateProps) => [ { title: isMobile() ? '' : localize('Type'), col_index: 'type', @@ -456,7 +461,7 @@ export const getAccumulatorOpenPositionsColumnsTemplate = ({ { title: localize('Growth rate'), col_index: 'growth_rate', - renderCellContent: ({ row_obj }) => + renderCellContent: ({ row_obj }: TCellContentProps) => row_obj.contract_info && row_obj.contract_info.growth_rate ? `${getGrowthRatePercentage(row_obj.contract_info.growth_rate)}%` : '', diff --git a/packages/reports/src/Containers/open-positions.tsx b/packages/reports/src/Containers/open-positions.tsx index ab3bcd969f11..9e117c3a0f63 100644 --- a/packages/reports/src/Containers/open-positions.tsx +++ b/packages/reports/src/Containers/open-positions.tsx @@ -18,15 +18,18 @@ import { isMobile, isMultiplierContract, isVanillaContract, + isTurbosContract, getTimePercentage, getUnsupportedContracts, getTotalProfit, getContractPath, - formatPortfolioPosition, - TContractInfo, getCurrentTick, + getDurationPeriod, + getDurationUnitText, getGrowthRatePercentage, getCardLabels, + toMoment, + TContractStore, } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; import { ReportsTableRowLoader } from '../Components/Elements/ContentLoader'; @@ -41,29 +44,23 @@ import { import PlaceholderComponent from '../Components/placeholder-component'; import { connect } from 'Stores/connect'; import type { TRootStore } from 'Stores/index'; +import { TColIndex } from 'Types'; +import moment from 'moment'; +type TPortfolioStore = TRootStore['portfolio']; +type TDataList = React.ComponentProps; +type TDataListCell = React.ComponentProps; +type TRowRenderer = TDataList['rowRenderer']; +type TMobileRowRenderer = { + row?: TDataList['data_source'][number]; + is_footer?: boolean; + columns_map?: Record; + server_time?: moment.Moment; + onClickCancel: (contract_id?: number) => void; + onClickSell: (contract_id?: number) => void; + measure?: () => void; +}; type TRangeFloatZeroToOne = React.ComponentProps['value']; -type TFormatPortfolioPosition = ReturnType; -type TGetMultiplierOpenPositionsColumnsTemplate = ReturnType; -type TGetOpenPositionsColumnsTemplate = ReturnType; -type TColumnsMap = TGetMultiplierOpenPositionsColumnsTemplate | TGetOpenPositionsColumnsTemplate; -type TColumnsMapElement = TColumnsMap[number]; -type TColIndex = - | 'type' - | 'reference' - | 'currency' - | 'purchase' - | 'payout' - | 'profit' - | 'indicative' - | 'id' - | 'multiplier' - | 'buy_price' - | 'cancellation' - | 'limit_order' - | 'bid_price' - | 'action'; - type TEmptyPlaceholderWrapper = React.PropsWithChildren<{ is_empty: boolean; component_icon: string; @@ -84,40 +81,20 @@ const EmptyPlaceholderWrapper = ({ is_empty, component_icon, children }: TEmptyP ); -type TMobileRowRenderer = { - row: TFormatPortfolioPosition & { is_sell_requested: boolean }; - is_footer: boolean; - columns_map: Record; - server_time: moment.Moment; - onClickCancel: () => void; - onClickSell: () => void; - measure: () => void; -}; - -type TOpenPositionsTable = { +type TOpenPositionsTable = Pick & { className: string; - columns: Record[]; + columns: Record[]; component_icon: string; currency: string; - active_positions: TFormatPortfolioPosition[]; + active_positions: TPortfolioStore['active_positions']; is_loading: boolean; - getRowAction: (row_obj: TRowObj) => - | string - | { - component: JSX.Element; - }; - mobileRowRenderer: (args: TMobileRowRenderer) => JSX.Element; - preloaderCheck: (item: { purchase: number }) => boolean; + mobileRowRenderer: TRowRenderer; + preloaderCheck: (item: TTotals) => boolean; row_size: number; totals: TTotals; is_empty: boolean; }; -type TRowObj = { - is_unsupported: false; - id: number; -}; - type TTotals = { contract_info?: { profit?: number; @@ -126,6 +103,11 @@ type TTotals = { cancellation?: { ask_price?: number; }; + limit_order?: { + take_profit?: { + order_amount?: number | null; + }; + }; }; indicative?: number; purchase?: number; @@ -134,33 +116,35 @@ type TTotals = { }; type TAddToastProps = { - key: string; + key?: string; content: string; - type: string; + timeout?: number; + is_bottom?: boolean; + type?: string; }; -type TOpenPositions = { - active_positions: TFormatPortfolioPosition[]; +type TOpenPositions = Pick< + TPortfolioStore, + | 'active_positions' + | 'error' + | 'getPositionById' + | 'is_loading' + | 'is_multiplier' + | 'onClickCancel' + | 'onClickSell' + | 'onMount' +> & { component_icon: string; currency: string; - error: string; - getPositionById: (id: number) => TFormatPortfolioPosition; - is_eu: boolean; - is_loading: boolean; - is_multiplier: boolean; is_accumulator: boolean; - is_vanilla: boolean; + is_eu: boolean; is_virtual: boolean; NotificationMessages: () => JSX.Element; - onClickCancel: () => void; - onClickSell: () => void; - onMount: () => void; server_time: moment.Moment; addToast: (obj: TAddToastProps) => void; current_focus: string; onClickRemove: () => void; - getContractById: (id: number) => TContractInfo; - is_mobile: boolean; + getContractById: (contract_id?: number) => TContractStore; removeToast: () => void; setCurrentFocus: () => void; should_show_cancellation_warning: boolean; @@ -168,19 +152,35 @@ type TOpenPositions = { toggleUnsupportedContractModal: () => void; }; +type TMobileRowRendererProps = Pick< + TOpenPositions, + | 'addToast' + | 'current_focus' + | 'getContractById' + | 'onClickRemove' + | 'removeToast' + | 'setCurrentFocus' + | 'should_show_cancellation_warning' + | 'toggleCancellationWarning' + | 'toggleUnsupportedContractModal' +> & + Omit & { + columns_map: { [key: TColIndex]: undefined | TDataListCell['column'] }; + }; + const MobileRowRenderer = ({ - row, + row = {}, is_footer, - columns_map, - server_time, + columns_map = {}, + server_time = toMoment(), onClickCancel, onClickSell, measure, ...props -}: TMobileRowRenderer) => { +}: TMobileRowRendererProps) => { React.useEffect(() => { if (!is_footer) { - measure(); + measure?.(); } }, [row.contract_info?.underlying, measure, is_footer]); @@ -205,25 +205,28 @@ const MobileRowRenderer = ({ ); } - const { contract_info, contract_update, type, is_sell_requested } = row; + const { contract_info, contract_update, type, is_sell_requested } = + row as TPortfolioStore['active_positions'][number]; const { currency, status, date_expiry, date_start, tick_count, purchase_time } = contract_info; const current_tick = tick_count ? getCurrentTick(contract_info) : null; - const duration_type = getContractDurationType(contract_info.longcode); + const turbos_duration_unit = tick_count ? 'ticks' : getDurationUnitText(getDurationPeriod(contract_info), true); + const duration_type = getContractDurationType( + isTurbosContract(contract_info.contract_type) ? turbos_duration_unit : contract_info.longcode || '' + ); const progress_value = (getTimePercentage(server_time, date_start ?? 0, date_expiry ?? 0) / 100) as TRangeFloatZeroToOne; - if (isMultiplierContract(type ?? '') || isAccumulatorContract(type ?? '')) { + if (isMultiplierContract(type ?? '') || isAccumulatorContract(type)) { return ( ); @@ -233,10 +236,10 @@ const MobileRowRenderer = ({ <>
- {isVanillaContract(type ?? '') ? ( + {isVanillaContract(type) || (isTurbosContract(type) && !tick_count) ? ( ); -const getRowAction = (row_obj: TRowObj) => +const getRowAction: TDataList['getRowAction'] = row_obj => row_obj.is_unsupported ? { component: ( + ]?.name, }} /> ), } - : getContractPath(row_obj.id); + : getContractPath(row_obj.id || 0); /* * After refactoring transactionHandler for creating positions, * purchase property in contract positions object is somehow NaN or undefined in the first few responses. * So we set it to true in these cases to show a preloader for the data-table-row until the correct value is set. */ -const isPurchaseReceived = (item: { purchase: number }) => isNaN(item.purchase) || !item.purchase; +const isPurchaseReceived: TOpenPositionsTable['preloaderCheck'] = (item: { purchase?: number }) => + isNaN(Number(item.purchase)) || !item.purchase; const getOpenPositionsTotals = ( - active_positions_filtered: TFormatPortfolioPosition[], + active_positions_filtered: TPortfolioStore['active_positions'], is_multiplier_selected: boolean, is_accumulator_selected: boolean ) => { @@ -408,9 +415,9 @@ const getOpenPositionsTotals = ( let profit = 0; active_positions_filtered?.forEach(({ contract_info }) => { - buy_price += +contract_info.buy_price; - bid_price += +contract_info.bid_price; - take_profit += contract_info.limit_order?.take_profit?.order_amount; + buy_price += +(contract_info.buy_price ?? 0); + bid_price += +(contract_info.bid_price ?? 0); + take_profit += contract_info.limit_order?.take_profit?.order_amount ?? 0; if (contract_info) { profit += getTotalProfit(contract_info); } @@ -460,7 +467,6 @@ const OpenPositions = ({ is_eu, is_loading, is_multiplier, - is_vanilla, is_virtual, NotificationMessages, onClickCancel, @@ -491,15 +497,15 @@ const OpenPositions = ({ const accumulators_rates_list = accumulator_rates.map(value => ({ text: value, value })); const active_positions_filtered = active_positions?.filter(({ contract_info }) => { if (contract_info) { - if (is_multiplier_selected) return isMultiplierContract(contract_info.contract_type); + if (is_multiplier_selected) return isMultiplierContract(contract_info.contract_type || ''); if (is_accumulator_selected) return ( isAccumulatorContract(contract_info.contract_type) && - (`${getGrowthRatePercentage(contract_info.growth_rate)}%` === accumulator_rate || + (`${getGrowthRatePercentage(contract_info.growth_rate || 0)}%` === accumulator_rate || !accumulator_rate.includes('%')) ); return ( - !isMultiplierContract(contract_info.contract_type) && + !isMultiplierContract(contract_info.contract_type || '') && !isAccumulatorContract(contract_info.contract_type) ); } @@ -527,7 +533,7 @@ const OpenPositions = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [previous_active_positions]); - const checkForAccuAndMultContracts = (prev_active_positions: TFormatPortfolioPosition[] = []) => { + const checkForAccuAndMultContracts = (prev_active_positions: TPortfolioStore['active_positions'] = []) => { if (active_positions === prev_active_positions) return; if (!has_accumulator_contract) { setHasAccumulatorContract( @@ -536,7 +542,7 @@ const OpenPositions = ({ } if (!has_multiplier_contract) { setHasMultiplierContract( - active_positions.some(({ contract_info }) => isMultiplierContract(contract_info?.contract_type)) + active_positions.some(({ contract_info }) => isMultiplierContract(contract_info?.contract_type || '')) ); } }; @@ -565,12 +571,12 @@ const OpenPositions = ({ const columns = getColumns(); - const columns_map = {} as Record; + const columns_map = {} as Record; columns.forEach(e => { - columns_map[e.col_index] = e; + columns_map[e.col_index] = e as TDataListCell['column']; }); - const mobileRowRenderer = (args: TMobileRowRenderer) => ( + const mobileRowRenderer: TRowRenderer = args => ( setContractTypeValue(e.target.value)} + onChange={(e: React.ChangeEvent & { target: { value: string } }) => + setContractTypeValue(e.target.value) + } /> {is_accumulator_selected && show_accu_in_dropdown && ( setAccumulatorRate(e.target.value)} + onChange={( + e: React.ChangeEvent & { target: { value: string } } + ) => setAccumulatorRate(e.target.value)} /> )}
@@ -703,7 +713,6 @@ export default withRouter( current_focus: ui.current_focus, onClickRemove: portfolio.removePositionById, getContractById: contract_trade.getContractById, - is_mobile: ui.is_mobile, removeToast: ui.removeToast, setCurrentFocus: ui.setCurrentFocus, should_show_cancellation_warning: ui.should_show_cancellation_warning, diff --git a/packages/reports/src/Helpers/market-underlying.ts b/packages/reports/src/Helpers/market-underlying.ts index 318adc5ad0ca..cc958ca544d6 100644 --- a/packages/reports/src/Helpers/market-underlying.ts +++ b/packages/reports/src/Helpers/market-underlying.ts @@ -7,6 +7,7 @@ type TMarketInfo = { }; type TTradeConfig = { + button_name?: JSX.Element; name: JSX.Element; position: string; }; @@ -39,14 +40,16 @@ export const getMarketInformation = (shortcode: string): TMarketInfo => { export const getMarketName = (underlying: string) => underlying ? getMarketNamesMap()[underlying.toUpperCase() as keyof typeof getMarketNamesMap] : null; -export const getTradeTypeName = (category: string, is_high_low = false) => - category - ? (getContractConfig(is_high_low)[category.toUpperCase() as keyof typeof getContractConfig] as TTradeConfig) - .name - : null; +export const getTradeTypeName = (category: string, is_high_low = false, show_button_name = false) => { + const trade_type = + category && + (getContractConfig(is_high_low)[category.toUpperCase() as keyof typeof getContractConfig] as TTradeConfig); + if (!trade_type) return null; + return (show_button_name && trade_type.button_name) || trade_type.name || null; +}; -export const getContractDurationType = (longcode: string, shortcode: string): string => { - if (/^(MULTUP|MULTDOWN)/.test(shortcode)) return ''; +export const getContractDurationType = (longcode: string, shortcode?: string): string => { + if (shortcode && /^(MULTUP|MULTDOWN)/.test(shortcode)) return ''; const duration_pattern = new RegExp('ticks|tick|seconds|minutes|minute|hour|hours'); const extracted = duration_pattern.exec(longcode); diff --git a/packages/reports/src/Types/common-prop.type.ts b/packages/reports/src/Types/common-prop.type.ts index 766908846776..7327d126599c 100644 --- a/packages/reports/src/Types/common-prop.type.ts +++ b/packages/reports/src/Types/common-prop.type.ts @@ -76,6 +76,7 @@ export type TCellContentProps = { passthrough: any; row_obj: any; is_footer: boolean; + is_turbos: boolean; is_vanilla: boolean; }; diff --git a/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss b/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss index 5b7c2732ee03..37970a44b037 100644 --- a/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss +++ b/packages/reports/src/sass/app/_common/components/market-symbol-icon.scss @@ -39,7 +39,7 @@ @include mobile { width: 0.8rem; - &__vanilla { + &__full-title { width: 100%; .market-symbol-icon { &-name { diff --git a/packages/shared/src/utils/constants/barriers.ts b/packages/shared/src/utils/constants/barriers.ts index f12ca799278e..37f4d32ca8c3 100644 --- a/packages/shared/src/utils/constants/barriers.ts +++ b/packages/shared/src/utils/constants/barriers.ts @@ -15,9 +15,11 @@ export const CONTRACT_SHADES = { ASIAND: 'BELOW', MULTUP: 'ABOVE', MULTDOWN: 'BELOW', + TURBOSLONG: 'NONE_SINGLE', + TURBOSSHORT: 'NONE_SINGLE', VANILLALONGCALL: 'NONE_SINGLE', VANILLALONGPUT: 'NONE_SINGLE', -}; +} as const; // Default non-shade according to number of barriers export const DEFAULT_SHADES = { diff --git a/packages/shared/src/utils/constants/contract.tsx b/packages/shared/src/utils/constants/contract.tsx index 1ad120d30033..f54b3fe187e2 100644 --- a/packages/shared/src/utils/constants/contract.tsx +++ b/packages/shared/src/utils/constants/contract.tsx @@ -1,12 +1,14 @@ import React from 'react'; import { localize, Localize } from '@deriv/translations'; -import { shouldShowCancellation, shouldShowExpiration } from '../contract'; +import { shouldShowCancellation, shouldShowExpiration, TURBOS } from '../contract'; export const getLocalizedBasis = () => ({ accumulator: localize('Accumulator'), payout: localize('Payout'), + payout_per_point: localize('Payout per point'), stake: localize('Stake'), multiplier: localize('Multiplier'), + turbos: localize('Turbos'), }); /** @@ -24,13 +26,7 @@ type TContractTypesConfig = { type TGetContractTypesConfig = (symbol: string) => Record; -type TContractConfig = { - button_name?: React.ReactNode; - name: React.ReactNode; - position: string; -}; - -export type TGetSupportedContracts = keyof ReturnType; +type TGetSupportedContracts = keyof ReturnType; export const getContractTypesConfig: TGetContractTypesConfig = symbol => ({ rise_fall: { @@ -144,11 +140,25 @@ export const getContractTypesConfig: TGetContractTypesConfig = symbol => ({ ], config: { hide_duration: true }, }, // hide Duration for Multiplier contracts for now + turboslong: { + title: localize('Long/Short'), + trade_types: ['TURBOSLONG'], + basis: ['stake'], + barrier_count: 1, + components: ['trade_type_tabs', 'barrier_selector', 'take_profit'], + }, + turbosshort: { + title: localize('Long/Short'), + trade_types: ['TURBOSSHORT'], + basis: ['stake'], + barrier_count: 1, + components: ['trade_type_tabs', 'barrier_selector', 'take_profit'], + }, vanilla: { title: localize('Call/Put'), trade_types: ['VANILLALONGCALL', 'VANILLALONGPUT'], basis: ['stake'], - components: ['duration', 'strike', 'amount', 'vanilla_trade_type'], + components: ['duration', 'strike', 'amount', 'trade_type_tabs'], barrier_count: 1, config: { should_override: true }, }, @@ -156,6 +166,7 @@ export const getContractTypesConfig: TGetContractTypesConfig = symbol => ({ // Config for rendering trade options export const getContractCategoriesConfig = () => ({ + Turbos: { name: localize('Turbos'), categories: [TURBOS.LONG, TURBOS.SHORT] }, Multipliers: { name: localize('Multipliers'), categories: ['multiplier'] }, 'Ups & Downs': { name: localize('Ups & Downs'), @@ -185,6 +196,7 @@ export const unsupported_contract_types_list = [ export const getCardLabels = () => ({ APPLY: localize('Apply'), + BARRIER: localize('Barrier:'), BUY_PRICE: localize('Buy price:'), CANCEL: localize('Cancel'), CLOSE: localize('Close'), @@ -318,148 +330,160 @@ export const getMarketNamesMap = () => ({ CRYLTCUSD: localize('LTC/USD'), }); -export const getUnsupportedContracts = () => ({ - EXPIRYMISS: { - name: localize('Ends Outside'), - position: 'top', - }, - EXPIRYRANGE: { - name: localize('Ends Between'), - position: 'bottom', - }, - RANGE: { - name: localize('Stays Between'), - position: 'top', - }, - UPORDOWN: { - name: localize('Goes Outside'), - position: 'bottom', - }, - RESETCALL: { - name: localize('Reset Call'), - position: 'top', - }, - RESETPUT: { - name: localize('Reset Put'), - position: 'bottom', - }, - TICKHIGH: { - name: localize('High Tick'), - position: 'top', - }, - TICKLOW: { - name: localize('Low Tick'), - position: 'bottom', - }, - ASIANU: { - name: localize('Asian Up'), - position: 'top', - }, - ASIAND: { - name: localize('Asian Down'), - position: 'bottom', - }, - LBFLOATCALL: { - name: localize('Close-to-Low'), - position: 'top', - }, - LBFLOATPUT: { - name: localize('High-to-Close'), - position: 'top', - }, - LBHIGHLOW: { - name: localize('High-to-Low'), - position: 'top', - }, - CALLSPREAD: { - name: localize('Spread Up'), - position: 'top', - }, - PUTSPREAD: { - name: localize('Spread Down'), - position: 'bottom', - }, -}); +export const getUnsupportedContracts = () => + ({ + EXPIRYMISS: { + name: localize('Ends Outside'), + position: 'top', + }, + EXPIRYRANGE: { + name: localize('Ends Between'), + position: 'bottom', + }, + RANGE: { + name: localize('Stays Between'), + position: 'top', + }, + UPORDOWN: { + name: localize('Goes Outside'), + position: 'bottom', + }, + RESETCALL: { + name: localize('Reset Call'), + position: 'top', + }, + RESETPUT: { + name: localize('Reset Put'), + position: 'bottom', + }, + TICKHIGH: { + name: localize('High Tick'), + position: 'top', + }, + TICKLOW: { + name: localize('Low Tick'), + position: 'bottom', + }, + ASIANU: { + name: localize('Asian Up'), + position: 'top', + }, + ASIAND: { + name: localize('Asian Down'), + position: 'bottom', + }, + LBFLOATCALL: { + name: localize('Close-to-Low'), + position: 'top', + }, + LBFLOATPUT: { + name: localize('High-to-Close'), + position: 'top', + }, + LBHIGHLOW: { + name: localize('High-to-Low'), + position: 'top', + }, + CALLSPREAD: { + name: localize('Spread Up'), + position: 'top', + }, + PUTSPREAD: { + name: localize('Spread Down'), + position: 'bottom', + }, + } as const); -export const getSupportedContracts = (is_high_low?: boolean) => ({ - ACCU: { - button_name: , - name: , - position: 'top', - }, - CALL: { - name: is_high_low ? : , - position: 'top', - }, - PUT: { - name: is_high_low ? : , - position: 'bottom', - }, - CALLE: { - name: , - position: 'top', - }, - PUTE: { - name: , - position: 'bottom', - }, - DIGITMATCH: { - name: , - position: 'top', - }, - DIGITDIFF: { - name: , - position: 'bottom', - }, - DIGITEVEN: { - name: , - position: 'top', - }, - DIGITODD: { - name: , - position: 'bottom', - }, - DIGITOVER: { - name: , - position: 'top', - }, - DIGITUNDER: { - name: , - position: 'bottom', - }, - ONETOUCH: { - name: , - position: 'top', - }, - NOTOUCH: { - name: , - position: 'bottom', - }, - MULTUP: { - name: , - position: 'top', - }, - MULTDOWN: { - name: , - position: 'bottom', - }, - VANILLALONGCALL: { - name: , - position: 'top', - }, - VANILLALONGPUT: { - name: , - position: 'bottom', - }, - RUNHIGH: { - name: localize('Only Ups'), - position: 'top', - }, - RUNLOW: { - name: localize('Only Downs'), - position: 'bottom', - }, -}); +export const getSupportedContracts = (is_high_low?: boolean) => + ({ + ACCU: { + button_name: , + name: , + position: 'top', + }, + CALL: { + name: is_high_low ? : , + position: 'top', + }, + PUT: { + name: is_high_low ? : , + position: 'bottom', + }, + CALLE: { + name: , + position: 'top', + }, + PUTE: { + name: , + position: 'bottom', + }, + DIGITMATCH: { + name: , + position: 'top', + }, + DIGITDIFF: { + name: , + position: 'bottom', + }, + DIGITEVEN: { + name: , + position: 'top', + }, + DIGITODD: { + name: , + position: 'bottom', + }, + DIGITOVER: { + name: , + position: 'top', + }, + DIGITUNDER: { + name: , + position: 'bottom', + }, + ONETOUCH: { + name: , + position: 'top', + }, + NOTOUCH: { + name: , + position: 'bottom', + }, + MULTUP: { + name: , + position: 'top', + }, + MULTDOWN: { + name: , + position: 'bottom', + }, + TURBOSLONG: { + name: , + button_name: , + position: 'top', + }, + TURBOSSHORT: { + name: , + button_name: , + position: 'bottom', + }, + VANILLALONGCALL: { + name: , + position: 'top', + }, + VANILLALONGPUT: { + name: , + position: 'bottom', + }, + RUNHIGH: { + name: , + position: 'top', + }, + RUNLOW: { + name: , + position: 'bottom', + }, + } as const); export const getContractConfig = (is_high_low?: boolean) => ({ ...getSupportedContracts(is_high_low), @@ -475,8 +499,9 @@ export const getContractTypeDisplay = ( is_high_low = false, show_button_name = false ) => { - const contract_config = getContractConfig(is_high_low)[type as TGetSupportedContracts] as TContractConfig; - return (show_button_name && contract_config.button_name) || contract_config.name || ''; + const contract_config = getContractConfig(is_high_low)[type as TGetSupportedContracts]; + if (show_button_name && 'button_name' in contract_config) return contract_config.button_name; + return contract_config.name || ''; }; export const getContractTypePosition = (type: TGetSupportedContracts, is_high_low = false) => diff --git a/packages/shared/src/utils/contract/__tests__/contract.spec.ts b/packages/shared/src/utils/contract/__tests__/contract.spec.ts index 37883fab4a56..b2fd4147641d 100644 --- a/packages/shared/src/utils/contract/__tests__/contract.spec.ts +++ b/packages/shared/src/utils/contract/__tests__/contract.spec.ts @@ -193,6 +193,15 @@ describe('isDigitContract', () => { }); }); +describe('isTurbosContract', () => { + it('should return true if contract_type includes TURBOS', () => { + expect(ContractUtils.isTurbosContract('TURBOS')).toEqual(true); + }); + it('should return false if contract_type does not include TURBOS', () => { + expect(ContractUtils.isTurbosContract('CALL')).toEqual(false); + }); +}); + describe('getDigitInfo', () => { it('should return an empty object when tick_stream is not in contract_info', () => { const contract_info: TContractInfo = {}; diff --git a/packages/shared/src/utils/contract/contract-types.ts b/packages/shared/src/utils/contract/contract-types.ts index 55678cd3f933..0442d8ffeeb6 100644 --- a/packages/shared/src/utils/contract/contract-types.ts +++ b/packages/shared/src/utils/contract/contract-types.ts @@ -1,7 +1,7 @@ -import { ContractUpdate, ProposalOpenContract } from '@deriv/api-types'; +import { ContractUpdate, Portfolio1, ProposalOpenContract } from '@deriv/api-types'; export type TContractStore = { - contract_info: ProposalOpenContract; + contract_info: TContractInfo; contract_update_take_profit: number | string; contract_update_stop_loss: number | string; clearContractUpdateConfigValues: () => void; @@ -12,9 +12,10 @@ export type TContractStore = { onChange: (param: { name: string; value: string | number | boolean }) => void; }; -export type TContractInfo = ProposalOpenContract & { - contract_update?: ContractUpdate; -}; +export type TContractInfo = ProposalOpenContract & + Portfolio1 & { + contract_update?: ContractUpdate; + }; export type TTickItem = { epoch?: number; diff --git a/packages/shared/src/utils/contract/contract.ts b/packages/shared/src/utils/contract/contract.ts index 9617f4b02466..f98bb9fd5f33 100644 --- a/packages/shared/src/utils/contract/contract.ts +++ b/packages/shared/src/utils/contract/contract.ts @@ -1,6 +1,7 @@ import moment from 'moment'; import { unique } from '../object'; -import { TContractInfo, TLimitOrder, TDigitsInfo, TTickItem } from './contract-types'; +import { capitalizeFirstLetter } from '../string/string_util'; +import { TContractInfo, TDigitsInfo, TLimitOrder, TTickItem } from './contract-types'; type TGetAccuBarriersDTraderTimeout = (params: { barriers_update_timestamp: number; @@ -13,6 +14,10 @@ type TGetAccuBarriersDTraderTimeout = (params: { export const DELAY_TIME_1S_SYMBOL = 500; // generation_interval will be provided via API later to help us distinguish between 1-second and 2-second symbols export const symbols_2s = ['R_10', 'R_25', 'R_50', 'R_75', 'R_100']; +export const TURBOS = { + LONG: 'turboslong', + SHORT: 'turbosshort', +} as const; export const getContractStatus = ({ contract_type, exit_tick_time, profit, status }: TContractInfo) => { const closed_contract_status = profit && profit < 0 && exit_tick_time ? 'lost' : 'won'; @@ -47,7 +52,7 @@ export const isUserSold = (contract_info: TContractInfo) => contract_info.status export const isValidToCancel = (contract_info: TContractInfo) => !!contract_info.is_valid_to_cancel; export const isValidToSell = (contract_info: TContractInfo) => - !isEnded(contract_info) && !isUserSold(contract_info) && Number(contract_info.is_valid_to_sell) === 1; + !isEnded(contract_info) && !isUserSold(contract_info) && !!contract_info.is_valid_to_sell; export const hasContractEntered = (contract_info: TContractInfo) => !!contract_info.entry_spot; @@ -59,6 +64,8 @@ export const isAccumulatorContractOpen = (contract_info: TContractInfo = {}) => export const isMultiplierContract = (contract_type = '') => /MULT/i.test(contract_type); +export const isTurbosContract = (contract_type = '') => /TURBOS/i.test(contract_type); + export const isVanillaContract = (contract_type = '') => /VANILLA/i.test(contract_type); export const isOnlyUpsDownsContract = (contract_type = '') => /RUN/i.test(contract_type); @@ -197,3 +204,8 @@ export const getContractUpdateConfig = ({ contract_update, limit_order }: TContr export const shouldShowExpiration = (symbol = '') => symbol.startsWith('cry'); export const shouldShowCancellation = (symbol = '') => !/^(cry|CRASH|BOOM|stpRNG|WLD|JD)/.test(symbol); + +export const getContractSubtype = (type: string) => + /(VANILLALONG|TURBOS)/i.test(type) + ? capitalizeFirstLetter(type.replace(/(VANILLALONG|TURBOS)/i, '').toLowerCase()) + : ''; diff --git a/packages/shared/src/utils/helpers/details.ts b/packages/shared/src/utils/helpers/details.ts index 067e191f42fc..2c6e960beb43 100644 --- a/packages/shared/src/utils/helpers/details.ts +++ b/packages/shared/src/utils/helpers/details.ts @@ -1,13 +1,7 @@ import { epochToMoment, formatMilliseconds, getDiffDuration } from '../date'; import { localize } from '@deriv/translations'; import moment from 'moment'; - -type TGetDurationPeriod = { - date_start: number; - purchase_time: number; - date_expiry: number; - tick_count?: number; -}; +import { TContractInfo } from '../contract'; export const getDurationUnitValue = (obj_duration: moment.Duration) => { const duration_ms = obj_duration.asMilliseconds() / 1000; @@ -48,33 +42,41 @@ export const getUnitMap = () => { }; }; -export const getDurationUnitText = (obj_duration: moment.Duration) => { +const TIME = { + SECOND: 1000, + MINUTE: 60000, + HOUR: 3600000, + DAY: 86400000, +} as const; + +export const getDurationUnitText = (obj_duration: moment.Duration, should_ignore_end_time?: boolean) => { const unit_map = getUnitMap(); - const duration_ms = obj_duration.asMilliseconds() / 1000; + const duration_ms = obj_duration.asMilliseconds() / TIME.SECOND; // return empty suffix string if duration is End Time set except for days and seconds, refer to L18 and L19 - if (duration_ms) { - if (duration_ms >= 86400000) { - const days_value = duration_ms / 86400000; - return days_value <= 2 ? unit_map.d.name_singular : unit_map.d.name_plural; - } else if (duration_ms >= 3600000 && duration_ms < 86400000) { - if (isEndTime(duration_ms / (1000 * 60 * 60))) return ''; - return duration_ms === 3600000 ? unit_map.h.name_singular : unit_map.h.name_plural; - } else if (duration_ms >= 60000 && duration_ms < 3600000) { - if (isEndTime(duration_ms / (1000 * 60))) return ''; - return duration_ms === 60000 ? unit_map.m.name_singular : unit_map.m.name_plural; - } else if (duration_ms >= 1000 && duration_ms < 60000) { - return unit_map.s.name; - } + if (duration_ms >= TIME.DAY) { + const days_value = duration_ms / TIME.DAY; + return days_value <= 2 ? unit_map.d.name_singular : unit_map.d.name_plural; + } + if (duration_ms >= TIME.HOUR && duration_ms < TIME.DAY) { + if (!should_ignore_end_time && isEndTime(duration_ms / TIME.HOUR)) return ''; + return duration_ms === TIME.HOUR ? unit_map.h.name_singular : unit_map.h.name_plural; + } + if (duration_ms >= TIME.MINUTE && duration_ms < TIME.HOUR) { + if (!should_ignore_end_time && isEndTime(duration_ms / TIME.MINUTE)) return ''; + return duration_ms === TIME.MINUTE ? unit_map.m.name_singular : unit_map.m.name_plural; + } + if (duration_ms >= TIME.SECOND && duration_ms < TIME.MINUTE) { + return unit_map.s.name; } return unit_map.s.name; }; -export const getDurationPeriod = (contract_info: TGetDurationPeriod) => +export const getDurationPeriod = (contract_info: TContractInfo) => getDiffDuration( - +epochToMoment(contract_info.date_start || contract_info.purchase_time), - +epochToMoment(contract_info.date_expiry) + +epochToMoment(contract_info.date_start || contract_info.purchase_time || 0), + +epochToMoment(contract_info.date_expiry || 0) ); -export const getDurationTime = (contract_info: TGetDurationPeriod) => +export const getDurationTime = (contract_info: TContractInfo) => contract_info.tick_count ? contract_info.tick_count : getDurationUnitValue(getDurationPeriod(contract_info)); diff --git a/packages/shared/src/utils/helpers/format-response.ts b/packages/shared/src/utils/helpers/format-response.ts index 7f111cd1c063..9f46174f556e 100644 --- a/packages/shared/src/utils/helpers/format-response.ts +++ b/packages/shared/src/utils/helpers/format-response.ts @@ -2,24 +2,7 @@ import { GetSettings, ResidenceList } from '@deriv/api-types'; import { getUnsupportedContracts } from '../constants'; import { getSymbolDisplayName, TActiveSymbols } from './active-symbols'; import { getMarketInformation } from './market-underlying'; - -type TPortfolioPos = { - buy_price: number; - contract_id?: number; - contract_type?: string; - longcode: string; - payout: number; - shortcode: string; - transaction_id?: number; - transaction_ids?: { - buy: number; - sell: number; - }; - limit_order?: { - stop_loss?: null | number; - take_profit?: null | number; - }; -}; +import { TContractInfo } from '../contract'; type TIsUnSupportedContract = { contract_type?: string; @@ -31,19 +14,22 @@ const isUnSupportedContract = (portfolio_pos: TIsUnSupportedContract) => !!portfolio_pos.is_forward_starting; // for forward start contracts export const formatPortfolioPosition = ( - portfolio_pos: TPortfolioPos, + portfolio_pos: TContractInfo, active_symbols: TActiveSymbols = [], indicative?: number ) => { const purchase = portfolio_pos.buy_price; const payout = portfolio_pos.payout; - const display_name = getSymbolDisplayName(active_symbols, getMarketInformation(portfolio_pos.shortcode).underlying); + const display_name = getSymbolDisplayName( + active_symbols, + getMarketInformation(portfolio_pos.shortcode || '').underlying + ); const transaction_id = portfolio_pos.transaction_id || (portfolio_pos.transaction_ids && portfolio_pos.transaction_ids.buy); return { contract_info: portfolio_pos, - details: portfolio_pos.longcode.replace(/\n/g, '
'), + details: portfolio_pos.longcode?.replace(/\n/g, '
'), display_name, id: portfolio_pos.contract_id, indicative: (indicative && isNaN(indicative)) || !indicative ? 0 : indicative, diff --git a/packages/shared/src/utils/helpers/logic.ts b/packages/shared/src/utils/helpers/logic.ts index ed730893fac6..691cef6f72b8 100644 --- a/packages/shared/src/utils/helpers/logic.ts +++ b/packages/shared/src/utils/helpers/logic.ts @@ -24,7 +24,18 @@ type TIsSoldBeforeStart = Required>; -type TGetEndTime = Pick & +type TGetEndTime = Pick< + TContractInfo, + | 'is_expired' + | 'sell_time' + | 'status' + | 'tick_count' + | 'bid_price' + | 'buy_price' + | 'contract_id' + | 'is_valid_to_sell' + | 'profit' +> & Required>; export const isContractElapsed = (contract_info: TGetEndTime, tick: TTick) => { @@ -50,7 +61,7 @@ export const isStarted = (contract_info: TIsStarted) => export const isUserCancelled = (contract_info: TContractInfo) => contract_info.status === 'cancelled'; -export const getEndTime = (contract_info: TGetEndTime) => { +export const getEndTime = (contract_info: TContractInfo) => { const { contract_type, exit_tick_time, @@ -66,12 +77,12 @@ export const getEndTime = (contract_info: TGetEndTime) => { if (!is_finished && !isUserSold(contract_info) && !isUserCancelled(contract_info)) return undefined; if (isUserSold(contract_info) && sell_time) { - return sell_time > date_expiry ? date_expiry : sell_time; - } else if (!is_tick_contract && sell_time && sell_time > date_expiry) { + return sell_time > Number(date_expiry) ? date_expiry : sell_time; + } else if (!is_tick_contract && sell_time && sell_time > Number(date_expiry)) { return date_expiry; } - return date_expiry > exit_tick_time && !+is_path_dependent ? date_expiry : exit_tick_time; + return Number(date_expiry) > Number(exit_tick_time) && !Number(is_path_dependent) ? date_expiry : exit_tick_time; }; export const getBuyPrice = (contract_store: TContractStore) => { diff --git a/packages/shared/src/utils/shortcode/shortcode.ts b/packages/shared/src/utils/shortcode/shortcode.ts index 9dc1eaa88539..6517450d2bd0 100644 --- a/packages/shared/src/utils/shortcode/shortcode.ts +++ b/packages/shared/src/utils/shortcode/shortcode.ts @@ -59,6 +59,7 @@ export const extractInfoFromShortcode = (shortcode: string): TInfoFromShortcode pattern = multipliers_regex; } else pattern = is_accumulators ? accumulators_regex : options_regex; const extracted = pattern.exec(shortcode); + if (extracted !== null) { info_from_shortcode.category = extracted[1].charAt(0).toUpperCase() + extracted[1].slice(1).toLowerCase(); info_from_shortcode.underlying = extracted[2]; diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index ae50f2cc5a77..39d665fe30ec 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -411,23 +411,14 @@ const mock = (): TStores & { is_mock: boolean } => { setP2PRedirectTo: jest.fn(), }, portfolio: { + positions: [], active_positions: [], - error: { - header: '', - message: '', - type: '', - redirect_label: '', - redirect_to: '', - should_clear_error_on_click: false, - should_show_refresh: false, - redirectOnClick: jest.fn(), - setError: jest.fn(), - app_routing_history: [], - }, + error: '', getPositionById: jest.fn(), is_loading: false, is_accumulator: false, is_multiplier: false, + is_turbos: false, onClickCancel: jest.fn(), onClickSell: jest.fn(), onMount: jest.fn(), diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 1a7b31d706ea..57e6aac82a72 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -1,12 +1,14 @@ import type { AccountLimitsResponse, Authorize, + ContractUpdate, DetailsOfEachMT5Loginid, GetAccountStatus, GetLimits, GetSettings, LandingCompany, LogOutResponse, + Portfolio1, ProposalOpenContract, ResidenceList, SetFinancialAssessmentRequest, @@ -81,6 +83,25 @@ type TPopulateSettingsExtensionsMenuItem = { value: (props: T) => JSX.Element; }; +type TPortfolioPosition = { + contract_info: ProposalOpenContract & + Portfolio1 & { + contract_update?: ContractUpdate; + }; + details?: string; + display_name: string; + id?: number; + indicative: number; + payout?: number; + purchase?: number; + reference: number; + type?: string; + is_unsupported: boolean; + contract_update: ProposalOpenContract['limit_order']; + is_sell_requested: boolean; + profit_loss: number; +}; + type TAppRoutingHistory = { action: string; hash: string; @@ -486,15 +507,17 @@ type TUiStore = { }; type TPortfolioStore = { - active_positions: ProposalOpenContract[]; - error: TCommonStoreError; - getPositionById: (id: number) => ProposalOpenContract; + active_positions: TPortfolioPosition[]; + error: string; + getPositionById: (id: number) => TPortfolioPosition; + is_accumulator: boolean; is_loading: boolean; is_multiplier: boolean; - is_accumulator: boolean; - onClickCancel: (contract_id: number) => void; - onClickSell: (contract_id: number) => void; + is_turbos: boolean; + onClickCancel: (contract_id?: number) => void; + onClickSell: (contract_id?: number) => void; onMount: () => void; + positions: TPortfolioPosition[]; removePositionById: (id: number) => void; }; diff --git a/packages/trader/package.json b/packages/trader/package.json index 612d662a0b07..ea9e649d4930 100644 --- a/packages/trader/package.json +++ b/packages/trader/package.json @@ -35,6 +35,10 @@ "devDependencies": { "@babel/eslint-parser": "^7.17.0", "@babel/preset-react": "^7.16.7", + "@testing-library/jest-dom": "^5.12.0", + "@testing-library/react": "^12.0.0", + "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.5.0", "@types/react": "^18.0.7", "@types/react-dom": "^18.0.0", "babel-loader": "^8.1.0", diff --git a/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit.jsx b/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit.jsx index 42c4dd8538d8..b738ec9160de 100644 --- a/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit.jsx +++ b/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit.jsx @@ -12,6 +12,7 @@ const ContractAudit = ({ is_accumulator, is_multiplier, is_only_ups_downs, + is_turbos, toggleHistoryTab, ...props }) => { @@ -36,7 +37,7 @@ const ContractAudit = ({ if (!has_result) return null; - if (!is_multiplier && !is_accumulator) { + if (!is_multiplier && !is_accumulator && !is_turbos) { return (
@@ -64,6 +65,7 @@ ContractAudit.propTypes = { is_accumulator: PropTypes.bool, is_multiplier: PropTypes.bool, is_only_ups_downs: PropTypes.bool, + is_turbos: PropTypes.bool, toggleHistoryTab: PropTypes.func, }; diff --git a/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx b/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx index 6b77ff535791..ee0ef9a820d4 100644 --- a/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx +++ b/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx @@ -10,6 +10,7 @@ import { isMobile, isMultiplierContract, isOnlyUpsDownsContract, + isTurbosContract, isUserSold, isEndedBeforeCancellationExpired, isUserCancelled, @@ -42,6 +43,9 @@ const ContractDetails = ({ contract_end_time, contract_info, duration, duration_ const is_profit = profit >= 0; const cancellation_price = getCancellationPrice(contract_info); + const show_barrier = !is_vanilla && !isAccumulatorContract(contract_type) && !isOnlyUpsDownsContract(contract_type); + const show_duration = !isAccumulatorContract(contract_type) || !isNaN(contract_end_time); + const show_payout_per_point = isTurbosContract(contract_type) || is_vanilla; const ticks_duration_text = isAccumulatorContract(contract_type) ? `${tick_passed}/${tick_count} ${localize('ticks')}` : `${tick_count} ${tick_count < 2 ? localize('tick') : localize('ticks')}`; @@ -83,7 +87,7 @@ const ContractDetails = ({ contract_end_time, contract_info, duration, duration_ ) : ( - {(!isAccumulatorContract(contract_type) || !isNaN(contract_end_time)) && ( + {show_duration && ( } @@ -92,40 +96,35 @@ const ContractDetails = ({ contract_end_time, contract_info, duration, duration_ /> )} {is_vanilla && ( - - } - label={getBarrierLabel(contract_info)} - value={getBarrierValue(contract_info) || ' - '} - /> - } - label={localize('Payout per point')} - value={ - `${display_number_of_contracts} ${getCurrencyDisplayCode(currency)}` || ' - ' - } - should_format={!is_vanilla} - /> - + } + label={getBarrierLabel(contract_info)} + value={getBarrierValue(contract_info) || ' - '} + /> + )} + {show_barrier && ( + + ) : ( + + ) + } + label={getBarrierLabel(contract_info)} + value={getBarrierValue(contract_info) || ' - '} + /> + )} + {show_payout_per_point && ( + } + label={localize('Payout per point')} + value={`${display_number_of_contracts} ${getCurrencyDisplayCode(currency)}` || ' - '} + /> )} - {!isAccumulatorContract(contract_type) && - !is_vanilla && - !isOnlyUpsDownsContract(contract_type) && ( - - ) : ( - - ) - } - label={getBarrierLabel(contract_info)} - value={getBarrierValue(contract_info) || ' - '} - /> - )} )} ); - const has_swipeable_drawer = is_sold || is_multiplier || is_accumulator || is_vanilla; + const has_swipeable_drawer = is_sold || is_multiplier || is_accumulator || is_turbos || is_vanilla; return ( @@ -169,6 +171,7 @@ ContractDrawerCard.propTypes = { currency: PropTypes.string, is_accumulator: PropTypes.bool, is_collapsed: PropTypes.bool, + is_turbos: PropTypes.bool, onClickCancel: PropTypes.func, onClickSell: PropTypes.func, }; diff --git a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx index f618d3f48677..a1a4accd8567 100644 --- a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx +++ b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer.jsx @@ -28,6 +28,7 @@ const ContractDrawer = observer( is_dark_theme, is_market_closed, is_multiplier, + is_turbos, is_vanilla, is_only_ups_downs, onClickCancel, @@ -42,63 +43,10 @@ const ContractDrawer = observer( const contract_drawer_ref = React.useRef(); const contract_drawer_card_ref = React.useRef(); const [should_show_contract_audit, setShouldShowContractAudit] = React.useState(false); - - const getBodyContent = () => { - const exit_spot = - isUserSold(contract_info) && !is_multiplier && !is_accumulator ? '-' : exit_tick_display_value; - - const contract_audit = ( - - ); - - return ( - - setShouldShowContractAudit(true)} - onSwipedDown={() => setShouldShowContractAudit(false)} - server_time={server_time} - status={status} - toggleContractAuditDrawer={() => setShouldShowContractAudit(!should_show_contract_audit)} - /> - {contract_audit} - - ); - }; - - if (!contract_info) return null; - - // For non-binary contract, the status is always null, so we check for is_expired in contract_info - const fallback_result = contract_info.status || contract_info.is_expired; - const exit_spot = - isUserSold(contract_info) && !is_multiplier && !is_accumulator ? '-' : exit_tick_display_value; + isUserSold(contract_info) && !is_accumulator && !is_multiplier && !is_turbos + ? '-' + : exit_tick_display_value; const contract_audit = ( ); + if (!contract_info) return null; + + // For non-binary contract, the status is always null, so we check for is_expired in contract_info + const fallback_result = contract_info.status || contract_info.is_expired; + const body_content = fallback_result ? ( - getBodyContent() + + setShouldShowContractAudit(true)} + onSwipedDown={() => setShouldShowContractAudit(false)} + server_time={server_time} + status={status} + toggleContractAuditDrawer={() => setShouldShowContractAudit(!should_show_contract_audit)} + /> + {contract_audit} + ) : (
@@ -134,7 +112,7 @@ const ContractDrawer = observer( className={classNames('contract-drawer', { 'contract-drawer--with-collapsible-btn': !!getEndTime(contract_info) || - ((is_multiplier || is_vanilla || is_accumulator) && isMobile()), + ((is_accumulator || is_multiplier || is_turbos || is_vanilla) && isMobile()), 'contract-drawer--is-multiplier': is_multiplier && isMobile(), 'contract-drawer--is-multiplier-sold': is_multiplier && isMobile() && getEndTime(contract_info), })} @@ -185,8 +163,9 @@ const ContractDrawer = observer( ContractDrawer.propTypes = { is_accumulator: PropTypes.bool, is_multiplier: PropTypes.bool, - is_vanilla: PropTypes.bool, is_only_ups_downs: PropTypes.bool, + is_turbos: PropTypes.bool, + is_vanilla: PropTypes.bool, toggleHistoryTab: PropTypes.func, }; diff --git a/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx b/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx index 669b36df7498..b4ed3696cc75 100644 --- a/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx +++ b/packages/trader/src/App/Components/Elements/PositionsDrawer/positions-drawer.jsx @@ -4,7 +4,7 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import { CSSTransition } from 'react-transition-group'; import { Icon, DataList, Text, PositionsDrawerCard } from '@deriv/components'; -import { routes, useNewRowTransition } from '@deriv/shared'; +import { routes, useNewRowTransition, TURBOS } from '@deriv/shared'; import { localize } from '@deriv/translations'; import EmptyPortfolioMessage from '../EmptyPortfolioMessage'; import { filterByContractType } from './helpers'; @@ -100,9 +100,11 @@ const PositionsDrawer = observer(({ ...props }) => { p => p.contract_info && symbol === p.contract_info.underlying && - filterByContractType(p.contract_info, trade_contract_type) + (trade_contract_type.includes('turbos') + ? filterByContractType(p.contract_info, TURBOS.SHORT) || + filterByContractType(p.contract_info, TURBOS.LONG) + : filterByContractType(p.contract_info, trade_contract_type)) ); - const body_content = ( diff --git a/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx b/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx index ceaa48693b70..692620e0d419 100644 --- a/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx +++ b/packages/trader/src/App/Components/Elements/TogglePositions/toggle-positions-mobile.jsx @@ -6,30 +6,25 @@ import { localize } from '@deriv/translations'; import { NavLink } from 'react-router-dom'; import EmptyPortfolioMessage from '../EmptyPortfolioMessage'; import PositionsModalCard from 'App/Components/Elements/PositionsDrawer/positions-modal-card.jsx'; -import { filterByContractType } from 'App/Components/Elements/PositionsDrawer/helpers'; import TogglePositions from './toggle-positions.jsx'; -import { useTraderStore } from 'Stores/useTraderStores'; import { observer, useStore } from '@deriv/stores'; const TogglePositionsMobile = observer( ({ active_positions_count, - all_positions, currency, disableApp, enableApp, error, + filtered_positions, is_empty, onClickSell, onClickCancel, toggleUnsupportedContractModal, }) => { const { portfolio, ui } = useStore(); - const { symbol, contract_type: trade_contract_type } = useTraderStore(); const { removePositionById: onClickRemove } = portfolio; const { togglePositionsDrawer, is_positions_drawer_on } = ui; - let filtered_positions = []; - const closeModal = () => { filtered_positions.slice(0, 5).map(position => { const { contract_info } = position; @@ -40,13 +35,6 @@ const TogglePositionsMobile = observer( togglePositionsDrawer(); }; - filtered_positions = all_positions.filter( - p => - p.contract_info && - symbol === p.contract_info.underlying && - filterByContractType(p.contract_info, trade_contract_type) - ); - // Show only 5 most recent open contracts const body_content = ( diff --git a/packages/trader/src/App/Components/Form/fieldset.jsx b/packages/trader/src/App/Components/Form/fieldset.jsx index 4556480a419d..254aa52c2ea8 100644 --- a/packages/trader/src/App/Components/Form/fieldset.jsx +++ b/packages/trader/src/App/Components/Form/fieldset.jsx @@ -58,7 +58,7 @@ Fieldset.propTypes = { children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), className: PropTypes.string, header: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - header_tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + header_tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.node]), is_center: PropTypes.bool, is_tooltip_disabled: PropTypes.bool, onMouseEnter: PropTypes.func, diff --git a/packages/trader/src/App/Containers/populate-header.jsx b/packages/trader/src/App/Containers/populate-header.jsx index 1690f023037a..b6831ac6f4d1 100644 --- a/packages/trader/src/App/Containers/populate-header.jsx +++ b/packages/trader/src/App/Containers/populate-header.jsx @@ -3,6 +3,7 @@ import TogglePositionsMobile from 'App/Components/Elements/TogglePositions/toggl import { filterByContractType } from 'App/Components/Elements/PositionsDrawer/helpers'; import { useTraderStore } from 'Stores/useTraderStores'; import { observer, useStore } from '@deriv/stores'; +import { TURBOS } from '@deriv/shared'; const PopulateHeader = observer(() => { const { portfolio, ui, client } = useStore(); @@ -18,20 +19,23 @@ const PopulateHeader = observer(() => { onClickCancel: onPositionsCancel, } = portfolio; - const symbol_positions = positions.filter( + const filtered_positions = positions.filter( p => p.contract_info && symbol === p.contract_info.underlying && - filterByContractType(p.contract_info, trade_contract_type) + (trade_contract_type.includes('turbos') + ? filterByContractType(p.contract_info, TURBOS.SHORT) || + filterByContractType(p.contract_info, TURBOS.LONG) + : filterByContractType(p.contract_info, trade_contract_type)) ); return ( \ No newline at end of file diff --git a/packages/trader/src/Assets/Trading/Categories/__tests__/turbos-trade-description.spec.tsx b/packages/trader/src/Assets/Trading/Categories/__tests__/turbos-trade-description.spec.tsx new file mode 100644 index 000000000000..4990d596887a --- /dev/null +++ b/packages/trader/src/Assets/Trading/Categories/__tests__/turbos-trade-description.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { TurbosTradeDescription } from '../turbos-trade-description'; + +describe('', () => { + it('a proper text of description should be rendered', () => { + render(); + expect( + screen.getByText( + /This product allows you to express a strong bullish or bearish view on an underlying asset/i + ) + ).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/Assets/Trading/Categories/icon-trade-categories.jsx b/packages/trader/src/Assets/Trading/Categories/icon-trade-categories.jsx index 4e13c7c5a8f7..1343782bbd7e 100644 --- a/packages/trader/src/Assets/Trading/Categories/icon-trade-categories.jsx +++ b/packages/trader/src/Assets/Trading/Categories/icon-trade-categories.jsx @@ -215,6 +215,19 @@ const IconTradeCategory = ({ category, className }) => {
); break; + case 'turboslong': + case 'turbosshort': + IconCategory = ( + +
+ +
+
+ +
+
+ ); + break; case 'vanilla': IconCategory = ( diff --git a/packages/trader/src/Assets/Trading/Categories/trade-categories-gif.jsx b/packages/trader/src/Assets/Trading/Categories/trade-categories-gif.jsx index 6d94956deec6..9ba4e6633ad4 100644 --- a/packages/trader/src/Assets/Trading/Categories/trade-categories-gif.jsx +++ b/packages/trader/src/Assets/Trading/Categories/trade-categories-gif.jsx @@ -16,6 +16,7 @@ import ImageRunHighLow from 'Assets/SvgComponents/trade_explanations/img-run-hig import ImageSpread from 'Assets/SvgComponents/trade_explanations/img-spread.svg'; import ImageTickHighLow from 'Assets/SvgComponents/trade_explanations/img-tick-high-low.svg'; import ImageTouch from 'Assets/SvgComponents/trade_explanations/img-touch.svg'; +import ImageTurbos from 'Assets/SvgComponents/trade_explanations/img-turbos.svg'; import ImageVanilla from 'Assets/SvgComponents/trade_explanations/img-vanilla.svg'; import ContractTypeDescriptionVideo from './contract-type-description-video'; @@ -58,6 +59,9 @@ const TradeCategoriesGIF = ({ category, selected_contract_type }) => { return ; case 'touch': return ; + case 'turbosshort': + case 'turboslong': + return ; case 'vanilla': return ; default: diff --git a/packages/trader/src/Assets/Trading/Categories/trade-categories.jsx b/packages/trader/src/Assets/Trading/Categories/trade-categories.jsx index 2d6ab5f68217..9ca7e851365e 100644 --- a/packages/trader/src/Assets/Trading/Categories/trade-categories.jsx +++ b/packages/trader/src/Assets/Trading/Categories/trade-categories.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { Text } from '@deriv/components'; import { localize, Localize } from '@deriv/translations'; import AccumulatorTradeDescription from './accumulator-trade-description'; +import { TurbosTradeDescription } from './turbos-trade-description'; // Templates are from Binary 1.0, it should be checked if they need change or not and add all of trade types // TODO: refactor the rest of descriptions to use them as components like AccumulatorTradeDescription @@ -430,6 +431,10 @@ const TradeCategories = ({ category, onClick }) => { ); break; + case 'turbosshort': + case 'turboslong': + TradeTypeTemplate = ; + break; case 'vanilla': TradeTypeTemplate = ( diff --git a/packages/trader/src/Assets/Trading/Categories/turbos-trade-description.tsx b/packages/trader/src/Assets/Trading/Categories/turbos-trade-description.tsx new file mode 100644 index 000000000000..1bdbfb853b08 --- /dev/null +++ b/packages/trader/src/Assets/Trading/Categories/turbos-trade-description.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Localize, localize } from '@deriv/translations'; +import { Text } from '@deriv/components'; + +export const TurbosTradeDescription = () => { + const content = [ + { + type: 'paragraph', + text: localize( + 'This product allows you to express a strong bullish or bearish view on an underlying asset.' + ), + }, + { type: 'heading', text: localize('For Long:') }, + { + type: 'paragraph', + text: ( + ]} + /> + ), + }, + { type: 'heading', text: localize('For Short:') }, + { + type: 'paragraph', + text: ( + ]} + /> + ), + }, + { + type: 'paragraph', + text: localize('You can determine the expiry of your contract by setting the duration or end time.'), + }, + ]; + + return ( + + {content.map(({ type, text }, index) => + type === 'heading' ? ( + + {text} + + ) : ( + + {text} + + ) + )} + + ); +}; diff --git a/packages/trader/src/Constants/contract.js b/packages/trader/src/Constants/contract.js index 1bffd31f4f48..179f0c548514 100644 --- a/packages/trader/src/Constants/contract.js +++ b/packages/trader/src/Constants/contract.js @@ -3,6 +3,7 @@ import { localize, Localize } from '@deriv/translations'; export const getCardLabels = () => ({ APPLY: localize('Apply'), + BARRIER: localize('Barrier:'), BUY_PRICE: localize('Buy price:'), CANCEL: localize('Cancel'), CLOSE: localize('Close'), @@ -262,6 +263,16 @@ export const getSupportedContracts = is_high_low => ({ name: , position: 'bottom', }, + TURBOSLONG: { + button_name: , + name: , + position: 'top', + }, + TURBOSSHORT: { + button_name: , + name: , + position: 'bottom', + }, VANILLALONGCALL: { name: , position: 'top', diff --git a/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx b/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx index 3f7dbca23bf1..39e173f7031a 100644 --- a/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx +++ b/packages/trader/src/Modules/Contract/Containers/contract-replay.jsx @@ -20,6 +20,7 @@ import { isEmptyObject, isMobile, isMultiplierContract, + isTurbosContract, isVanillaContract, isOnlyUpsDownsContract, urlFor, @@ -80,6 +81,7 @@ const ContractReplay = observer(({ contract_id }) => { 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_only_ups_downs = isOnlyUpsDownsContract(contract_info.contract_type); @@ -93,6 +95,7 @@ const ContractReplay = observer(({ contract_id }) => { is_dark_theme={is_dark_theme} is_market_closed={is_market_closed} is_multiplier={is_multiplier} + is_turbos={is_turbos} is_sell_requested={is_sell_requested} is_valid_to_cancel={is_valid_to_cancel} is_vanilla={is_vanilla} diff --git a/packages/trader/src/Modules/Trading/Components/Elements/__tests__/payout-per-point-mobile.spec.tsx b/packages/trader/src/Modules/Trading/Components/Elements/__tests__/payout-per-point-mobile.spec.tsx new file mode 100644 index 000000000000..e85c2d1b55d4 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Elements/__tests__/payout-per-point-mobile.spec.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import PayoutPerPointMobile from '../payout-per-point-mobile'; +import { mockStore } from '@deriv/stores'; +import userEvent from '@testing-library/user-event'; +import TraderProviders from '../../../../../trader-providers'; + +const mocked_root_store = { + modules: { + trade: { + currency: 'EUR', + proposal_info: { + TURBOSLONG: { obj_contract_basis: { text: 'Payout per point', value: 10 }, message: 'test' }, + }, + contract_type: 'turboslong', + vanilla_trade_type: 'VANILLALONGCALL', + }, + }, +}; + +describe('', () => { + beforeEach(() => { + render( + + + + ); + }); + it('should render label name correctly', () => { + expect(screen.getByText('Payout per point')).toBeInTheDocument(); + }); + it('should render amount correctly', () => { + expect(screen.getByText(/10/i)).toBeInTheDocument(); + }); + it('should render currency correctly', () => { + expect(screen.getByText(/EUR/i)).toBeInTheDocument(); + }); + it('should render tooltip text correctly', () => { + userEvent.hover(screen.getByTestId('dt_popover_wrapper')); + expect(screen.getByText(/test/i)).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/Modules/Trading/Components/Elements/payout-per-point-mobile.tsx b/packages/trader/src/Modules/Trading/Components/Elements/payout-per-point-mobile.tsx new file mode 100644 index 000000000000..169dab63fea0 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Elements/payout-per-point-mobile.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Icon, Money, Text, Popover } from '@deriv/components'; +import { Localize, localize } from '@deriv/translations'; +import Fieldset from 'App/Components/Form/fieldset.jsx'; +import { observer } from '@deriv/stores'; +import { getContractSubtype, isVanillaContract } from '@deriv/shared'; +import { useTraderStore } from 'Stores/useTraderStores'; + +type TProposalInfo = { + [key: string]: { + has_error?: boolean; + id: string; + has_increased?: boolean; + message?: string; + cancellation?: { + ask_price: number; + date_expiry: number; + }; + growth_rate?: number; + obj_contract_basis?: Record<'text' | 'value', string>; + returns?: string; + stake: string; + }; +}; + +const PayoutPerPointMobile = observer(() => { + const { currency, proposal_info, contract_type, vanilla_trade_type } = useTraderStore(); + const contract_key = isVanillaContract(contract_type) ? vanilla_trade_type : contract_type?.toUpperCase(); + // remove assertion and local TProposalInfo type after TS migration for trade package is complete + const { has_error, has_increased, id, message, obj_contract_basis } = + (proposal_info as TProposalInfo)?.[contract_key] || {}; + const { text: label, value: payout_per_point } = obj_contract_basis || {}; + const has_error_or_not_loaded = has_error || !id; + const turbos_titles = { + Long: localize('For Long:'), + Short: localize('For Short:'), + }; + const tooltip_text = isVanillaContract(contract_type) ? ( + + ) : ( + ]} + values={{ + title: turbos_titles[getContractSubtype(contract_key) as keyof typeof turbos_titles], + message, + }} + /> + ); + if (!payout_per_point) return
; + return ( +
+
+ + {label} + + +
+ + + + {!has_error_or_not_loaded && has_increased !== null && has_increased ? ( + + ) : ( + + )} + + +
+ ); +}); + +export default PayoutPerPointMobile; diff --git a/packages/trader/src/Modules/Trading/Components/Elements/purchase-button.jsx b/packages/trader/src/Modules/Trading/Components/Elements/purchase-button.jsx index f7aea8ea8a78..3b43b46a2f3c 100644 --- a/packages/trader/src/Modules/Trading/Components/Elements/purchase-button.jsx +++ b/packages/trader/src/Modules/Trading/Components/Elements/purchase-button.jsx @@ -38,6 +38,7 @@ const PurchaseButton = ({ is_multiplier, is_vanilla, is_proposal_empty, + is_turbos, purchased_states_arr, setPurchaseState, should_fade, @@ -60,7 +61,7 @@ const PurchaseButton = ({ ); - } else if (!is_vanilla) { + } else if (!is_vanilla && !is_turbos) { button_value = ( {!(is_loading || is_disabled) ? non_multiplier_info_right : ''} @@ -82,6 +83,7 @@ const PurchaseButton = ({ 'btn-purchase--accumulator': is_accumulator, 'btn-purchase--multiplier': is_multiplier, 'btn-purchase--multiplier-deal-cancel': has_deal_cancellation, + 'btn-purchase--turbos': is_turbos, 'btn-purchase--1__vanilla-opts': index === 0 && is_vanilla, 'btn-purchase--2__vanilla-opts': index === 1 && is_vanilla, })} @@ -123,7 +125,7 @@ const PurchaseButton = ({ is_high_low={is_high_low} />
- {!is_vanilla && ( + {!is_turbos && !is_vanilla && (
@@ -87,6 +90,7 @@ const PurchaseFieldset = ({ has_increased={info.has_increased} is_loading={is_loading} is_multiplier={is_multiplier} + is_turbos={is_turbos} is_vanilla={is_vanilla} should_fade={should_fade} type={type} @@ -163,6 +167,7 @@ PurchaseFieldset.propTypes = { is_multiplier: PropTypes.bool, is_proposal_empty: PropTypes.bool, is_proposal_error: PropTypes.bool, + is_turbos: PropTypes.bool, is_vanilla: PropTypes.bool, onClickPurchase: PropTypes.func, onHoverPurchase: PropTypes.func, diff --git a/packages/trader/src/Modules/Trading/Components/Form/ContractType/ContractTypeInfo/contract-type-info.jsx b/packages/trader/src/Modules/Trading/Components/Form/ContractType/ContractTypeInfo/contract-type-info.jsx index d16704345bb2..641c5b2b2708 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/ContractType/ContractTypeInfo/contract-type-info.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/ContractType/ContractTypeInfo/contract-type-info.jsx @@ -17,7 +17,9 @@ const TABS = { const Info = ({ handleNavigationClick, handleSelect, initial_index, item, list }) => { const [carousel_index, setCarouselIndex] = React.useState(''); const [selected_tab, setSelectedTab] = React.useState(TABS.DESCRIPTION); - const contract_types = getContractTypes(list, item).filter(i => i.value !== 'rise_fall_equal'); + const contract_types = getContractTypes(list, item).filter( + i => i.value !== 'rise_fall_equal' && i.value !== 'turbosshort' + ); const has_toggle_buttons = /accumulator|vanilla/i.test(carousel_index); const is_description_tab_selected = selected_tab === TABS.DESCRIPTION; const is_glossary_tab_selected = selected_tab === TABS.GLOSSARY; diff --git a/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-list.jsx b/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-list.jsx index dea16a03d3fa..885f0e0232ef 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-list.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/ContractType/contract-type-list.jsx @@ -9,14 +9,14 @@ const List = ({ handleInfoClick, handleSelect, list, name, value }) => list.map((contract_category, key) => { const contract_types = contract_category.contract_types?.filter(contract_type => { const base_contract_type = /^(.*)_equal$/.exec(contract_type.value)?.[1]; - + if (contract_type.value === 'turbosshort') return false; if (base_contract_type) { return !contract_category.contract_types.some(c => c.value === base_contract_type); } return true; }); - const is_new = contract_category.key === 'Accumulators' || contract_category.key === 'Vanillas'; + const is_new = /(Accumulators|Turbos|Vanillas)/i.test(contract_category.key); return (
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 6e367e6dd4b8..db0407318c3a 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 @@ -12,7 +12,6 @@ const ContractTypeWidget = ({ is_equal, name, value, list, onChange, languageCha const [selected_category, setSelectedCategory] = React.useState(null); const [search_query, setSearchQuery] = React.useState(''); const [item, setItem] = React.useState(null); - const [selected_item, setSelectedItem] = React.useState(null); const handleClickOutside = React.useCallback( event => { @@ -44,17 +43,11 @@ const ContractTypeWidget = ({ is_equal, name, value, list, onChange, languageCha setDialogVisibility(false); setInfoDialogVisibility(false); setItem(clicked_item); - setSelectedItem(clicked_item); setSelectedCategory(key); + onChange({ target: { name, value: clicked_item.value } }); } }; - React.useEffect(() => { - if (selected_item && selected_item.value !== value) { - onChange({ target: { name, value: selected_item.value } }); - } - }, [selected_item, onChange, name, value]); - const handleInfoClick = clicked_item => { setInfoDialogVisibility(!is_info_dialog_open); @@ -122,7 +115,7 @@ const ContractTypeWidget = ({ is_equal, name, value, list, onChange, languageCha categories.push({ label: localize('Options'), contract_categories: options_category, - component: options_category.some(category => category.key === 'Vanillas') && ( + component: options_category.some(category => /Vanillas|Turbos/i.test(category.key)) && ( {localize('NEW')}! ), key: 'Options', @@ -174,7 +167,7 @@ const ContractTypeWidget = ({ is_equal, name, value, list, onChange, languageCha const selected_contract_index = () => { const contract_types_arr = list_with_category()?.flatMap(category => category.contract_types); return contract_types_arr - .filter(type => type.value !== 'rise_fall_equal') + .filter(type => type.value !== 'rise_fall_equal' && type.value !== 'turbosshort') .findIndex(type => type.value === item?.value); }; diff --git a/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx b/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx index 3b1007a9139d..56eb0af097a3 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { DesktopWrapper, Icon, MobileWrapper, Money, Popover, Text } from '@deriv/components'; import { Localize, localize } from '@deriv/translations'; -import { getCurrencyDisplayCode, getLocalizedBasis, isMobile, getGrowthRatePercentage } from '@deriv/shared'; +import { getContractSubtype, getCurrencyDisplayCode, getLocalizedBasis, getGrowthRatePercentage } from '@deriv/shared'; import CancelDealInfo from './cancel-deal-info.jsx'; export const ValueMovement = ({ @@ -11,21 +11,22 @@ export const ValueMovement = ({ proposal_info, currency, has_increased, + is_turbos, is_vanilla, value, show_currency = true, }) => ( -
-
+
+
{!has_error_or_not_loaded && ( )}
@@ -47,18 +48,18 @@ const ContractInfo = ({ is_loading, is_accumulator, is_multiplier, + is_turbos, is_vanilla, should_fade, proposal_info, type, }) => { const localized_basis = getLocalizedBasis(); - const stakeOrPayout = () => { switch (basis) { case 'stake': { - if (is_vanilla) { - return localize('Payout per point'); + if (is_vanilla || is_turbos) { + return localized_basis.payout_per_point; } return localized_basis.payout; } @@ -70,39 +71,48 @@ const ContractInfo = ({ } }; - const setBasisText = () => { - if (is_vanilla) { - return localize('Payout per point'); - } - return proposal_info.obj_contract_basis.text; - }; - const has_error_or_not_loaded = proposal_info.has_error || !proposal_info.id; - - const basis_text = has_error_or_not_loaded ? stakeOrPayout() : setBasisText(); - + const basis_text = has_error_or_not_loaded ? stakeOrPayout() : proposal_info.obj_contract_basis.text; const { message, obj_contract_basis, stake } = proposal_info; const setHintMessage = () => { - if (['VANILLALONGCALL', 'VANILLALONGPUT'].includes(type)) { + if (is_turbos) { + return ( + ]} + values={{ + title: getContractSubtype(type) === 'Long' ? localize('For Long:') : localize('For Short:'), + message, + }} + /> + ); + } + if (is_vanilla) { return ( ); } + return message; }; return ( -
+
- {(is_multiplier || is_accumulator) && ( + {is_multiplier || is_accumulator ? ( {!is_accumulator && ( @@ -123,80 +133,36 @@ const ContractInfo = ({
- )} - {is_vanilla && isMobile() && ( - - -
- {basis_text} -
-
- -
- - } - /> -
-
-
-
- )} - {!is_multiplier && !is_accumulator && !(is_vanilla && isMobile()) && obj_contract_basis && ( - -
- {basis_text} -
- - - - -
+ ) : ( + !is_multiplier && + !is_accumulator && + obj_contract_basis && ( + +
{basis_text}
+ -
-
-
+ + +
+ +
+
+ + ) )}
{!is_multiplier && !is_accumulator && ( @@ -223,6 +189,7 @@ ContractInfo.propTypes = { has_increased: PropTypes.bool, is_accumulator: PropTypes.bool, is_multiplier: PropTypes.bool, + is_turbos: PropTypes.bool, is_vanilla: PropTypes.bool, is_loading: PropTypes.bool, proposal_info: PropTypes.object, diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Turbos/__tests__/barrier-selector.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Turbos/__tests__/barrier-selector.spec.tsx new file mode 100644 index 000000000000..62c20c76d552 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Turbos/__tests__/barrier-selector.spec.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import BarrierSelector from '../barrier-selector'; +import { mockStore } from '@deriv/stores'; +import TraderProviders from '../../../../../../../trader-providers'; + +const mocked_root_store = { + modules: { + trade: { + barrier_1: '16', + onChange: jest.fn(e => { + if (mocked_root_store.modules) { + mocked_root_store.modules.trade.barrier_1 = e.target.value; + } + }), + setHoveredBarrier: jest.fn(), + barrier_choices: ['16', '33', '40'], + }, + }, +}; + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), +})); + +jest.mock('@deriv/components', () => { + const original_module = jest.requireActual('@deriv/components'); + return { + ...original_module, + Icon: jest.fn(() =>
IcCross
), + }; +}); + +describe('', () => { + const barriers_list_header = 'Barriers'; + let current_barrier: HTMLElement; + beforeEach(() => { + render( + + + + ); + current_barrier = screen.getByTestId('current_barrier'); + }); + it('should render properly with Barrier inside it', () => { + const barrier_title = screen.getByText('Barrier'); + + expect(barrier_title).toBeInTheDocument(); + }); + it('barrier_1 value is selected by default', () => { + expect(screen.getByText('16')).toBeInTheDocument(); + }); + it('barrier list should not be rendered by default', () => { + expect(screen.queryByText(barriers_list_header)).not.toBeInTheDocument(); + }); + it('barrier list is displayed after clicking on the current barrier', () => { + userEvent.click(current_barrier); + + expect(screen.getByText(barriers_list_header)).toBeInTheDocument(); + }); + it('should render all available barrier values from barrier_choices in barrier list when it is expanded', () => { + userEvent.click(current_barrier); + + ['16', '33', '40'].forEach(barrier => expect(screen.getByTestId(barrier)).toBeInTheDocument()); + }); + it('onChange should be called with the new barrier option when it is clicked', () => { + userEvent.click(current_barrier); + const clicked_barrier = screen.getByTestId('33'); + userEvent.click(clicked_barrier); + expect(mocked_root_store.modules?.trade.barrier_1).toBe('33'); + }); + it('barrier list should not be rendered when cross icon is clicked', () => { + userEvent.click(current_barrier); + const icon_cross = screen.getAllByText('IcCross'); + userEvent.click(icon_cross[0]); + + expect(screen.queryByText(barriers_list_header)).not.toBeInTheDocument(); + }); + it('barrier list should not be rendered when the new barrier option was clicked', () => { + userEvent.click(current_barrier); + const clicked_barrier = screen.getByTestId('33'); + userEvent.click(clicked_barrier); + + expect(screen.queryByText(barriers_list_header)).not.toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Turbos/barrier-selector.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Turbos/barrier-selector.tsx new file mode 100644 index 000000000000..622914917c7a --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Turbos/barrier-selector.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import BarriersList from '../barriers-list'; +import { DesktopWrapper, Icon, MobileDialog, MobileWrapper, Text, Popover } from '@deriv/components'; +import Fieldset from 'App/Components/Form/fieldset.jsx'; +import { Localize, localize } from '@deriv/translations'; +import { observer } from '@deriv/stores'; +import { useTraderStore } from 'Stores/useTraderStores'; + +const BarrierSelector = observer(() => { + const { barrier_1, onChange, setHoveredBarrier, barrier_choices } = useTraderStore(); + const [is_barriers_table_expanded, setIsBarriersTableExpanded] = React.useState(false); + const [is_mobile_tooltip_visible, setIsMobileTooltipVisible] = React.useState(false); + const [selected_barrier, setSelectedBarrier] = React.useState(barrier_1); + + const toggleMobileTooltip = () => setIsMobileTooltipVisible(!is_mobile_tooltip_visible); + + const toggleBarriersTable = () => { + setIsMobileTooltipVisible(false); + setIsBarriersTableExpanded(!is_barriers_table_expanded); + }; + + const onBarrierClick = (barrier: string) => { + setHoveredBarrier(''); + setSelectedBarrier(barrier); + onChange({ + target: { + name: 'barrier_1', + value: barrier, + }, + }); + setIsBarriersTableExpanded(false); + }; + + React.useEffect(() => { + setSelectedBarrier(barrier_1); + }, [barrier_1]); + + const header_tooltip_text = [localize('For Long:'), localize('For Short:')].map(title => ( +
+ ]} + values={{ + title, + price_position: + title === localize('For Long:') ? localize('above the barrier') : localize('below the barrier'), + }} + /> +
+ )); + + const barriers_header_mobile = ( +
+
{localize('Barriers')}
+ +
+ ); + + return ( + + +
+ + {localize('Spot')} + + + {barrier_1} + + + {localize('Barrier')} + +
+ + + +
+ +
+
+ + {localize('Spot')} + + + {barrier_1} + + +
+
+ {is_barriers_table_expanded && ( + + )} +
+
+ ); +}); + +export default BarrierSelector; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/barriers-list.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/barriers-list.spec.tsx new file mode 100644 index 000000000000..44907c267788 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/barriers-list.spec.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import BarriersList from '../barriers-list'; + +const barrier_choices = ['16', '33', '40']; +const classname = 'trade-container__barriers-table'; +const mockClickCallback = jest.fn(); +const mockHoverCallback = jest.fn(); +const mockClickCrossCallback = jest.fn(); + +describe('', () => { + beforeEach(() => { + render( + + ); + }); + it('all barrier options should be rendered', () => { + barrier_choices.forEach(barrier => expect(screen.getByTestId(barrier)).toBeInTheDocument()); + }); + it('selected barrier should have a proper className', () => { + expect(screen.getByTestId(barrier_choices[0])).toHaveClass(`${classname}__item ${classname}__item--selected`); + }); + it('non-selected barrier option should have a proper className', () => { + expect(screen.getByTestId(barrier_choices[1])).toHaveClass(`${classname}__item`); + expect(screen.getByTestId(barrier_choices[1])).not.toHaveClass( + `${classname}__item ${classname}__item--selected` + ); + }); + it('click handler should be called after clicking on the 2nd barrier option (33)', () => { + userEvent.click(screen.getByTestId(barrier_choices[1])); + expect(mockClickCallback).toHaveBeenCalled(); + }); + it('hover handler should be called when the 3rd barrier option (40) is hovered', () => { + userEvent.hover(screen.getByTestId(barrier_choices[2])); + expect(mockHoverCallback).toHaveBeenCalled(); + }); + it('hover handler should be called with null when mouseLeave event fires on the 3rd barrier option (40)', () => { + userEvent.unhover(screen.getByTestId(barrier_choices[2])); + expect(mockHoverCallback).toHaveBeenCalledWith(''); + }); +}); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/min-max-stake-info.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/min-max-stake-info.spec.tsx new file mode 100644 index 000000000000..10b4a342ee27 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/min-max-stake-info.spec.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import MinMaxStakeInfo from '../min-max-stake-info'; +import { mockStore } from '@deriv/stores'; +import TraderProviders from '../../../../../../trader-providers'; + +const mocked_root_store = { + modules: { + trade: { + currency: 'USD', + contract_type: 'turboslong', + stake_boundary: { + TURBOSLONG: { + min_stake: 0, + max_stake: 100, + }, + }, + }, + }, +}; + +describe('', () => { + const mock_props = { + className: 'trade-container__stake-field', + }; + + it('should be rendered correctly with both Min. stake and Max. stake', () => { + render( + + + + ); + + [screen.getByText('Min. stake'), screen.getByText('Max. stake')].forEach(stake_text => { + expect(stake_text).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/trade-type-tabs.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/trade-type-tabs.spec.tsx new file mode 100644 index 000000000000..f746485a515a --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/__tests__/trade-type-tabs.spec.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import TradeTypeTabs from '../trade-type-tabs'; +import { mockStore } from '@deriv/stores'; +import TraderProviders from '../../../../../../trader-providers'; + +describe('Trade Type Tabs', () => { + const mock_root_store = { + modules: { + trade: { + contract_type: 'turboslong', + onChange: jest.fn(() => { + if (mock_root_store.modules) { + mock_root_store.modules.trade.contract_type = 'turbosshort'; + } + }), + vanilla_trade_type: 'VANILLALONGCALL', + }, + }, + }; + const mockTradeTypeTabs = (mocked_store: typeof mock_root_store) => { + return ( + + + + ); + }; + it('should render Long & Short tabs when contract_type = turboslong', () => { + render(mockTradeTypeTabs(mock_root_store)); + const long_tab = screen.getByText('Long'); + const short_tab = screen.getByText('Short'); + [long_tab, short_tab].forEach(tab => { + expect(tab).toBeInTheDocument(); + }); + }); + + it('should render Call & Put tabs when contract_type = vanilla, and vanilla_trade_type = VANILLALONGCALL', () => { + if (mock_root_store.modules) { + mock_root_store.modules.trade.contract_type = 'vanilla'; + } + render(mockTradeTypeTabs(mock_root_store)); + const call_tab = screen.getByText('Call'); + const put_tab = screen.getByText('Put'); + [call_tab, put_tab].forEach(tab => { + expect(tab).toBeInTheDocument(); + }); + }); + + it('should not render if contract_type is other than turbos or vanillas', () => { + if (mock_root_store.modules) { + mock_root_store.modules.trade.contract_type = 'invalid_type'; + } + render(mockTradeTypeTabs(mock_root_store)); + const long_tab = screen.queryByText('Long'); + const short_tab = screen.queryByText('Short'); + [long_tab, short_tab].forEach(tab => { + expect(tab).not.toBeInTheDocument(); + }); + }); + + it('should call onChange when a tab is clicked', () => { + if (mock_root_store.modules) { + mock_root_store.modules.trade.contract_type = 'turboslong'; + } + render(mockTradeTypeTabs(mock_root_store)); + + const short_tab = screen.getByText('Short'); + userEvent.click(short_tab); + + expect(mock_root_store.modules?.trade.contract_type).toBe('turbosshort'); + }); +}); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount-mobile.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount-mobile.jsx index d588f1377351..e78e2f9303a7 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount-mobile.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount-mobile.jsx @@ -1,11 +1,11 @@ -import { Localize, localize } from '@deriv/translations'; -import { Money, Numpad, Tabs, Text } from '@deriv/components'; -import { getDecimalPlaces, isEmptyObject } from '@deriv/shared'; - import React from 'react'; import classNames from 'classnames'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; +import { Localize, localize } from '@deriv/translations'; +import { Money, Numpad, Tabs } from '@deriv/components'; +import { getDecimalPlaces, isEmptyObject } from '@deriv/shared'; +import MinMaxStakeInfo from './min-max-stake-info'; const Basis = observer( ({ @@ -19,16 +19,17 @@ const Basis = observer( setAmountError, }) => { const { ui, client } = useStore(); - const { addToast, vanilla_trade_type } = ui; + const { addToast } = ui; const { currency } = client; const { + is_turbos, + is_vanilla, onChangeMultiple, trade_amount, trade_basis, trade_duration_unit, trade_duration, contract_type, - stake_boundary, } = useTraderStore(); const user_currency_decimal_places = getDecimalPlaces(currency); const onNumberChange = num => { @@ -74,53 +75,42 @@ const Basis = observer( }; return ( -
- {contract_type === 'vanilla' && ( -
-
- {localize('Min. stake')} - - {stake_boundary[vanilla_trade_type].min_stake} {currency} - -
-
- {localize('Max. stake')} - - {stake_boundary[vanilla_trade_type].max_stake} {currency} - -
-
- )} -
- { - return ( -
- {parseFloat(v) > 0 ? ( - - ) : ( - v - )} -
- ); - }} - reset_press_interval={450} - reset_value='' - pip_size={user_currency_decimal_places} - onValidate={validateAmount} - submit_label={localize('OK')} - onValueChange={onNumberChange} - /> + +
+ {(is_turbos || is_vanilla) && } +
+ { + return ( +
+ {parseFloat(value) > 0 ? ( + + ) : ( + value + )} +
+ ); + }} + reset_press_interval={450} + reset_value='' + pip_size={user_currency_decimal_places} + onValidate={validateAmount} + submit_label={localize('OK')} + onValueChange={onNumberChange} + /> +
-
+ ); } ); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx index 978d26394d8d..0e9ac8b72281 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx @@ -1,11 +1,12 @@ import { AMOUNT_MAX_LENGTH, addComma, getDecimalPlaces } from '@deriv/shared'; -import { ButtonToggle, Dropdown, InputField, Text } from '@deriv/components'; +import { ButtonToggle, Dropdown, InputField } from '@deriv/components'; import { Localize, localize } from '@deriv/translations'; import AllowEquals from './allow-equals.jsx'; import Fieldset from 'App/Components/Form/fieldset.jsx'; import Multiplier from './Multiplier/multiplier.jsx'; import MultipliersInfo from './Multiplier/info.jsx'; +import MinMaxStakeInfo from './min-max-stake-info'; import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; @@ -51,7 +52,7 @@ export const Input = ({ const Amount = observer(({ is_minimized, is_nativepicker }) => { const { ui, client } = useStore(); const { currencies_list, is_single_currency } = client; - const { setCurrentFocus, vanilla_trade_type, current_focus } = ui; + const { setCurrentFocus, current_focus } = ui; const { amount, basis, @@ -65,10 +66,11 @@ const Amount = observer(({ is_minimized, is_nativepicker }) => { is_accumulator, is_equal, is_multiplier, + is_turbos, + is_vanilla, has_equals_only, onChange, validation_errors, - stake_boundary, } = useTraderStore(); if (is_minimized) { @@ -107,7 +109,7 @@ const Amount = observer(({ is_minimized, is_nativepicker }) => {
{ /> )} - {contract_type === 'vanilla' && ( -
-
- {localize('Min. stake')} - - {stake_boundary[vanilla_trade_type].min_stake} {currency} - -
-
- {localize('Max. stake')} - - {stake_boundary[vanilla_trade_type].max_stake} {currency} - -
-
- )} + {(is_turbos || is_vanilla) && }
); }); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/barriers-list-body.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/barriers-list-body.tsx new file mode 100644 index 000000000000..b109b2bf6d9b --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/barriers-list-body.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { isMobile } from '@deriv/shared'; +import { Text, ThemedScrollbars } from '@deriv/components'; +import classNames from 'classnames'; + +export type TBarriersListBody = { + barriers_list: string[]; + className?: string; + onClick: (barrier: string) => void; + onHover?: (barrier: string) => void; + selected_item: string; + subheader?: string; +}; + +const BarriersListBody = ({ + barriers_list, + className, + onClick, + onHover, + selected_item, + subheader, +}: TBarriersListBody) => { + const onMouseEnter = (barrier: string) => { + if (selected_item !== barrier && typeof onHover === 'function') { + onHover(barrier); + } + }; + return ( + + {subheader && ( + + {subheader} + + )} + +
    + {barriers_list.map(barrier => ( + onClick(barrier)} + onMouseEnter={() => onMouseEnter(barrier)} + onMouseLeave={() => typeof onHover === 'function' && onHover('')} + > + {barrier} + + ))} +
+
+
+ ); +}; + +export default React.memo(BarriersListBody); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/barriers-list.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/barriers-list.tsx new file mode 100644 index 000000000000..1ea46ae88a84 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/barriers-list.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import classNames from 'classnames'; +import { DesktopWrapper, MobileWrapper, Text, Icon } from '@deriv/components'; +import { CSSTransition } from 'react-transition-group'; +import Fieldset from 'App/Components/Form/fieldset.jsx'; +import BarriersListBody, { TBarriersListBody } from './barriers-list-body'; + +type TBarriersList = TBarriersListBody & { + header: string; + onClickCross: () => void; + show_table: boolean; +}; + +const BarriersList = ({ className, header, onClickCross, show_table, ...props }: TBarriersList) => ( + + + +
+
+ + {header} + +
+ +
+
+ +
+
+
+ + + +
+); + +export default React.memo(BarriersList); diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/min-max-stake-info.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/min-max-stake-info.tsx new file mode 100644 index 000000000000..22bc7338352b --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/min-max-stake-info.tsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import React from 'react'; +import { Money, Text } from '@deriv/components'; +import { Localize } from '@deriv/translations'; +import { observer } from '@deriv/stores'; +import { isMobile, isVanillaContract } from '@deriv/shared'; +import { useTraderStore } from 'Stores/useTraderStores'; + +type TMinMaxStakeInfo = { + className?: string; +}; +type TStakeBoundary = { [key: string]: { min_stake: number; max_stake: number } }; + +const MinMaxStakeInfo = observer(({ className }: TMinMaxStakeInfo) => { + const { contract_type, currency, stake_boundary, vanilla_trade_type } = useTraderStore(); + // remove assertion and local TStakeBoundary type after TS migration for trade package is complete + const { min_stake, max_stake } = + (isVanillaContract(contract_type) + ? (stake_boundary as TStakeBoundary)[vanilla_trade_type] + : (stake_boundary as TStakeBoundary)[contract_type.toUpperCase()]) || {}; + + return ( +
+ {!isNaN(min_stake) && + !isNaN(max_stake) && + ['Min', 'Max'].map(text => ( + + , + ]} + /> + + ))} +
+ ); +}); + +export default MinMaxStakeInfo; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike-field.scss b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike-field.scss deleted file mode 100644 index 05999a2eb179..000000000000 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike-field.scss +++ /dev/null @@ -1,38 +0,0 @@ -.strike-field { - background-color: var(--general-section-1); - border-radius: $BORDER_RADIUS; - box-sizing: border-box; - display: flex; - flex-direction: column; - height: 100%; - overflow: auto; - position: absolute; - top: 0; - width: 24rem; - z-index: 3; - - &--header { - padding: 1.6rem; - display: flex; - justify-content: space-between; - border-bottom: 1px solid var(--general-hover); - } - - &--body { - padding: 1.6rem; - display: flex; - flex-direction: column; - cursor: pointer; - gap: 0.8rem; - - &-item { - &:hover { - background-color: var(--state-hover); - } - } - - &--active { - background-color: var(--state-active); - } - } -} diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx index e48ba76b826b..8453e0c5e18a 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx @@ -1,18 +1,26 @@ import React from 'react'; import classNames from 'classnames'; -import { DesktopWrapper, InputField, MobileWrapper, Dropdown, Text, Icon } from '@deriv/components'; +import BarriersList from './barriers-list'; +import { DesktopWrapper, InputField, MobileWrapper, Dropdown, Text } from '@deriv/components'; import { localize, Localize } from '@deriv/translations'; import { toMoment } from '@deriv/shared'; import Fieldset from 'App/Components/Form/fieldset.jsx'; import StrikeParamModal from 'Modules/Trading/Containers/strike-param-modal'; -import './strike-field.scss'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; const Strike = observer(() => { const { ui, common } = useStore(); - const { barrier_1, onChange, validation_errors, strike_price_choices, expiry_type, expiry_date } = useTraderStore(); - const { current_focus, setCurrentFocus, advanced_duration_unit, vanilla_trade_type } = ui; + const { + barrier_1, + onChange, + validation_errors, + barrier_choices: strike_price_choices, + expiry_type, + expiry_date, + vanilla_trade_type, + } = useTraderStore(); + const { current_focus, setCurrentFocus, advanced_duration_unit } = ui; const { server_time } = common; const [is_open, setIsOpen] = React.useState(false); @@ -35,38 +43,6 @@ const Strike = observer(() => { value: strike_price, })); - if (should_open_dropdown) { - return ( -
-
- - {localize('Strike Prices')} - - setShouldOpenDropdown(false)} /> -
-
- {strike_price_list.map(strike => ( - { - setSelectedValue(strike.value); - setShouldOpenDropdown(false); - onChange({ target: { name: 'barrier_1', value: strike.value } }); - }} - > - {strike.value} - - ))} -
-
- ); - } - return ( @@ -125,6 +101,21 @@ const Strike = observer(() => {
)} + {should_open_dropdown && ( + { + setSelectedValue(strike); + setShouldOpenDropdown(false); + onChange({ target: { name: 'barrier_1', value: strike } }); + }} + onClickCross={() => setShouldOpenDropdown(false)} + /> + )}
diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/trade-type-tabs.tsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/trade-type-tabs.tsx new file mode 100644 index 000000000000..33950c371391 --- /dev/null +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/trade-type-tabs.tsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +import React from 'react'; +import { ButtonToggle } from '@deriv/components'; +import { isTurbosContract, isVanillaContract, TURBOS } from '@deriv/shared'; +import { localize } from '@deriv/translations'; +import { observer } from '@deriv/stores'; +import { useTraderStore } from 'Stores/useTraderStores'; + +type TTradeTypeTabs = { + className?: string; +}; + +const TradeTypeTabs = observer(({ className }: TTradeTypeTabs) => { + const { onChange, contract_type, vanilla_trade_type } = useTraderStore(); + const is_turbos = isTurbosContract(contract_type); + const is_vanilla = isVanillaContract(contract_type); + const tab_list = [ + { text: localize('Long'), value: TURBOS.LONG, is_displayed: is_turbos }, + { text: localize('Short'), value: TURBOS.SHORT, is_displayed: is_turbos }, + { text: localize('Call'), value: 'VANILLALONGCALL', is_displayed: is_vanilla }, + { text: localize('Put'), value: 'VANILLALONGPUT', is_displayed: is_vanilla }, + ]; + + if (!is_turbos && !is_vanilla) return null; + + return ( +
+ is_displayed)} + name={is_turbos ? 'contract_type' : 'vanilla_trade_type'} + className='trade-container__trade-type-tabs--button' + is_animated + onChange={onChange} + value={ + tab_list.find(({ value }) => (is_turbos ? value === contract_type : value === vanilla_trade_type)) + ?.value ?? '' + } + /> +
+ ); +}); + +export default TradeTypeTabs; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/vanilla-trade-types.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/vanilla-trade-types.jsx deleted file mode 100644 index d7fad2343977..000000000000 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/vanilla-trade-types.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { ButtonToggle } from '@deriv/components'; -import Fieldset from 'App/Components/Form/fieldset.jsx'; -import { observer, useStore } from '@deriv/stores'; -import { useTraderStore } from 'Stores/useTraderStores'; - -const VanillaTradeTypes = observer(() => { - const { ui } = useStore(); - const { onChange } = useTraderStore(); - const { onChangeUiStore, vanilla_trade_type } = ui; - - const changeTradeType = ({ target }) => { - const { name, value } = target; - - onChange({ target: { name, value } }); - onChangeUiStore({ name, value }); - }; - - return ( -
- -
- ); -}); - -export default VanillaTradeTypes; diff --git a/packages/trader/src/Modules/Trading/Components/Form/screen-small.jsx b/packages/trader/src/Modules/Trading/Components/Form/screen-small.jsx index b58e1e8f8c5f..8a2dc1843ed1 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/screen-small.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/screen-small.jsx @@ -24,7 +24,9 @@ import 'Sass/app/_common/mobile-widget.scss'; import classNames from 'classnames'; import AccumulatorsStats from 'Modules/Contract/Components/AccumulatorsStats'; import Strike from 'Modules/Trading/Components/Form/TradeParams/strike.jsx'; -import VanillaTradeTypes from 'Modules/Trading/Components/Form/TradeParams/vanilla-trade-types.jsx'; +import BarrierSelector from 'Modules/Trading/Components/Form/TradeParams/Turbos/barrier-selector'; +import PayoutPerPointMobile from 'Modules/Trading/Components/Elements/payout-per-point-mobile'; +import TradeTypeTabs from 'Modules/Trading/Components/Form/TradeParams/trade-type-tabs'; import { observer } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; @@ -35,8 +37,9 @@ const CollapsibleTradeParams = ({ previous_symbol, is_allow_equal, is_accumulator, - is_trade_params_expanded, is_multiplier, + is_trade_params_expanded, + is_turbos, is_vanilla, onChange, take_profit, @@ -65,8 +68,8 @@ const CollapsibleTradeParams = ({
{is_multiplier && } + {isVisible('trade_type_tabs') && } {is_accumulator && } - {is_vanilla && }
{isVisible('last_digit') && (
@@ -78,6 +81,11 @@ const CollapsibleTradeParams = ({
)} + {isVisible('barrier_selector') && ( +
+ +
+ )} {isVisible('strike') && (
@@ -90,7 +98,7 @@ const CollapsibleTradeParams = ({
)} - {is_multiplier && ( + {(is_multiplier || is_turbos) && (
@@ -113,13 +121,15 @@ const CollapsibleTradeParams = ({
, ]} - {is_vanilla ? ( + {(is_turbos || is_vanilla) && } +
- ) : ( -
- -
- )} +
); }; @@ -129,6 +139,7 @@ const ScreenSmall = observer(({ is_trade_enabled }) => { const { is_accumulator, is_multiplier, + is_turbos, is_vanilla, duration_unit, contract_types_list, @@ -148,6 +159,7 @@ const ScreenSmall = observer(({ is_trade_enabled }) => { const collapsible_trade_params_props = { is_accumulator, is_multiplier, + is_turbos, is_vanilla, form_components, has_take_profit, diff --git a/packages/trader/src/Modules/Trading/Containers/Multiplier/risk-management-dialog.jsx b/packages/trader/src/Modules/Trading/Containers/Multiplier/risk-management-dialog.jsx index 7cb12004a094..fb08c577353f 100644 --- a/packages/trader/src/Modules/Trading/Containers/Multiplier/risk-management-dialog.jsx +++ b/packages/trader/src/Modules/Trading/Containers/Multiplier/risk-management-dialog.jsx @@ -11,6 +11,7 @@ import CancelDeal from 'Modules/Trading/Components/Elements/Multiplier/cancel-de const RiskManagementDialog = observer(({ is_open, onClose, toggleDialog }) => { const { + is_turbos, take_profit, has_take_profit, has_stop_loss, @@ -103,13 +104,15 @@ const RiskManagementDialog = observer(({ is_open, onClose, toggleDialog }) => { onChangeMultiple={onChangeMultipleLocal} validation_errors={validation_errors} /> - + {!is_turbos && ( + + )} {should_show_deal_cancellation && ( { } = useStore(); const list = getAvailableContractTypes(contract_types_list, unsupported_contract_types_list); - const digits_message = localize('Last digit stats for latest 1000 ticks for {{ underlying_name }}', { underlying_name: getMarketNamesMap()[symbol.toUpperCase()], }); diff --git a/packages/trader/src/Modules/Trading/Containers/purchase.jsx b/packages/trader/src/Modules/Trading/Containers/purchase.jsx index 8b434f09da6e..bbbf1d2aee09 100644 --- a/packages/trader/src/Modules/Trading/Containers/purchase.jsx +++ b/packages/trader/src/Modules/Trading/Containers/purchase.jsx @@ -1,12 +1,11 @@ import React from 'react'; -import { isAccumulatorContract, isEmptyObject, isMobile } from '@deriv/shared'; +import { isAccumulatorContract, isEmptyObject } from '@deriv/shared'; import { localize } from '@deriv/translations'; import PurchaseButtonsOverlay from 'Modules/Trading/Components/Elements/purchase-buttons-overlay.jsx'; import PurchaseFieldset from 'Modules/Trading/Components/Elements/purchase-fieldset.jsx'; import { getContractTypePosition } from 'Constants/contract'; import { useTraderStore } from 'Stores/useTraderStores'; import { observer, useStore } from '@deriv/stores'; -import ContractInfo from 'Modules/Trading/Components/Form/Purchase/contract-info.jsx'; const Purchase = observer(({ is_market_closed }) => { const { @@ -22,6 +21,7 @@ const Purchase = observer(({ is_market_closed }) => { growth_rate, has_cancellation, is_purchase_enabled, + is_turbos, is_vanilla, onPurchase: onClickPurchase, onHoverPurchase, @@ -51,45 +51,32 @@ const Purchase = observer(({ is_market_closed }) => { const is_proposal_error = is_multiplier || (is_accumulator && !is_mobile) ? info.has_error && !!info.message : info.has_error; const purchase_fieldset = ( -
- {is_vanilla && isMobile() && ( - - )} - -
+ ); if (!is_vanilla) { diff --git a/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx b/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx index a1c7cfe1f159..201de7955531 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx +++ b/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx @@ -174,7 +174,7 @@ const TradeParamsMobile = observer( h_duration, d_duration, }) => { - const { basis_list, basis, is_vanilla, expiry_epoch } = useTraderStore(); + const { basis_list, basis, expiry_epoch, is_turbos, is_vanilla } = useTraderStore(); const getDurationText = () => { const duration = duration_units_list.find(d => d.value === duration_unit); return `${duration_value} ${ @@ -208,7 +208,7 @@ const TradeParamsMobile = observer( return (
- {is_vanilla ? localize('Stake') : localize('Amount')} + {is_turbos || is_vanilla ? localize('Stake') : localize('Amount')}
{ const isVisible = component_key => { return form_components.includes(component_key); }; + return ( {isVisible('duration') && } {isVisible('barrier') && } {isVisible('last_digit') && } {isVisible('accumulator') && } -
- {isVisible('vanilla_trade_type') && } - {isVisible('strike') && } - {isVisible('amount') && } -
+ {(isVisible('trade_type_tabs') || isVisible('strike') || isVisible('barrier_selector')) && ( +
+ {isVisible('trade_type_tabs') && } + {isVisible('strike') && } + {isVisible('barrier_selector') && } +
+ )} + {isVisible('amount') && } {isVisible('take_profit') && } {isVisible('stop_loss') && } {isVisible('cancellation') && } diff --git a/packages/trader/src/Modules/Trading/Containers/trade.jsx b/packages/trader/src/Modules/Trading/Containers/trade.jsx index 5a18ce1abc37..a65dcbddf672 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade.jsx +++ b/packages/trader/src/Modules/Trading/Containers/trade.jsx @@ -49,6 +49,7 @@ const Trade = observer(() => { symbol, is_synthetics_available, is_synthetics_trading_market_available, + is_turbos, is_vanilla, } = useTraderStore(); const { @@ -149,12 +150,17 @@ const Trade = observer(() => { ); const form_wrapper_class = isMobile() ? '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 (
@@ -167,7 +173,7 @@ const Trade = observer(() => { id='chart_container' className='chart-container' is_disabled={isDesktop()} - height_offset={is_accumulator ? '295px' : '259px'} + height_offset={chart_height_offset} > Accumulators: 'IcCatAccumulator', Options: 'IcCatOptions', Multipliers: 'IcCatMultiplier', + Turbos: 'IcCatTurbos', } as const); /** diff --git a/packages/trader/src/Stores/Modules/SmartChart/Constants/barriers.js b/packages/trader/src/Stores/Modules/SmartChart/Constants/barriers.js deleted file mode 100644 index 26bef1e88d04..000000000000 --- a/packages/trader/src/Stores/Modules/SmartChart/Constants/barriers.js +++ /dev/null @@ -1,37 +0,0 @@ -export const CONTRACT_SHADES = { - CALL: 'ABOVE', - PUT: 'BELOW', - CALLE: 'ABOVE', - PUTE: 'BELOW', - EXPIRYRANGE: 'BETWEEN', - EXPIRYMISS: 'OUTSIDE', - RANGE: 'BETWEEN', - UPORDOWN: 'OUTSIDE', - ONETOUCH: 'NONE_SINGLE', // no shade - NOTOUCH: 'NONE_SINGLE', // no shade - ASIANU: 'ABOVE', - ASIAND: 'BELOW', - MULTUP: 'ABOVE', - MULTDOWN: 'BELOW', - ACCU: 'NONE_DOUBLE', -}; - -// Default non-shade according to number of barriers -export const DEFAULT_SHADES = { - 1: 'NONE_SINGLE', - 2: 'NONE_DOUBLE', -}; - -export const BARRIER_COLORS = { - GREEN: '#4bb4b3', - RED: '#ec3f3f', - ORANGE: '#ff6444', - GRAY: '#999999', - DARK_GRAY: '#6E6E6E', -}; - -export const BARRIER_LINE_STYLES = { - DASHED: 'dashed', - DOTTED: 'dotted', - SOLID: 'solid', -}; diff --git a/packages/trader/src/Stores/Modules/SmartChart/Helpers/barriers.js b/packages/trader/src/Stores/Modules/SmartChart/Helpers/barriers.js index f26a39e3146f..ca001aee0023 100644 --- a/packages/trader/src/Stores/Modules/SmartChart/Helpers/barriers.js +++ b/packages/trader/src/Stores/Modules/SmartChart/Helpers/barriers.js @@ -1,6 +1,5 @@ import { toJS } from 'mobx'; -import { isEmptyObject } from '@deriv/shared'; -import { CONTRACT_SHADES } from '../Constants/barriers'; +import { isEmptyObject, CONTRACT_SHADES } from '@deriv/shared'; export const isBarrierSupported = contract_type => contract_type in CONTRACT_SHADES; diff --git a/packages/trader/src/Stores/Modules/SmartChart/chart-barrier-store.js b/packages/trader/src/Stores/Modules/SmartChart/chart-barrier-store.js index 12c8b6f950fd..718086c6ee9c 100644 --- a/packages/trader/src/Stores/Modules/SmartChart/chart-barrier-store.js +++ b/packages/trader/src/Stores/Modules/SmartChart/chart-barrier-store.js @@ -1,5 +1,5 @@ import { action, computed, observable, makeObservable } from 'mobx'; -import { BARRIER_COLORS, BARRIER_LINE_STYLES, CONTRACT_SHADES, DEFAULT_SHADES } from './Constants/barriers'; +import { BARRIER_COLORS, BARRIER_LINE_STYLES, CONTRACT_SHADES, DEFAULT_SHADES } from '@deriv/shared'; import { barriersToString } from './Helpers/barriers'; export class ChartBarrierStore { diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/barrier-utils.spec.ts b/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/barrier-utils.spec.ts new file mode 100644 index 000000000000..0c08a6fec99f --- /dev/null +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/__tests__/barrier-utils.spec.ts @@ -0,0 +1,13 @@ +import { getHoveredColor } from '../barrier-utils'; + +describe('getHoveredColor', () => { + it('should return red color (#ec3f3f) if passed value is TURBOSSHORT', () => { + expect(getHoveredColor('TURBOSSHORT')).toEqual('#ec3f3f'); + }); + it('should return green color (#4bb4b3) if passed value is TURBOSLONG', () => { + expect(getHoveredColor('TURBOSLONG')).toEqual('#4bb4b3'); + }); + it('should return gray color (#999999) if passed value not TURBOSLONG or TURBOSSHORT', () => { + expect(getHoveredColor('TESTTYPE')).toEqual('#999999'); + }); +}); diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/barrier-utils.ts b/packages/trader/src/Stores/Modules/Trading/Helpers/barrier-utils.ts new file mode 100644 index 000000000000..8d3df7d38019 --- /dev/null +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/barrier-utils.ts @@ -0,0 +1,12 @@ +import { BARRIER_COLORS } from '@deriv/shared'; + +export const getHoveredColor = (type: string): string => { + switch (type) { + case 'TURBOSSHORT': + return BARRIER_COLORS.RED; + case 'TURBOSLONG': + return BARRIER_COLORS.GREEN; + default: + return BARRIER_COLORS.GRAY; + } +}; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js b/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js index 81638cd7e01c..99b04c99f481 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/contract-type.js @@ -14,6 +14,7 @@ import { unsupported_contract_types_list, getContractCategoriesConfig, getContractTypesConfig, + getContractSubtype, getLocalizedBasis, } from '@deriv/shared'; import ServerTime from '_common/base/server_time'; @@ -71,6 +72,7 @@ export const ContractType = (() => { config.durations = !config.hide_duration && buildDurationConfig(contract, config.durations); config.trade_types = buildTradeTypesConfig(contract, config.trade_types); config.barriers = buildBarriersConfig(contract, config.barriers); + config.barrier_choices = contract.barrier_choices; config.forward_starting_dates = buildForwardStartingConfig(contract, config.forward_starting_dates); config.growth_rate_range = contract.growth_rate_range; config.multiplier_range = contract.multiplier_range; @@ -109,22 +111,31 @@ export const ContractType = (() => { start_date, cancellation_duration, symbol, + short_barriers, + long_barriers, } = store; if (!contract_type) return {}; + let stored_barriers_data = {}; + if (getContractSubtype(contract_type) === 'Short') { + stored_barriers_data = short_barriers; + } else if (getContractSubtype(contract_type) === 'Long') { + stored_barriers_data = long_barriers; + } + const form_components = getComponents(contract_type); const obj_basis = getBasis(contract_type, basis); const obj_trade_types = getTradeTypes(contract_type); const obj_start_dates = getStartDates(contract_type, start_date); const obj_start_type = getStartType(obj_start_dates.start_date); - const obj_barrier = getBarriers(contract_type, contract_expiry_type); + const obj_barrier = getBarriers(contract_type, contract_expiry_type, stored_barriers_data.barrier); const obj_duration_unit = getDurationUnit(duration_unit, contract_type, obj_start_type.contract_start_type); const obj_duration_units_list = getDurationUnitsList(contract_type, obj_start_type.contract_start_type); const obj_duration_units_min_max = getDurationMinMax(contract_type, obj_start_type.contract_start_type); - const obj_accumulator_range_list = getAccumulatorRange(contract_type); + const obj_barrier_choices = getBarrierChoices(contract_type, stored_barriers_data.barrier_choices); const obj_multiplier_range_list = getMultiplierRange(contract_type, multiplier); const obj_cancellation = getCancellation(contract_type, cancellation_duration, symbol); const obj_expiry_type = getExpiryType(obj_duration_units_list, expiry_type); @@ -142,6 +153,7 @@ export const ContractType = (() => { ...obj_duration_units_min_max, ...obj_expiry_type, ...obj_accumulator_range_list, + ...obj_barrier_choices, ...obj_multiplier_range_list, ...obj_cancellation, ...obj_equal, @@ -474,14 +486,14 @@ export const ContractType = (() => { trade_types: getPropertyValue(available_contract_types, [contract_type, 'config', 'trade_types']), }); - const getBarriers = (contract_type, expiry_type) => { + const getBarriers = (contract_type, expiry_type, stored_barrier_value) => { const barriers = getPropertyValue(available_contract_types, [contract_type, 'config', 'barriers']) || {}; const barrier_values = barriers[expiry_type] || {}; const barrier_1 = barrier_values.barrier || barrier_values.high_barrier || ''; const barrier_2 = barrier_values.low_barrier || ''; return { barrier_count: barriers.count || 0, - barrier_1: barrier_1.toString(), + barrier_1: stored_barrier_value || barrier_1.toString(), barrier_2: barrier_2.toString(), }; }; @@ -502,6 +514,12 @@ export const ContractType = (() => { getPropertyValue(available_contract_types, [contract_type, 'config', 'growth_rate_range']) || [], }); + const getBarrierChoices = (contract_type, stored_barrier_choices = []) => ({ + barrier_choices: stored_barrier_choices.length + ? stored_barrier_choices + : getPropertyValue(available_contract_types, [contract_type, 'config', 'barrier_choices']) || [], + }); + const getMultiplierRange = (contract_type, multiplier) => { const arr_multiplier = getPropertyValue(available_contract_types, [contract_type, 'config', 'multiplier_range']) || []; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/limit-orders.js b/packages/trader/src/Stores/Modules/Trading/Helpers/limit-orders.js index 069a7607b69e..201f45f199f0 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/limit-orders.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/limit-orders.js @@ -1,5 +1,4 @@ -import { isMultiplierContract } from '@deriv/shared'; -import { BARRIER_COLORS, BARRIER_LINE_STYLES } from '../../SmartChart/Constants/barriers'; +import { isMultiplierContract, BARRIER_COLORS, BARRIER_LINE_STYLES } from '@deriv/shared'; import { ChartBarrierStore } from '../../SmartChart/chart-barrier-store'; import { removeBarrier } from '../../SmartChart/Helpers/barriers'; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.js b/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.js index 858209efa645..eb22fb34ed43 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.js +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.js @@ -1,4 +1,12 @@ -import { convertToUnix, getDecimalPlaces, getPropertyValue, isAccumulatorContract, toMoment } from '@deriv/shared'; +import { + convertToUnix, + getDecimalPlaces, + getLocalizedBasis, + getPropertyValue, + isAccumulatorContract, + isTurbosContract, + toMoment, +} from '@deriv/shared'; const isVisible = elem => !(!elem || (elem.offsetWidth === 0 && elem.offsetHeight === 0)); @@ -25,9 +33,10 @@ export const getProposalInfo = (store, response, obj_prev_contract_basis) => { const stake = proposal.display_value; const basis_list = store.basis_list; - const contract_basis = store.is_vanilla - ? { text: 'Payout', value: 'display_number_of_contracts' } - : basis_list.find(o => o.value !== store.basis) || {}; + const contract_basis = + store.is_vanilla || store.is_turbos + ? { text: getLocalizedBasis().payout_per_point, value: 'display_number_of_contracts' } + : basis_list.find(o => o.value !== store.basis) || {}; const is_stake = contract_basis.value === 'stake'; const price = is_stake ? stake : proposal[contract_basis.value]; @@ -111,6 +120,7 @@ const createProposalRequestForContract = (store, type_of_contract) => { const obj_accumulator = {}; const obj_expiry = {}; const obj_multiplier = {}; + let limit_order; if (store.expiry_type === 'endtime') { const expiry_date = toMoment(store.expiry_date); @@ -125,6 +135,10 @@ const createProposalRequestForContract = (store, type_of_contract) => { setProposalAccumulator(store, obj_accumulator); } + if (isTurbosContract(store.contract_type) && store.has_take_profit && store.take_profit) { + limit_order = { take_profit: +store.take_profit || 0 }; + } + return { proposal: 1, subscribe: 1, @@ -147,5 +161,6 @@ const createProposalRequestForContract = (store, type_of_contract) => { ...(store.barrier_count === 2 && !isAccumulatorContract(type_of_contract) && { barrier2: store.barrier_2 }), ...obj_accumulator, ...obj_multiplier, + limit_order, }; }; diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.js b/packages/trader/src/Stores/Modules/Trading/trade-store.js index ba1ae8af25f4..7ada3c3902f2 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.js +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.js @@ -10,12 +10,14 @@ import { getMinPayout, getPlatformSettings, getPropertyValue, + getContractSubtype, isBarrierSupported, isCryptocurrency, isDesktop, isEmptyObject, isMarketClosed, isMobile, + isTurbosContract, pickDefaultSymbol, removeBarrier, resetEndTimeOnVolatilityIndices, @@ -25,6 +27,8 @@ import { formatMoney, getCurrencyDisplayCode, unsupported_contract_types_list, + BARRIER_COLORS, + BARRIER_LINE_STYLES, } from '@deriv/shared'; import { localize } from '@deriv/translations'; import { getValidationRules, getMultiplierValidationRules } from 'Stores/Modules/Trading/Constants/validation-rules'; @@ -37,7 +41,7 @@ import { getUpdatedTicksHistoryStats } from './Helpers/accumulator'; import { processTradeParams } from './Helpers/process'; import { action, computed, makeObservable, observable, override, reaction, runInAction, toJS, when } from 'mobx'; import { createProposalRequests, getProposalErrorField, getProposalInfo } from './Helpers/proposal'; -import { BARRIER_COLORS } from '../SmartChart/Constants/barriers'; +import { getHoveredColor } from './Helpers/barrier-utils'; import BaseStore from '../../base-store'; import { ChartBarrierStore } from '../SmartChart/chart-barrier-store'; import debounce from 'lodash.debounce'; @@ -74,7 +78,7 @@ export default class TradeStore extends BaseStore { basis = ''; basis_list = []; currency = ''; - stake_boundary = { VANILLALONGCALL: {}, VANILLALONGPUT: {} }; + stake_boundary = {}; // Duration duration = 5; @@ -90,9 +94,10 @@ export default class TradeStore extends BaseStore { barrier_1 = ''; barrier_2 = ''; barrier_count = 0; - main_barrier = null; barriers = []; - strike_price_choices = []; + hovered_barrier = ''; + main_barrier = null; + barrier_choices = []; // Start Time start_date = Number(0); // Number(0) refers to 'now' @@ -146,6 +151,10 @@ export default class TradeStore extends BaseStore { cancellation_duration = '60m'; cancellation_range_list = []; + // Turbos trade params + long_barriers = {}; + short_barriers = {}; + // Vanilla trade params vanilla_trade_type = 'VANILLALONGCALL'; @@ -182,6 +191,9 @@ export default class TradeStore extends BaseStore { 'has_take_profit', 'has_stop_loss', 'has_cancellation', + 'hovered_barrier', + 'short_barriers', + 'long_barriers', 'is_equal', 'last_digit', 'multiplier', @@ -206,6 +218,7 @@ export default class TradeStore extends BaseStore { barrier_1: observable, barrier_2: observable, barrier_count: observable, + barrier_choices: observable, barriers: observable, basis_list: observable, basis: observable, @@ -233,6 +246,7 @@ export default class TradeStore extends BaseStore { has_equals_only: observable, has_stop_loss: observable, has_take_profit: observable, + hovered_barrier: observable, hovered_contract_type: observable, is_accumulator: computed, is_chart_loading: observable, @@ -243,7 +257,9 @@ export default class TradeStore extends BaseStore { is_trade_component_mounted: observable, is_trade_enabled: observable, is_trade_params_expanded: observable, + is_turbos: computed, last_digit: observable, + long_barriers: observable, main_barrier: observable, market_close_times: observable, market_open_times: observable, @@ -254,8 +270,10 @@ export default class TradeStore extends BaseStore { previous_symbol: observable, proposal_info: observable.ref, purchase_info: observable.ref, + setHoveredBarrier: action.bound, sessions: observable, setDefaultGrowthRate: action.bound, + short_barriers: observable, should_show_active_symbols_loading: observable, should_skip_prepost_lifecycle: observable, stake_boundary: observable, @@ -264,7 +282,6 @@ export default class TradeStore extends BaseStore { start_time: observable, stop_loss: observable, stop_out: observable, - strike_price_choices: observable, symbol: observable, take_profit: observable, tick_size_barrier: observable, @@ -275,10 +292,12 @@ export default class TradeStore extends BaseStore { barriers_flattened: computed, changeDurationValidationRules: action.bound, chartStateChange: action.bound, + clearContractPurchaseToastBox: action.bound, clearContracts: action.bound, clearLimitOrderBarriers: action.bound, clearPurchaseInfo: action.bound, clientInitListener: action.bound, + contract_purchase_toast_box: observable, enablePurchase: action.bound, exportLayout: action.bound, forgetAllProposal: action.bound, @@ -314,7 +333,9 @@ export default class TradeStore extends BaseStore { resetPreviousSymbol: action.bound, setActiveSymbols: action.bound, setAllowEqual: action.bound, + setBarrierChoices: action.bound, setChartStatus: action.bound, + setContractPurchaseToastbox: action.bound, setContractTypes: action.bound, setDefaultSymbol: action.bound, setIsTradeParamsExpanded: action.bound, @@ -324,7 +345,6 @@ export default class TradeStore extends BaseStore { setPurchaseSpotBarrier: action.bound, setSkipPrePostLifecycle: action.bound, setStakeBoundary: action.bound, - setStrikeChoices: action.bound, setTradeStatus: action.bound, show_digits_stats: computed, themeChangeListener: action.bound, @@ -332,9 +352,7 @@ 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, + vanilla_trade_type: observable, }); // Adds intercept to change min_max value of duration validation @@ -382,7 +400,7 @@ export default class TradeStore extends BaseStore { () => [this.contract_type], () => { this.root_store.portfolio.setContractType(this.contract_type); - if (this.is_multiplier || this.is_accumulator) { + if (this.is_accumulator || this.is_multiplier || this.is_turbos) { // when switching back to Multiplier contract, re-apply Stop loss / Take profit validation rules Object.assign(this.validation_rules, getMultiplierValidationRules()); } else { @@ -633,6 +651,10 @@ export default class TradeStore extends BaseStore { this.root_store.common.setSelectedContractType(this.contract_type); } + setHoveredBarrier(hovered_value) { + this.hovered_barrier = hovered_value; + } + setPreviousSymbol(symbol) { if (this.previous_symbol !== symbol) this.previous_symbol = symbol; } @@ -743,15 +765,22 @@ export default class TradeStore extends BaseStore { if (!proposal_info) { return; } - const { contract_type, barrier, high_barrier, low_barrier } = proposal_info; + const { barrier, contract_type, high_barrier, low_barrier } = proposal_info; if (isBarrierSupported(contract_type)) { const color = this.root_store.ui.is_dark_mode_on ? BARRIER_COLORS.DARK_GRAY : BARRIER_COLORS.GRAY; + // create barrier only when it's available in response - this.main_barrier = new ChartBarrierStore(barrier || high_barrier, low_barrier, this.onChartBarrierChange, { - color, - not_draggable: this.is_vanilla, - }); + this.main_barrier = new ChartBarrierStore( + this.hovered_barrier || barrier || high_barrier, + low_barrier, + this.onChartBarrierChange, + { + color: this.hovered_barrier ? getHoveredColor(contract_type) : color, + line_style: this.hovered_barrier && BARRIER_LINE_STYLES.DASHED, + not_draggable: this.is_turbos || this.is_vanilla, + } + ); // this.main_barrier.updateBarrierShade(true, contract_type); } else { this.main_barrier = null; @@ -1178,17 +1207,7 @@ export default class TradeStore extends BaseStore { if (response.error) { const error_id = getProposalErrorField(response); if (error_id) { - if (this.is_vanilla) { - /** - * This if-block ensures only the particular trade type's error message is selected - * even though 2 proposal calls are made - */ - if (this.vanilla_trade_type === contract_type) { - this.setValidationErrorMessages(error_id, [response.error.message]); - } - } else { - this.setValidationErrorMessages(error_id, [response.error.message]); - } + this.setValidationErrorMessages(error_id, [response.error.message]); } // Commission for multipliers is normally set from proposal response. // But when we change the multiplier and if it is invalid, we don't get the proposal response to set the commission. We only get error message. @@ -1202,17 +1221,30 @@ export default class TradeStore extends BaseStore { } if (this.is_accumulator) this.resetAccumulatorData(); + // Sometimes when we navigate fast, `forget_all` proposal is called immediately after proposal subscription calls. + // But, in the BE, `forget_all` proposal call is processed before the proposal subscriptions are registered. In this case, `forget_all` proposal doesn't forget the new subscriptions. + // So when we send new proposal subscription requests, we get `AlreadySubscribed` error. + // If we get an error message with code `AlreadySubscribed`, `forget_all` proposal will be called and all the existing subscriptions will be marked as complete in `deriv-api` and will subscribe to new proposals + if (response.error.code === 'AlreadySubscribed') { + this.refresh(); + + if (this.is_trade_component_mounted) { + this.debouncedProposal(); + } + return; + } + // Sometimes the initial barrier doesn't match with current barrier choices received from API. // When this happens we want to populate the list of barrier choices to choose from since the value cannot be specified manually - if (this.is_vanilla) { + if ((this.is_turbos || this.is_vanilla) && response.error.details?.barrier_choices) { const { barrier_choices, max_stake, min_stake } = response.error.details; this.setStakeBoundary(contract_type, min_stake, max_stake); - this.setStrikeChoices(barrier_choices); - if (!this.strike_price_choices.includes(this.barrier_1)) { + this.setBarrierChoices(barrier_choices); + if (!this.barrier_choices.includes(this.barrier_1)) { // Since on change of duration `proposal` API call is made which returns a new set of barrier values. // The new list is set and the mid value is assigned - const index = Math.floor(this.strike_price_choices.length / 2); - this.barrier_1 = this.strike_price_choices[index]; + const index = Math.floor(this.barrier_choices.length / 2); + this.barrier_1 = this.is_vanilla ? this.barrier_choices[index] : this.barrier_choices[0]; this.onChange({ target: { name: 'barrier_1', @@ -1221,24 +1253,11 @@ export default class TradeStore extends BaseStore { }); } } - - // Sometimes when we navigate fast, `forget_all` proposal is called immediately after proposal subscription calls. - // But, in the BE, `forget_all` proposal call is processed before the proposal subscriptions are registered. In this case, `forget_all` proposal doesn't forget the new subscriptions. - // So when we send new proposal subscription requests, we get `AlreadySubscribed` error. - // If we get an error message with code `AlreadySubscribed`, `forget_all` proposal will be called and all the existing subscriptions will be marked as complete in `deriv-api` and will subscribe to new proposals - if (response.error.code === 'AlreadySubscribed') { - this.refresh(); - - if (this.is_trade_component_mounted) { - this.debouncedProposal(); - } - return; - } } else { this.validateAllProperties(); - if (this.is_vanilla) { + if (this.is_turbos || this.is_vanilla) { const { max_stake, min_stake, barrier_choices } = response.proposal; - this.setStrikeChoices(barrier_choices); + this.setBarrierChoices(barrier_choices); this.setStakeBoundary(contract_type, min_stake, max_stake); } } @@ -1550,6 +1569,10 @@ export default class TradeStore extends BaseStore { return this.contract_type === 'multiplier'; } + get is_turbos() { + return isTurbosContract(this.contract_type); + } + get is_vanilla() { return this.contract_type === 'vanilla'; } @@ -1582,11 +1605,19 @@ export default class TradeStore extends BaseStore { return findFirstOpenMarket(active_symbols, markets_to_search); } - setStrikeChoices(strike_prices) { - this.strike_price_choices = strike_prices ?? []; + setBarrierChoices(barrier_choices) { + this.barrier_choices = barrier_choices ?? []; + if (this.is_turbos) { + const stored_barriers_data = { barrier: this.barrier_1, barrier_choices }; + if (getContractSubtype(this.contract_type) === 'Long') { + this.long_barriers = stored_barriers_data; + } else { + this.short_barriers = stored_barriers_data; + } + } } setStakeBoundary(type, min_stake, max_stake) { - this.stake_boundary[type] = { min_stake, max_stake }; + if (min_stake && max_stake) this.stake_boundary[type] = { min_stake, max_stake }; } } diff --git a/packages/trader/src/sass/app/_common/components/contract-type-info.scss b/packages/trader/src/sass/app/_common/components/contract-type-info.scss index ad50887418a4..6435e9fd07b3 100644 --- a/packages/trader/src/sass/app/_common/components/contract-type-info.scss +++ b/packages/trader/src/sass/app/_common/components/contract-type-info.scss @@ -101,11 +101,16 @@ margin-bottom: 1.6rem; } /* postcss-bem-linter: ignore */ + h6, + span { + @include typeface(--small-left-bold-black, none); + } p, ul li { @include typeface(--small-left-normal-black, none); } h2, + h6, p, ul li { line-height: 1.5; @@ -121,11 +126,13 @@ margin-bottom: 1.6rem; @include mobile { + display: flex; + align-items: center; + justify-content: center; width: 32.8rem; height: 16.4rem; background-color: var(--general-section-1); svg { - width: 100%; height: 100%; } } diff --git a/packages/trader/src/sass/app/_common/components/contract-type-list.scss b/packages/trader/src/sass/app/_common/components/contract-type-list.scss index f39f7b78992c..1b526090667a 100644 --- a/packages/trader/src/sass/app/_common/components/contract-type-list.scss +++ b/packages/trader/src/sass/app/_common/components/contract-type-list.scss @@ -51,6 +51,9 @@ &__icon { margin-left: auto; + @include mobile { + display: block; + } @include mobile { display: block; diff --git a/packages/trader/src/sass/app/_common/components/contract-type-widget.scss b/packages/trader/src/sass/app/_common/components/contract-type-widget.scss index 93544bb0d903..21140788b286 100644 --- a/packages/trader/src/sass/app/_common/components/contract-type-widget.scss +++ b/packages/trader/src/sass/app/_common/components/contract-type-widget.scss @@ -100,11 +100,16 @@ &--multiplier { margin-bottom: 0.6rem; } + &--turbosshort, + &--turboslong { + top: 0.2rem; + margin-right: 0.7rem; + } &--vanilla { margin-right: 0.4rem; } .contract-type-widget__display { - padding: 0.8rem; + padding: 0.8rem 2rem 0.8rem 0.8rem; } } } diff --git a/packages/trader/src/sass/app/_common/components/purchase-button.scss b/packages/trader/src/sass/app/_common/components/purchase-button.scss index 6ccb8da7b588..6216a02fff9c 100644 --- a/packages/trader/src/sass/app/_common/components/purchase-button.scss +++ b/packages/trader/src/sass/app/_common/components/purchase-button.scss @@ -314,7 +314,6 @@ } &--multiplier, &--accumulator { - margin: 0; .btn-purchase__info--left { width: 35%; flex: none; @@ -397,4 +396,32 @@ } } } + &--turbos { + .btn-purchase { + &__info--left { + width: 70%; + } + &__effect-detail { + &--arrow { + left: 12rem; + } + } + @include mobile { + &__top { + align-items: center; + width: auto; + } + } + } + &.btn-purchase--swoosh { + .btn-purchase { + &__effect-detail { + transform: scale3d(15, 1, 1); + &--arrow { + transform: translate3d(12rem, 0, 0) rotate(45deg); + } + } + } + } + } } diff --git a/packages/trader/src/sass/app/_common/layout/trader-layouts.scss b/packages/trader/src/sass/app/_common/layout/trader-layouts.scss index a5d457f2739f..34ed50d41e42 100644 --- a/packages/trader/src/sass/app/_common/layout/trader-layouts.scss +++ b/packages/trader/src/sass/app/_common/layout/trader-layouts.scss @@ -404,7 +404,8 @@ $FLOATING_HEADER_HEIGHT: 41px; position: relative; z-index: 2; - .trade-container--accumulators & { + .trade-container--accumulators &, + .trade-container--turbos & { height: 24.8rem; } &__content-loader { diff --git a/packages/trader/src/sass/app/modules/trading-mobile.scss b/packages/trader/src/sass/app/modules/trading-mobile.scss index ffa70084d1fc..acf3c0c67947 100644 --- a/packages/trader/src/sass/app/modules/trading-mobile.scss +++ b/packages/trader/src/sass/app/modules/trading-mobile.scss @@ -242,7 +242,7 @@ } } } - &__strike { + &__stake-container { display: flex; flex-direction: column; .trade-container__stake-field { diff --git a/packages/trader/src/sass/app/modules/trading.scss b/packages/trader/src/sass/app/modules/trading.scss index acd799b737c2..e3245432923d 100644 --- a/packages/trader/src/sass/app/modules/trading.scss +++ b/packages/trader/src/sass/app/modules/trading.scss @@ -297,6 +297,12 @@ opacity: 0; } } + &--turbos { + display: flex; + justify-self: left; + align-items: flex-start; + flex-direction: column; + } &-value { font-size: 1.4rem; font-weight: 700; @@ -321,22 +327,16 @@ color: var(--text-colored-background); } } - &-strike { - @include mobile { - color: var(--text-less-prominent); - font-size: var(--text-size-xxs); - display: flex; - gap: 0.8rem; - justify-content: space-between; - padding: 1rem 0; - } - } &-currency { margin-left: 4px; margin-right: 1px; display: inline-block; position: relative; font-weight: bold; + + &--payout-per-point { + margin-left: 0; + } } &-movement { margin-left: 4px; @@ -346,6 +346,10 @@ position: relative; } } + &--turbos { + align-items: center; + margin-bottom: 0.8rem; + } } &__price-info { &-currency { @@ -353,41 +357,69 @@ font-size: 0.9rem; } } - &-modal { - @include mobile { - &--vanilla { - max-width: calc(min(33rem, 85vw)); - } - } - } } &__barriers { display: flex; flex-direction: column; + position: relative; + bottom: 0.1rem; + + .trade-container__fieldset-info--left { + transform: translateX(0.7rem); + } + &__wrapper { + display: flex; + justify-content: center; + align-items: center; + } &:first-child { - padding-right: 8px; + padding-right: 0.8rem; + } + &-value { + display: flex; + justify-content: center; + align-items: center; + margin-top: 0.4rem; + background: var(--fill-normal); + border-radius: 0.4rem; + width: 100%; + height: 3.2rem; + cursor: pointer; + position: relative; + &--arrow-right { + position: absolute; + right: 0.8rem; + transform: rotate(180deg); + } } &-input { - padding-left: 3px; + padding-left: 0.3rem; } &-single { width: 100%; } + &-tooltip:first-child { + margin-bottom: 0.8rem; + } + &-spot { + margin-top: 0.8rem; + padding: 0rem 0.8rem; + } &-multiple { &-input { - padding-left: 25px; - padding-right: 9px; + padding-left: 2.5rem; + padding-right: 0.9rem; text-align: center; } &:first-of-type { - padding-right: 8px; + padding-right: 0.8rem; } } &--up, &--down { position: absolute; - margin-top: 15px; + margin-top: 1.5rem; } &--up { right: 86.5%; @@ -396,6 +428,90 @@ right: 39%; } } + &__barriers-table { + position: absolute; + top: 0; + left: 0; + z-index: 100; + width: 100%; + height: calc(100vh - 4.8rem - 3.6rem - 1.6rem - 1.6rem); + display: flex; + flex-direction: column; + transition: opacity 0.25s cubic-bezier(0.25, 0.1, 0.1, 0.25); + &--enter, + &--exit { + opacity: 0; + pointer-events: none; + } + &--enter-done { + opacity: 1; + pointer-events: auto; + } + &__header { + height: $header-height; + display: flex; + flex-direction: row; + align-items: center; + padding: 0 1rem; + border-bottom: 0.1rem solid var(--general-hover); + @include mobile { + height: 4.5rem; + padding: 0 1.6rem; + border-bottom: 0.1rem solid var(--general-section-1); + } + &-wrapper { + display: flex; + gap: 0.8rem; + } + } + &__icon-close { + display: inline-block; + margin-left: auto; + cursor: pointer; + svg { + @extend %inline-icon; + height: 1.6em; + width: 1.6em; + } + } + &__text { + padding: 1.6rem 1.6rem 0; + @include mobile { + padding: 1.6rem 0 0.8rem 1.6rem; + } + } + &__list { + overflow-y: scroll; + flex: 1 1 auto; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0 0.6rem; + } + &__item { + padding: 1rem 1.4rem; + border-radius: 0.6rem; + height: 3.8rem; + cursor: pointer; + + &:hover:not(&--selected) { + background-color: var(--general-hover); + font-weight: var(--text-weight-bold); + } + &--selected, + &:active, + &:focus { + background-color: var(--general-active); + font-weight: var(--text-weight-bold); + } + @include mobile { + display: flex; + align-items: center; + height: 4.8rem; + padding: 1.5rem 1.4rem; + } + } + } &__allow-equals { /* postcss-bem-linter: ignore */ &__label { @@ -506,6 +622,25 @@ align-self: flex-end; } } + &__trade { + &-type-tabs { + border-radius: $BORDER_RADIUS; + padding: 0.8rem; + background-color: var(--general-section-1); + border-color: var(--general-section-1); + color: var(--text-general); + @include mobile { + width: 50%; + padding: 0; + margin-bottom: 0rem; + margin-top: 0.6rem; + &--button { + height: 4rem; + bottom: 0.5rem; + } + } + } + } &__deal-cancellation-popover { width: 28rem; } @@ -610,6 +745,9 @@ margin-top: 0.4rem; display: flex; justify-content: space-between; + @include desktop { + height: 2.8rem; + } &--min { display: flex; @@ -652,11 +790,11 @@ /** @define purchase-container; weak */ .purchase-container, -.purchase-container--accumulator { +.purchase-container__accumulator { position: relative; &__option { - padding: 1.6rem 0.8rem; + padding: 0.8rem; &:not(:only-of-type) { &:nth-last-child(2) { @@ -699,12 +837,15 @@ grid-template-areas: 'a b'; grid-column-gap: 0.5rem; - &--accumulator { + &__accumulator { grid-template-areas: 'a'; .purchase-container__option { margin-bottom: 0 !important; } } + &__turbos { + display: block; + } } .purchase-buttons-overlay { @@ -818,6 +959,30 @@ } } +.payout-per-point { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.6rem; + height: 4.15rem; + + &__label { + position: relative; + bottom: 0.3rem; + &-wrapper { + display: flex; + gap: 0.8rem; + } + } + &__value { + position: relative; + bottom: 0.2rem; + .trade-container__price-info-movement { + top: 0.3rem; + } + } +} + /** @define market-unavailable-modal */ @include mobile { .market-unavailable-modal { @@ -852,17 +1017,7 @@ } } -.strike { - &--info { - color: var(--text-general); - &__value { - font-size: var(--text-size-xxs); - margin-left: unset; - } - &__wrapper { - padding-right: 1rem; - } - } +.price-info { &--value-container { display: flex; } diff --git a/packages/translations/src/i18next/i18next.ts b/packages/translations/src/i18next/i18next.ts index 2c01b1f72a51..809453cd0161 100644 --- a/packages/translations/src/i18next/i18next.ts +++ b/packages/translations/src/i18next/i18next.ts @@ -136,7 +136,7 @@ export const changeLanguage = async (lang: string, cb: (arg0: string) => void) = // component wrapped with i18n export const Localize = withI18n(i18n); -export const localize = (string: string, values?: T) => { +export const localize = (string: string, values?: T): string => { if (!string) return ''; return i18n.t(crc32(string).toString(), { defaultValue: string, ...values });