diff --git a/packages/trader/src/AppV2/Components/Carousel/__tests__/carousel-header.spec.tsx b/packages/trader/src/AppV2/Components/Carousel/__tests__/carousel-header.spec.tsx new file mode 100644 index 000000000000..de197c743b3f --- /dev/null +++ b/packages/trader/src/AppV2/Components/Carousel/__tests__/carousel-header.spec.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CarouselHeader from '../carousel-header'; + +jest.mock('@deriv/quill-icons', () => ({ + ...jest.requireActual('@deriv/quill-icons'), + LabelPairedArrowLeftMdRegularIcon: jest.fn(({ onClick }) => ( + + )), + LabelPairedCircleInfoMdRegularIcon: jest.fn(({ onClick }) => ( + + )), +})); + +const mock_props = { + current_index: 0, + onNextClick: jest.fn(), + onPrevClick: jest.fn(), + title:
Title
, +}; + +describe('CarouselHeader', () => { + it('should render passed title and correct icon for passed index. If user clicks on info icon, onNextClick should be called', () => { + render(); + + expect(screen.getByText('Title')).toBeInTheDocument(); + + const info_icon = screen.getByText('LabelPairedCircleInfoMdRegularIcon'); + expect(info_icon).toBeInTheDocument(); + + expect(mock_props.onNextClick).not.toBeCalled(); + userEvent.click(info_icon); + expect(mock_props.onNextClick).toBeCalled(); + }); + + it('should render correct icon for passed index. If user clicks on arrow icon, onPrevClick should be called', () => { + render(); + + const arrow_icon = screen.getByText('LabelPairedArrowLeftMdRegularIcon'); + expect(arrow_icon).toBeInTheDocument(); + + expect(mock_props.onPrevClick).not.toBeCalled(); + userEvent.click(arrow_icon); + expect(mock_props.onPrevClick).toBeCalled(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/Carousel/carousel-header.tsx b/packages/trader/src/AppV2/Components/Carousel/carousel-header.tsx new file mode 100644 index 000000000000..6c802c0b89b3 --- /dev/null +++ b/packages/trader/src/AppV2/Components/Carousel/carousel-header.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { ActionSheet } from '@deriv-com/quill-ui'; +import { LabelPairedArrowLeftMdRegularIcon, LabelPairedCircleInfoMdRegularIcon } from '@deriv/quill-icons'; + +type TCarouselHeaderProps = { + current_index: number; + onNextClick: () => void; + onPrevClick: () => void; + title?: React.ReactNode; +}; + +const CarouselHeader = ({ current_index, onNextClick, onPrevClick, title }: TCarouselHeaderProps) => ( + + ) : ( + + ) + } + iconPosition={current_index ? 'left' : 'right'} + /> +); + +export default CarouselHeader; diff --git a/packages/trader/src/AppV2/Components/Carousel/carousel.scss b/packages/trader/src/AppV2/Components/Carousel/carousel.scss index c6200c3026d2..12dc2f3c5c8d 100644 --- a/packages/trader/src/AppV2/Components/Carousel/carousel.scss +++ b/packages/trader/src/AppV2/Components/Carousel/carousel.scss @@ -9,3 +9,9 @@ transition: transform 0.24s ease-in-out; } } + +.carousel-controls { + .quill-action-sheet--title--icon { + z-index: 2; + } +} diff --git a/packages/trader/src/AppV2/Components/Carousel/carousel.tsx b/packages/trader/src/AppV2/Components/Carousel/carousel.tsx index 64299084f564..2f6bc4fb8d1c 100644 --- a/packages/trader/src/AppV2/Components/Carousel/carousel.tsx +++ b/packages/trader/src/AppV2/Components/Carousel/carousel.tsx @@ -1,16 +1,13 @@ import React from 'react'; +import CarouselHeader from './carousel-header'; -type THeaderProps = { - current_index: number; - onNextClick: () => void; - onPrevClick: () => void; -}; type TCarousel = { - header: ({ current_index, onNextClick, onPrevClick }: THeaderProps) => JSX.Element; + header: typeof CarouselHeader; pages: { id: number; component: JSX.Element }[]; + title?: React.ReactNode; }; -const Carousel = ({ header, pages }: TCarousel) => { +const Carousel = ({ header, pages, title }: TCarousel) => { const [current_index, setCurrentIndex] = React.useState(0); const HeaderComponent = header; @@ -20,7 +17,12 @@ const Carousel = ({ header, pages }: TCarousel) => { return ( - +
    {pages.map(({ component, id }) => (
  • mediaQueryList); + +describe('TakeProfit', () => { + let default_mock_store: ReturnType; + + beforeEach(() => (default_mock_store = mockStore({}))); + + afterEach(() => jest.clearAllMocks()); + + const mockTakeProfit = () => + render( + + + + + + ); + + it('should render trade param with "Take profit" label', () => { + mockTakeProfit(); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByText(take_profit_trade_param)).toBeInTheDocument(); + }); + + it('should open ActionSheet with input, "Save" button and text content with definition if user clicks on trade param', () => { + mockTakeProfit(); + + expect(screen.queryByTestId('dt-actionsheet-overlay')).not.toBeInTheDocument(); + + userEvent.click(screen.getByText(take_profit_trade_param)); + + expect(screen.getByTestId('dt-actionsheet-overlay')).toBeInTheDocument(); + const input = screen.getByRole('spinbutton'); + expect(input).toBeInTheDocument(); + expect(screen.getByText('Save')).toBeInTheDocument(); + expect( + screen.getByText( + 'When your profit reaches or exceeds the set amount, your trade will be closed automatically.' + ) + ).toBeInTheDocument(); + }); + + it('should render alternative text content with definition for Accumulators', () => { + default_mock_store.modules.trade.is_accumulator = true; + mockTakeProfit(); + + userEvent.click(screen.getByText(take_profit_trade_param)); + + expect(screen.getByText('Note: Cannot be adjusted for ongoing accumulator contracts.')).toBeInTheDocument(); + }); + + it('should enable input, when user clicks on ToggleSwitch', () => { + mockTakeProfit(); + + userEvent.click(screen.getByText(take_profit_trade_param)); + + const toggle_switcher = screen.getAllByRole('button')[0]; + const input = screen.getByRole('spinbutton'); + + expect(input).toBeDisabled(); + userEvent.click(toggle_switcher); + expect(input).toBeEnabled(); + }); + + it('should enable input, when user clicks on Take Profit overlay', () => { + mockTakeProfit(); + + userEvent.click(screen.getByText(take_profit_trade_param)); + + const take_profit_overlay = screen.getByTestId('dt_take_profit_overlay'); + const input = screen.getByRole('spinbutton'); + + expect(input).toBeDisabled(); + userEvent.click(take_profit_overlay); + expect(input).toBeEnabled(); + }); + + it('should validate values, that user typed, and show error text if they are out of acceptable range. If values are wrong, when user clicks on "Save" button onChangeMultiple and onChange will not be called', () => { + default_mock_store.modules.trade.validation_params = { + take_profit: { + min: '0.01', + max: '100', + }, + }; + mockTakeProfit(); + + userEvent.click(screen.getByText(take_profit_trade_param)); + + const toggle_switcher = screen.getAllByRole('button')[0]; + userEvent.click(toggle_switcher); + + const input = screen.getByRole('spinbutton'); + userEvent.type(input, ' '); + expect(screen.getByText('Please enter a take profit amount.')); + + const save_button = screen.getByText('Save'); + userEvent.click(save_button); + expect(default_mock_store.modules.trade.onChangeMultiple).not.toBeCalled(); + expect(default_mock_store.modules.trade.onChange).not.toBeCalled(); + + userEvent.type(input, '0.0002'); + expect(screen.getByText('Acceptable range: 0.01 to 100')); + + userEvent.click(save_button); + expect(default_mock_store.modules.trade.onChangeMultiple).not.toBeCalled(); + expect(default_mock_store.modules.trade.onChange).not.toBeCalled(); + }); + + it('should validate values, that user typed. In case if values are correct, when user clicks on "Save" button onChangeMultiple and onChange will be called', () => { + default_mock_store.modules.trade.validation_params = { + take_profit: { + min: '0.01', + max: '100', + }, + }; + mockTakeProfit(); + + userEvent.click(screen.getByText(take_profit_trade_param)); + + const toggle_switcher = screen.getAllByRole('button')[0]; + userEvent.click(toggle_switcher); + + const input = screen.getByRole('spinbutton'); + userEvent.type(input, '2'); + expect(screen.getByText('Acceptable range: 0.01 to 100')); + + userEvent.click(screen.getByText('Save')); + expect(default_mock_store.modules.trade.onChangeMultiple).toBeCalled(); + expect(default_mock_store.modules.trade.onChange).toBeCalled(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/index.ts b/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/index.ts index 331cdb569ef4..6c22d518d8d1 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/index.ts +++ b/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/index.ts @@ -1,3 +1,4 @@ +import './take-profit.scss'; import TakeProfit from './take-profit'; export default TakeProfit; diff --git a/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/take-profit-description.tsx b/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/take-profit-description.tsx new file mode 100644 index 000000000000..58a76c6c5a26 --- /dev/null +++ b/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/take-profit-description.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { ActionSheet, Text } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv/translations'; + +const TakeProfitDescription = () => ( + + + + + +); + +export default TakeProfitDescription; diff --git a/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/take-profit-input.tsx b/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/take-profit-input.tsx new file mode 100644 index 000000000000..84ed031af22c --- /dev/null +++ b/packages/trader/src/AppV2/Components/TradeParameters/TakeProfit/take-profit-input.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { ActionSheet, CaptionText, Text, ToggleSwitch, TextFieldWithSteppers } from '@deriv-com/quill-ui'; +import { Localize, localize } from '@deriv/translations'; + +type TTakeProfitInputProps = { + currency?: string; + decimals?: number; + error_message?: React.ReactNode; + is_enabled?: boolean; + is_accumulator?: boolean; + message?: React.ReactNode; + onToggleSwitch: (new_value: boolean) => void; + onInputChange: (e: React.ChangeEvent) => void; + onSave: () => void; + take_profit_value?: string | number; +}; + +const TakeProfitInput = React.forwardRef( + ( + { + currency, + decimals, + error_message, + is_enabled, + is_accumulator, + message, + onToggleSwitch, + onInputChange, + onSave, + take_profit_value, + }: TTakeProfitInputProps, + ref: React.ForwardedRef + ) => { + return ( + + +
    + + + + +
    + + {!is_enabled && ( + + +
    + ); + }; + + render(); + + const input = screen.getByRole('spinbutton'); + expect(input).not.toHaveFocus(); + + userEvent.click(screen.getByText('Focus')); + + jest.runAllTimers(); + + expect(input).toHaveFocus(); + }); +}); + describe('getTradeTypeTabsList', () => { it('should return correct tabs list for Turbos', () => { expect(getTradeTypeTabsList(TRADE_TYPES.TURBOS.SHORT)).toEqual([ diff --git a/packages/trader/src/AppV2/Utils/trade-params-utils.tsx b/packages/trader/src/AppV2/Utils/trade-params-utils.tsx index a322245ecda3..fe4fea2e17e0 100644 --- a/packages/trader/src/AppV2/Utils/trade-params-utils.tsx +++ b/packages/trader/src/AppV2/Utils/trade-params-utils.tsx @@ -50,6 +50,21 @@ export const isDigitContractWinning = ( return win_conditions[contract_type]; }; +export const focusAndOpenKeyboard = (focused_input?: HTMLInputElement | null, main_input?: HTMLInputElement | null) => { + if (main_input && focused_input) { + // Reveal a temporary input element and put focus on it + focused_input.style.display = 'block'; + focused_input.focus({ preventScroll: true }); + + // The keyboard is open, so now adding a delayed focus on the target element and hide the temporary input element + return setTimeout(() => { + main_input.focus(); + main_input.click(); + focused_input.style.display = 'none'; + }, 300); + } +}; + export const getTradeTypeTabsList = (contract_type = '') => { const is_turbos = isTurbosContract(contract_type); const is_vanilla = isVanillaContract(contract_type); diff --git a/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-button.spec.tsx b/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-button.spec.tsx index 1cad9c32e5f8..00dd532bff26 100644 --- a/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-button.spec.tsx +++ b/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-button.spec.tsx @@ -31,6 +31,7 @@ const default_mocked_props: React.ComponentProps = { profit: '', returns: '', spot: 0, + validation_params: undefined, }, is_accumulator: false, is_disabled: false, diff --git a/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-fieldset.spec.tsx b/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-fieldset.spec.tsx index 2c2f207b1aff..836e61a51d19 100644 --- a/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-fieldset.spec.tsx +++ b/packages/trader/src/Modules/Trading/Components/Elements/__tests__/purchase-fieldset.spec.tsx @@ -29,6 +29,7 @@ const default_mocked_props: React.ComponentProps = { profit: '', returns: '', spot: 0, + validation_params: undefined, }, is_accumulator: false, is_disabled: false, diff --git a/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/cancel-deal-info.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/cancel-deal-info.spec.tsx index 297a9c15dfb5..870d0c001684 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/cancel-deal-info.spec.tsx +++ b/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/cancel-deal-info.spec.tsx @@ -26,6 +26,7 @@ const mock_proposal_info: TProposalTypeInfo = { returns: '', stake: '', spot: 0, + validation_params: undefined, }; const default_mock_store = { diff --git a/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/contract-info.spec.tsx b/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/contract-info.spec.tsx index 90ea045984e6..1315e7ba9f04 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/contract-info.spec.tsx +++ b/packages/trader/src/Modules/Trading/Components/Form/Purchase/__tests__/contract-info.spec.tsx @@ -37,6 +37,7 @@ const default_mock_props: React.ComponentProps = { profit: '', returns: '', spot: 0, + validation_params: undefined, }, type: 'test_contract_type', }; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts b/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts index 21bd2c3cb277..2e5f59606203 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts @@ -36,6 +36,25 @@ type TObjExpiry = { date_expiry?: number; }; +type TValidationParams = + | { + validation_params?: { + max_payout?: string; + max_ticks?: number; + stake?: { + max: string; + min: string; + }; + take_profit: { + max: string; + min: string; + }; + }; + } + | undefined; + +type ExpandedProposal = Proposal & TValidationParams; + const isVisible = (elem: HTMLElement) => !(!elem || (elem.offsetWidth === 0 && elem.offsetHeight === 0)); const map_error_field: { [key: string]: string } = { @@ -59,7 +78,7 @@ export const getProposalInfo = ( response: PriceProposalResponse & TError, obj_prev_contract_basis: TObjContractBasis ) => { - const proposal = response.proposal || ({} as Proposal); + const proposal: ExpandedProposal = response.proposal || ({} as ExpandedProposal); const profit = (proposal.payout || 0) - (proposal.ask_price || 0); const returns = (profit * 100) / (proposal.ask_price || 1); const stake = proposal.display_value; @@ -72,7 +91,7 @@ export const getProposalInfo = ( const is_stake = contract_basis?.value === 'stake'; - const price = is_stake ? stake : (proposal[contract_basis?.value as keyof Proposal] as string | number); + const price = is_stake ? stake : (proposal[contract_basis?.value as keyof ExpandedProposal] as string | number); const obj_contract_basis = { text: contract_basis?.text || '', @@ -103,6 +122,7 @@ export const getProposalInfo = ( returns: `${returns.toFixed(2)}%`, stake, spot: proposal.spot, + validation_params: proposal?.validation_params, ...accumulators_details, }; }; diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.ts b/packages/trader/src/Stores/Modules/Trading/trade-store.ts index bcccc279d8c8..54f546cb8216 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.ts +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.ts @@ -173,6 +173,7 @@ type TStakeBoundary = Record< >; type TTicksHistoryResponse = TicksHistoryResponse | TicksStreamResponse; type TBarriersData = Record | { barrier: string; barrier_choices: string[] }; +type TValidationParams = ReturnType['validation_params']; const store_name = 'trade_store'; const g_subscribers_map: Partial>> = {}; // blame amin.m @@ -246,6 +247,7 @@ export default class TradeStore extends BaseStore { * */ market_close_times: string[] = []; + validation_params?: TValidationParams | Record = {}; // Last Digit digit_stats: number[] = []; @@ -421,6 +423,7 @@ export default class TradeStore extends BaseStore { market_open_times: observable, maximum_payout: observable, maximum_ticks: observable, + validation_params: observable, multiplier_range_list: observable, multiplier: observable, non_available_contract_types_list: observable, @@ -1376,6 +1379,16 @@ export default class TradeStore extends BaseStore { } this.stop_out = limit_order?.stop_out?.order_amount; } + + if (this.is_turbos && (this.proposal_info?.TURBOSSHORT || this.proposal_info?.TURBOSLONG)) { + if (this.proposal_info?.TURBOSSHORT) { + this.validation_params = this.proposal_info.TURBOSSHORT.validation_params; + } + if (this.proposal_info?.TURBOSLONG) { + this.validation_params = this.proposal_info.TURBOSLONG.validation_params; + } + } + if (this.is_accumulator && this.proposal_info?.ACCU) { const { barrier_spot_distance, @@ -1387,12 +1400,14 @@ export default class TradeStore extends BaseStore { high_barrier, low_barrier, spot_time, + validation_params, } = this.proposal_info.ACCU; this.ticks_history_stats = getUpdatedTicksHistoryStats({ previous_ticks_history_stats: this.ticks_history_stats, new_ticks_history_stats: ticks_stayed_in, last_tick_epoch, }); + this.validation_params = validation_params; this.maximum_ticks = maximum_ticks; this.maximum_payout = maximum_payout; this.tick_size_barrier_percentage = tick_size_barrier_percentage;