diff --git a/packages/api-v2/src/APIProvider.tsx b/packages/api-v2/src/APIProvider.tsx index a2abdb6c89..8fbac041c4 100644 --- a/packages/api-v2/src/APIProvider.tsx +++ b/packages/api-v2/src/APIProvider.tsx @@ -38,8 +38,7 @@ type APIContextData = { * @returns {string} The WebSocket URL. */ const getWebSocketURL = (endpoint: string) => { - // TODO remove hardcoded app_id in future - return `wss://${endpoint}/websockets/v3?app_id=16929&brand=${getBrandName().toLowerCase()}`; + return `wss://${endpoint}/websockets/v3?brand=${getBrandName().toLowerCase()}`; }; const APIContext = createContext(null); @@ -97,6 +96,11 @@ const APIProvider = ({ children }: PropsWithChildren) => { refetchOnReconnect: false, }, }, + logger: { + log: () => {}, + warn: () => {}, + error: () => {}, + }, }); } diff --git a/packages/api-v2/src/AuthProvider.tsx b/packages/api-v2/src/AuthProvider.tsx index 112495233f..35ed569952 100644 --- a/packages/api-v2/src/AuthProvider.tsx +++ b/packages/api-v2/src/AuthProvider.tsx @@ -201,7 +201,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun setLoginid(res?.authorize?.loginid ?? ''); }) .catch(async (e: TAuthorizeError) => { - if (e?.error.code === API_ERROR_CODES.DISABLED_ACCOUNT) { + if (e?.code === API_ERROR_CODES.DISABLED_ACCOUNT) { await logout?.(); } setIsLoading(false); @@ -248,7 +248,7 @@ const AuthProvider = ({ loginIDKey, children, cookieTimeout, selectDefaultAccoun setLoginid(newLoginId); processAuthorizeResponse(authorizeResponse); } catch (e: unknown) { - if (typeof e === 'object' && (e as TAuthorizeError)?.error.code === API_ERROR_CODES.DISABLED_ACCOUNT) { + if (typeof e === 'object' && (e as TAuthorizeError)?.code === API_ERROR_CODES.DISABLED_ACCOUNT) { await logout?.(); } } finally { diff --git a/packages/api-v2/src/index.ts b/packages/api-v2/src/index.ts index cda7292966..054c5cea70 100644 --- a/packages/api-v2/src/index.ts +++ b/packages/api-v2/src/index.ts @@ -11,12 +11,4 @@ export { default as useRemoteConfig } from './hooks/useRemoteConfig'; export { default as useTrackJS } from './hooks/useTrackJS'; export * from './hooks'; -export { - useInfiniteQuery, - useMutation, - useQuery, - /** @deprecated use `useQuery` instead */ - useQuery as useFetch, - /** @deprecated use `useMutation` instead */ - useMutation as useRequest, -}; +export { useInfiniteQuery, useMutation, useQuery }; diff --git a/packages/api-v2/src/useAuthorizedQuery.ts b/packages/api-v2/src/useAuthorizedQuery.ts index 843a4fa58d..a23328b637 100644 --- a/packages/api-v2/src/useAuthorizedQuery.ts +++ b/packages/api-v2/src/useAuthorizedQuery.ts @@ -43,7 +43,7 @@ const useAuthorizedQuery = ( const isEnabled = typeof options?.enabled === 'boolean' ? options.enabled : true; - return _useQuery, TSocketError>(keys, () => send(name, payload), { + return _useQuery, TSocketError['error']>(keys, () => send(name, payload), { ...options, enabled: !!(isSuccess && !isLoading && loginid && isEnabled), }); diff --git a/packages/api-v2/src/useInfiniteQuery.ts b/packages/api-v2/src/useInfiniteQuery.ts index 8c809b6576..907e915b3b 100644 --- a/packages/api-v2/src/useInfiniteQuery.ts +++ b/packages/api-v2/src/useInfiniteQuery.ts @@ -25,7 +25,7 @@ const useInfiniteQuery = ( const initial_offset = payload?.offset || 0; const limit = payload?.limit || 50; - return _useInfiniteQuery, TSocketError>( + return _useInfiniteQuery, TSocketError['error']>( getQueryKeys(name, payload), ({ pageParam = 0 }) => diff --git a/packages/api-v2/src/useMutation.ts b/packages/api-v2/src/useMutation.ts index ec92a25bb6..525047cad4 100644 --- a/packages/api-v2/src/useMutation.ts +++ b/packages/api-v2/src/useMutation.ts @@ -16,7 +16,7 @@ const useMutation = (name: T, options?: TSocketR mutate: _mutate, mutateAsync: _mutateAsync, ...rest - } = _useMutation, TSocketError, TSocketAcceptableProps>(props => { + } = _useMutation, TSocketError['error'], TSocketAcceptableProps>(props => { const prop = props?.[0]; const payload = prop && 'payload' in prop ? (prop.payload as TSocketRequestPayload['payload']) : undefined; diff --git a/packages/api-v2/src/useQuery.ts b/packages/api-v2/src/useQuery.ts index b5a1ae010a..1170315ba1 100644 --- a/packages/api-v2/src/useQuery.ts +++ b/packages/api-v2/src/useQuery.ts @@ -16,7 +16,7 @@ const useQuery = (name: T, ...props: TSocketAcce const options = prop && 'options' in prop ? (prop.options as TSocketRequestQueryOptions) : undefined; const { send } = useAPI(); - return _useQuery, TSocketError>( + return _useQuery, TSocketError['error']>( getQueryKeys(name, payload), () => send(name, payload), options diff --git a/packages/api-v2/types.ts b/packages/api-v2/types.ts index e30ea2e54f..e0b8c3d2df 100644 --- a/packages/api-v2/types.ts +++ b/packages/api-v2/types.ts @@ -453,15 +453,15 @@ export type TSocketRequestPayload< }; export type TSocketRequestQueryOptions = Parameters< - typeof useQuery, TSocketError> + typeof useQuery, TSocketError['error']> >[2]; export type TSocketRequestInfiniteQueryOptions = Parameters< - typeof useInfiniteQuery, TSocketError> + typeof useInfiniteQuery, TSocketError['error']> >[2]; export type TSocketRequestMutationOptions = Parameters< - typeof useMutation, TSocketError, TSocketAcceptableProps> + typeof useMutation, TSocketError['error'], TSocketAcceptableProps> >[2]; type TSocketRequestWithOptions< diff --git a/packages/api/src/APIProvider.tsx b/packages/api/src/APIProvider.tsx index eebef22594..6c24a10904 100644 --- a/packages/api/src/APIProvider.tsx +++ b/packages/api/src/APIProvider.tsx @@ -49,7 +49,19 @@ declare global { // This is a temporary workaround to share a single `QueryClient` instance between all the packages. const getSharedQueryClientContext = (): QueryClient => { if (!window.ReactQueryClient) { - window.ReactQueryClient = new QueryClient(); + window.ReactQueryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }, + }, + logger: { + log: () => {}, + warn: () => {}, + error: () => {}, + }, + }); } return window.ReactQueryClient; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 6f2682d54d..9abad038cd 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -10,15 +10,7 @@ export { default as useRemoteConfig } from './hooks/useRemoteConfig'; export { default as useTrackJS } from './hooks/useTrackJS'; export * from './hooks'; -export { - useInfiniteQuery, - useMutation, - useQuery, - /** @deprecated use `useQuery` instead */ - useQuery as useFetch, - /** @deprecated use `useMutation` instead */ - useMutation as useRequest, -}; +export { useInfiniteQuery, useMutation, useQuery }; // Export types from types.ts export type { diff --git a/packages/api/src/useInfiniteQuery.ts b/packages/api/src/useInfiniteQuery.ts index aaa941db83..6052223a14 100644 --- a/packages/api/src/useInfiniteQuery.ts +++ b/packages/api/src/useInfiniteQuery.ts @@ -25,7 +25,7 @@ const useInfiniteQuery = ( const initial_offset = payload?.offset || 0; const limit = payload?.limit || 50; - return _useInfiniteQuery, TSocketError>( + return _useInfiniteQuery, TSocketError['error']>( getQueryKeys(name, payload), ({ pageParam = 0 }) => diff --git a/packages/api/src/useMutation.ts b/packages/api/src/useMutation.ts index ced10c1f12..45c017a935 100644 --- a/packages/api/src/useMutation.ts +++ b/packages/api/src/useMutation.ts @@ -16,7 +16,7 @@ const useMutation = (name: T, options?: TSocketR mutate: _mutate, mutateAsync: _mutateAsync, ...rest - } = _useMutation, TSocketError, TSocketAcceptableProps>(props => { + } = _useMutation, TSocketError['error'], TSocketAcceptableProps>(props => { const prop = props?.[0]; const payload = prop && 'payload' in prop ? (prop.payload as TSocketRequestPayload) : undefined; diff --git a/packages/api/src/useQuery.ts b/packages/api/src/useQuery.ts index a30b76dffd..20e1178a8c 100644 --- a/packages/api/src/useQuery.ts +++ b/packages/api/src/useQuery.ts @@ -16,7 +16,7 @@ const useQuery = (name: T, ...props: TSocketAcce const options = prop && 'options' in prop ? (prop.options as TSocketRequestQueryOptions) : undefined; const { send } = useAPI(); - return _useQuery, TSocketError>( + return _useQuery, TSocketError['error']>( getQueryKeys(name, payload), () => send(name, payload), options diff --git a/packages/api/types.ts b/packages/api/types.ts index 1f6110ae36..08a9e93bd0 100644 --- a/packages/api/types.ts +++ b/packages/api/types.ts @@ -127,7 +127,9 @@ type PriceProposalRequest = Omit< }; type PriceProposalResponse = Omit & { - proposal?: Omit, 'display_value'>; + proposal?: Omit, 'display_value'> & { + payout_choices?: string[]; + }; }; type BuyContractRequest = Omit & { @@ -502,15 +504,15 @@ export type TSocketRequestPayload< }; export type TSocketRequestQueryOptions = Parameters< - typeof useQuery, TSocketError> + typeof useQuery, TSocketError['error']> >[2]; export type TSocketRequestInfiniteQueryOptions = Parameters< - typeof useInfiniteQuery, TSocketError> + typeof useInfiniteQuery, TSocketError['error']> >[2]; export type TSocketRequestMutationOptions = Parameters< - typeof useMutation, TSocketError, TSocketAcceptableProps> + typeof useMutation, TSocketError['error'], TSocketAcceptableProps> >[2]; type TSocketRequestWithOptions< diff --git a/packages/core/src/Stores/active-symbols-store.js b/packages/core/src/Stores/active-symbols-store.js deleted file mode 100644 index fe8a08cbd8..0000000000 --- a/packages/core/src/Stores/active-symbols-store.js +++ /dev/null @@ -1,28 +0,0 @@ -import { WS } from '@deriv/shared'; -import { observable, action, runInAction, makeObservable } from 'mobx'; -import BaseStore from './base-store'; - -export default class ActiveSymbolsStore extends BaseStore { - active_symbols = []; - - constructor() { - // TODO: [mobx-undecorate] verify the constructor arguments and the arguments of this automatically generated super call - super(); - - makeObservable(this, { - active_symbols: observable, - setActiveSymbols: action.bound, - }); - } - - async setActiveSymbols() { - const { active_symbols, error } = await WS.authorized.activeSymbols(); - runInAction(() => { - if (!active_symbols.length || error) { - this.active_symbols = []; - return; - } - this.active_symbols = active_symbols; - }); - } -} diff --git a/packages/core/src/Stores/index.js b/packages/core/src/Stores/index.js index fd45c12039..2670a6f0d6 100644 --- a/packages/core/src/Stores/index.js +++ b/packages/core/src/Stores/index.js @@ -5,7 +5,6 @@ import GTMStore from './gtm-store'; import ModulesStore from './Modules'; import NotificationStore from './notification-store'; import UIStore from './ui-store'; -import ActiveSymbolsStore from './active-symbols-store'; import PortfolioStore from './portfolio-store'; import ContractReplayStore from './contract-replay-store'; import ContractTradeStore from './contract-trade-store'; @@ -19,7 +18,6 @@ export default class RootStore { this.ui = new UIStore(this); this.gtm = new GTMStore(this); this.notifications = new NotificationStore(this); - this.active_symbols = new ActiveSymbolsStore(this); this.portfolio = new PortfolioStore(this); this.contract_replay = new ContractReplayStore(this); this.contract_trade = new ContractTradeStore(this); diff --git a/packages/core/src/_common/base/socket_base.js b/packages/core/src/_common/base/socket_base.js index 7acdbca8bb..79a77777e7 100644 --- a/packages/core/src/_common/base/socket_base.js +++ b/packages/core/src/_common/base/socket_base.js @@ -29,8 +29,7 @@ const BinarySocketBase = (() => { if (is_mock_server) { return 'ws://127.0.0.1:42069'; } - // TODO remove hardcoded app_id in future - return `wss://${getSocketURL()}/websockets/v3?app_id=16929&brand=${getBrandName().toLowerCase()}`; + return `wss://${getSocketURL()}/websockets/v3?brand=${getBrandName().toLowerCase()}`; }; const isReady = () => hasReadyState(1); diff --git a/packages/trader/src/AppV2/Components/ClosedMarketMessage/closed-market-message.tsx b/packages/trader/src/AppV2/Components/ClosedMarketMessage/closed-market-message.tsx index 9fd7671d43..c9298b6081 100644 --- a/packages/trader/src/AppV2/Components/ClosedMarketMessage/closed-market-message.tsx +++ b/packages/trader/src/AppV2/Components/ClosedMarketMessage/closed-market-message.tsx @@ -2,12 +2,11 @@ import React from 'react'; import clsx from 'clsx'; import { TTradingTimesRequest } from '@deriv/api'; -import { isMarketClosed, toMoment, useIsMounted, WS, mapErrorMessage } from '@deriv/shared'; +import { toMoment, useIsMounted, WS, mapErrorMessage } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; import { Localize } from '@deriv-com/translations'; import { CaptionText } from '@deriv-com/quill-ui'; -import useActiveSymbols from 'AppV2/Hooks/useActiveSymbols'; import { calculateTimeLeft, getSymbol } from 'AppV2/Utils/closed-market-message-utils'; import { useTraderStore } from 'Stores/useTraderStores'; @@ -34,7 +33,6 @@ const ClosedMarketMessage = observer(() => { const { common } = useStore(); const { current_language } = common; const { symbol, prepareTradeStore, is_market_closed } = useTraderStore(); - const { activeSymbols } = useActiveSymbols(); const isMounted = useIsMounted(); const [when_market_opens, setWhenMarketOpens] = React.useState({} as TWhenMarketOpens); @@ -84,7 +82,7 @@ const ClosedMarketMessage = observer(() => { setTimeLeft({}); setWhenMarketOpens({} as TWhenMarketOpens); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeSymbols, symbol]); + }, [symbol, is_market_closed]); React.useEffect(() => { let timer: ReturnType; diff --git a/packages/trader/src/AppV2/Components/MarketSelector/market-selector.scss b/packages/trader/src/AppV2/Components/MarketSelector/market-selector.scss index 11fb3ef806..ca84e1e21b 100644 --- a/packages/trader/src/AppV2/Components/MarketSelector/market-selector.scss +++ b/packages/trader/src/AppV2/Components/MarketSelector/market-selector.scss @@ -6,6 +6,9 @@ &__container { padding: 0 var(--core-spacing-800); } + &__skeleton { + padding: 0 var(--semantic-spacing-general-sm); + } &-info { display: flex; flex-direction: column; diff --git a/packages/trader/src/AppV2/Components/MarketSelector/market-selector.tsx b/packages/trader/src/AppV2/Components/MarketSelector/market-selector.tsx index ac6117d07e..c5fd231d3b 100644 --- a/packages/trader/src/AppV2/Components/MarketSelector/market-selector.tsx +++ b/packages/trader/src/AppV2/Components/MarketSelector/market-selector.tsx @@ -13,7 +13,7 @@ import SymbolIconsMapper from '../SymbolIconsMapper/symbol-icons-mapper'; const MarketSelector = observer(() => { const [isOpen, setIsOpen] = useState(false); - const { activeSymbols } = useActiveSymbols(); + const { activeSymbols, isLoading } = useActiveSymbols(); const { symbol: storeSymbol, tick_data, is_market_closed, contract_type } = useTraderStore(); const { addSnackbar } = useSnackbar(); const { trade_types } = useContractsFor(); @@ -23,7 +23,7 @@ const MarketSelector = observer(() => { const contract_name = trade_types?.find((item: TContractType) => item.value === contract_type)?.text; useEffect(() => { - if (!currentSymbol) { + if (!currentSymbol && !isLoading) { const symbol_name = getMarketNamesMap()[storeSymbol as keyof typeof getMarketNamesMap] || storeSymbol; const message = contract_name ? ( { // Show skeleton loader for a reasonable time, then fallback to basic UI if (typeof currentSymbol?.exchange_is_open === 'undefined' && !showFallback) { - return ; + return ( +
+ +
+ ); } // Fallback UI when data is not available after timeout diff --git a/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration.spec.tsx b/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration.spec.tsx index af1d2d0e5e..47fe9b950a 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration.spec.tsx @@ -43,6 +43,11 @@ jest.mock('@deriv/shared', () => ({ })), })); +jest.mock('../day', () => ({ + __esModule: true, + default: jest.fn(() =>
Mocked DayInput
), +})); + describe('Duration', () => { let default_trade_store: TCoreStores, mockOnChangeMultiple: jest.Mock; diff --git a/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration_container.spec.tsx b/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration_container.spec.tsx index 35b8a6f80c..b0796e5c96 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration_container.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/Duration/__tests__/duration_container.spec.tsx @@ -3,11 +3,9 @@ import moment from 'moment'; import { mockStore } from '@deriv/stores'; import { TCoreStores } from '@deriv/stores/types'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ContractType } from 'Stores/Modules/Trading/Helpers/contract-type'; - import TraderProviders from '../../../../../trader-providers'; import DurationActionSheetContainer from '../container'; @@ -50,6 +48,11 @@ jest.mock('@deriv-com/quill-ui', () => ({ )), })); +jest.mock('../day', () => ({ + __esModule: true, + default: jest.fn(() =>
Mocked DayInput
), +})); + describe('DurationActionSheetContainer', () => { let default_trade_store: TCoreStores; @@ -226,36 +229,9 @@ describe('DurationActionSheetContainer', () => { }); }); - it('should show Expiry Date when days are selected', () => { - renderDurationContainer(default_trade_store, 'd'); - expect(screen.getByText('Expiry')).toBeInTheDocument(); - }); - - it('should show End Time Screen on selecting the days unit', () => { - renderDurationContainer(default_trade_store, 'd'); - const date_input = screen.getByTestId('dt_date_input'); - expect(date_input).toBeInTheDocument(); - }); - - it('should open datepicker on clicking on date input in the days page', async () => { - renderDurationContainer(default_trade_store, 'd'); - const mockEvents = [{ dates: 'Fridays, Saturdays', descrip: 'Some description' }]; - jest.spyOn(ContractType, 'getTradingEvents').mockResolvedValue(mockEvents); - - const date_input = screen.getByTestId('dt_date_input'); - expect(date_input).toBeInTheDocument(); - await userEvent.click(date_input); - expect(screen.getByText('Pick an end date')); - }); - - it('should save and close datepicker on clicking done button', async () => { + it('should render DayInput component when days unit is selected', () => { renderDurationContainer(default_trade_store, 'd'); - const date_input = screen.getByTestId('dt_date_input'); - expect(date_input).toBeInTheDocument(); - await userEvent.click(date_input); - expect(screen.getByText('Pick an end date')); - await userEvent.click(screen.getByText('Done')); - await waitFor(() => expect(screen.queryByText('Pick an end date')).not.toBeInTheDocument()); + expect(screen.getByText('Mocked DayInput')).toBeInTheDocument(); }); it('should not render chips if duration_units_list contains only ticks', () => { diff --git a/packages/trader/src/AppV2/Components/TradeParameters/Duration/day.tsx b/packages/trader/src/AppV2/Components/TradeParameters/Duration/day.tsx index 10674e1364..236988ffa4 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/Duration/day.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/Duration/day.tsx @@ -1,19 +1,15 @@ import React, { useEffect, useState } from 'react'; +import { useInvalidateQuery } from '@deriv/api'; import { LabelPairedCalendarSmRegularIcon, LabelPairedClockThreeSmRegularIcon } from '@deriv/quill-icons'; import { hasIntradayDurationUnit, setTime, toMoment, mapErrorMessage } from '@deriv/shared'; import { useStore } from '@deriv/stores'; -import { Localize } from '@deriv-com/translations'; import { ActionSheet, Text, TextField, useSnackbar } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv-com/translations'; -import { invalidateDTraderCache, useDtraderQuery } from 'AppV2/Hooks/useDtraderQuery'; -import { - getClosestTimeToCurrentGMT, - getDatePickerStartDate, - getProposalRequestObject, -} from 'AppV2/Utils/trade-params-utils'; +import { useProposal } from 'AppV2/Hooks/useProposal'; +import { getClosestTimeToCurrentGMT, getDatePickerStartDate } from 'AppV2/Utils/trade-params-utils'; import { getBoundaries } from 'Stores/Modules/Trading/Helpers/end-time'; -import { TProposalResponse } from 'Stores/Modules/Trading/trade-store'; import { useTraderStore } from 'Stores/useTraderStores'; import DaysDatepicker from './datepicker'; @@ -86,47 +82,40 @@ const DayInput = ({ symbol, ...(payout_per_point && { payout_per_point }), ...(barrier_value && { barrier: barrier_value }), + ...(barrier_1 && !is_turbos && !barrier_value ? { barrier_1: Math.round(tick_data?.quote as number) } : {}), }; - const proposal_req = getProposalRequestObject({ - new_values, + const { data: response, error: queryError } = useProposal({ trade_store, - trade_type: Object.keys(trade_types)[0], + proposal_request_values: new_values, + contract_type: Object.keys(trade_types)[0], + is_enabled: trigger_date, }); - const { data: response } = useDtraderQuery( - ['proposal', JSON.stringify(day), JSON.stringify(payout_per_point), JSON.stringify(barrier_value)], - { - ...proposal_req, - ...(barrier_1 && !is_turbos && !barrier_value ? { barrier: Math.round(tick_data?.quote as number) } : {}), - }, - { - enabled: trigger_date, - } - ); + const invalidate = useInvalidateQuery(); useEffect(() => { - if (response) { - if (response?.error?.code === 'ContractBuyValidationError') { - const details = response.error.details; + if (queryError) { + if (queryError?.code === 'ContractBuyValidationError') { + const details = queryError.details; - if (details?.field === 'payout_per_point' && details?.payout_per_point_choices) { - const suggested_payout = details?.payout_per_point_choices[0]; - setPayoutPerPoint(suggested_payout); + if (details?.field === 'payout_per_point' && Array.isArray(details?.payout_per_point_choices)) { + const suggested_payout = details.payout_per_point_choices[0]; + setPayoutPerPoint(suggested_payout as number); setTriggerDate(true); return; } - if (details?.field === 'barrier' && details?.barrier_choices) { - const suggested_barrier = details?.barrier_choices[0]; - setBarrierValue(suggested_barrier); + if (details?.field === 'barrier' && Array.isArray(details?.barrier_choices)) { + const suggested_barrier = details.barrier_choices[0]; + setBarrierValue(suggested_barrier as string); setTriggerDate(true); return; } } - if (response?.error?.message && response?.error?.details?.field === 'duration') { - const mappedMessage = mapErrorMessage(response.error); + if (queryError?.message && queryError?.details?.field === 'duration') { + const mappedMessage = mapErrorMessage(queryError); addSnackbar({ message: , status: 'fail', @@ -134,19 +123,15 @@ const DayInput = ({ style: { marginBottom: '48px' }, }); setIsDisabled(true); - } else { - setIsDisabled(false); } + } - invalidateDTraderCache([ - 'proposal', - JSON.stringify(day), - JSON.stringify(payout_per_point), - JSON.stringify(barrier_value), - ]); + if (response) { + setIsDisabled(false); + invalidate('proposal'); setTriggerDate(false); } - }, [response, setSelectedExpiryDate]); + }, [response, setSelectedExpiryDate, invalidate]); // Always calculate adjusted_start_time based on TODAY, not the selected expiry_date const today_moment = toMoment(server_time); diff --git a/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/__tests__/payout-per-point.spec.tsx b/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/__tests__/payout-per-point.spec.tsx index 988b0bc8f2..08cdc0e7eb 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/__tests__/payout-per-point.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/__tests__/payout-per-point.spec.tsx @@ -27,24 +27,25 @@ jest.mock('@deriv-com/quill-ui', () => ({ )), })); -jest.mock('@deriv/shared', () => ({ - ...jest.requireActual('@deriv/shared'), - WS: { - send: jest.fn(), - authorized: { - send: jest.fn(), - }, - }, -})); -jest.mock('AppV2/Hooks/useDtraderQuery', () => ({ - ...jest.requireActual('AppV2/Hooks/useDtraderQuery'), - useDtraderQuery: jest.fn(() => ({ - data: { - proposal: { barrier_spot_distance: '+5.37' }, - echo_req: { contract_type: 'TURBOSSHORT' }, - error: {}, - }, - })), +jest.mock('../payout-per-point-wheel', () => ({ + __esModule: true, + default: jest.fn(({ barrier, onPayoutPerPointSelect, onClose, payout_per_point_list }) => ( +
+

WheelPicker

+
    + {payout_per_point_list.map(({ value }: { value: string }) => ( +
  • + +
  • + ))} +
+
+

Barrier

+ {barrier &&

{barrier}

} +
+ +
+ )), })); describe('PayoutPerPoint', () => { diff --git a/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/payout-per-point-wheel.tsx b/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/payout-per-point-wheel.tsx index 8477da2341..92d4bd7e86 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/payout-per-point-wheel.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/PayoutPerPoint/payout-per-point-wheel.tsx @@ -2,13 +2,11 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; import { Skeleton } from '@deriv/components'; -import { Localize } from '@deriv-com/translations'; import { ActionSheet, Text, WheelPicker } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv-com/translations'; -import { useDtraderQuery } from 'AppV2/Hooks/useDtraderQuery'; -import { getProposalRequestObject } from 'AppV2/Utils/trade-params-utils'; +import { useProposal } from 'AppV2/Hooks/useProposal'; import { useTraderStore } from 'Stores/useTraderStores'; -import { TTradeStore } from 'Types'; type TPayoutPerPointWheelProps = { barrier?: string | number; @@ -20,7 +18,6 @@ type TPayoutPerPointWheelProps = { value: string; }[]; }; -type TOnProposalResponse = TTradeStore['onProposalResponse']; const PayoutPerPointWheel = observer( ({ @@ -41,25 +38,18 @@ const PayoutPerPointWheel = observer( const is_api_response_received_ref = React.useRef(false); const new_values = { payout_per_point: String(value) }; - const proposal_req = getProposalRequestObject({ - new_values, + + // Sending proposal without subscription to get a new barrier value + const { + data: response, + error, + isFetching, + } = useProposal({ trade_store, - trade_type: Object.keys(trade_types)[0], + proposal_request_values: new_values, + contract_type: Object.keys(trade_types)[0], + is_enabled: is_open, }); - // Sending proposal without subscription to get a new barrier value - const { data: response } = useDtraderQuery[0]>( - [ - 'proposal', - ...Object.entries(new_values).flat().join('-'), - `${barrier}`, - Object.keys(trade_types)[0], - JSON.stringify(proposal_req), - ], - proposal_req, - { - enabled: is_open, - } - ); const onChange = (new_value: string | number) => { // If a new value is equal to previous one, then we won't send API request @@ -78,16 +68,14 @@ const PayoutPerPointWheel = observer( }; React.useEffect(() => { - const onProposalResponse: TOnProposalResponse = response => { - const { error, proposal } = response; - const { barrier_spot_distance } = proposal ?? {}; + if (response) { + const { proposal } = response; + const { barrier_spot_distance } = proposal?.contract_details ?? {}; // Currently we are not handling errors - if (barrier_spot_distance && !error) setDisplayedBarrierValue(barrier_spot_distance); + if (barrier_spot_distance) setDisplayedBarrierValue(barrier_spot_distance); is_api_response_received_ref.current = true; - }; - - if (response) onProposalResponse(response); + } }, [response]); return ( @@ -101,7 +89,11 @@ const PayoutPerPointWheel = observer( - {displayed_barrier_value ?? } + {!displayed_barrier_value || error || isFetching ? ( + + ) : ( + displayed_barrier_value + )} diff --git a/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-container.spec.tsx b/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-container.spec.tsx index c0099bf4ed..3cc1fb4baa 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-container.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-container.spec.tsx @@ -13,9 +13,23 @@ jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), WS: { forget: jest.fn(), + send: jest.fn(), + authorized: { + send: jest.fn(), + }, }, })); +jest.mock('AppV2/Hooks/useProposal', () => ({ + useProposal: jest.fn(() => ({ + data: { + proposal: {}, + }, + error: null, + isFetching: false, + })), +})); + describe('TakeProfitAndStopLossContainer', () => { let default_mock_store: ReturnType; diff --git a/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-input.spec.tsx b/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-input.spec.tsx index e68296148f..fa741e7b80 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-input.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/__tests__/take-profit-and-stop-loss-input.spec.tsx @@ -23,14 +23,14 @@ jest.mock('@deriv/shared', () => ({ }, }, })); -jest.mock('AppV2/Hooks/useDtraderQuery', () => ({ - ...jest.requireActual('AppV2/Hooks/useDtraderQuery'), - useDtraderQuery: jest.fn(() => ({ +jest.mock('AppV2/Hooks/useProposal', () => ({ + useProposal: jest.fn(() => ({ data: { proposal: {}, echo_req: { contract_type: 'TURBOSLONG' }, - // No error for successful proposal }, + error: undefined, + isFetching: false, })), })); diff --git a/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/take-profit-and-stop-loss-input.tsx b/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/take-profit-and-stop-loss-input.tsx index 19113fd9a4..64a28aaad4 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/take-profit-and-stop-loss-input.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/RiskManagement/take-profit-and-stop-loss-input.tsx @@ -6,10 +6,10 @@ import { getCurrencyDisplayCode, getDecimalPlaces, mapErrorMessage } from '@deri import { ActionSheet, CaptionText, Text, TextFieldWithSteppers, ToggleSwitch } from '@deriv-com/quill-ui'; import { Localize, useTranslations } from '@deriv-com/translations'; -import { useDtraderQuery } from 'AppV2/Hooks/useDtraderQuery'; import useIsVirtualKeyboardOpen from 'AppV2/Hooks/useIsVirtualKeyboardOpen'; +import { useProposal } from 'AppV2/Hooks/useProposal'; import useTradeError from 'AppV2/Hooks/useTradeError'; -import { focusAndOpenKeyboard, getProposalRequestObject } from 'AppV2/Utils/trade-params-utils'; +import { focusAndOpenKeyboard } from 'AppV2/Utils/trade-params-utils'; import { getDisplayedContractTypes } from 'AppV2/Utils/trade-types-utils'; import { ExpandedProposal } from 'Stores/Modules/Trading/Helpers/proposal'; import { useTraderStore } from 'Stores/useTraderStores'; @@ -98,33 +98,15 @@ const TakeProfitAndStopLossInput = ({ : { stop_loss: is_enabled ? new_input_value : '' }), }; - const proposal_req = getProposalRequestObject({ - new_values, + const { data: response, error: queryError } = useProposal({ trade_store, - trade_type: Object.keys(trade_types)[0], + proposal_request_values: new_values, + contract_type: Object.keys(trade_types)[0], + is_enabled, + // Exclude the opposite field when validating tp/sl independently + should_skip_validation: is_take_profit_input ? 'stop_loss' : 'take_profit', }); - // We need to exclude tp in case if type === sl and vise versa in limit order to validate them independently - if (is_take_profit_input && proposal_req.limit_order?.stop_loss) { - delete proposal_req.limit_order.stop_loss; - } - if (!is_take_profit_input && proposal_req.limit_order?.take_profit) { - delete proposal_req.limit_order.take_profit; - } - - const { data: response } = useDtraderQuery[0]>( - [ - 'proposal', - ...Object.entries(new_values).flat().join('-'), - Object.keys(trade_types)[0], - JSON.stringify(proposal_req), - ], - proposal_req, - { - enabled: is_enabled, - } - ); - const input_message = info.min_value && info.max_value ? ( { - const onProposalResponse: TOnProposalResponse = response => { - const { error, proposal } = response; - - const new_error = error ? mapErrorMessage(error) : ''; - const is_error_field_match = error?.details?.field === type || !error?.details?.field; + if (queryError) { + const new_error = queryError ? mapErrorMessage(queryError) : ''; + const is_error_field_match = queryError?.details?.field === type || !queryError?.details?.field; setErrorText(is_error_field_match ? new_error : ''); updateParentRef({ field_name: is_take_profit_input ? 'tp_error_text' : 'sl_error_text', new_value: is_error_field_match ? new_error : '', }); + is_api_response_received_ref.current = true; + } + + if (response) { + const { proposal } = response; + + // Clear errors on successful response + setErrorText(''); + updateParentRef({ + field_name: is_take_profit_input ? 'tp_error_text' : 'sl_error_text', + new_value: '', + }); // Recovery for min and max allowed values in case of error if (!info.min_value || !info.max_value) { @@ -182,10 +174,8 @@ const TakeProfitAndStopLossInput = ({ ); } is_api_response_received_ref.current = true; - }; - - if (response) onProposalResponse(response); - }, [is_enabled, response]); + } + }, [is_enabled, response, queryError]); const onInputChange = (e: React.ChangeEvent) => { let value = String(e.target.value); diff --git a/packages/trader/src/AppV2/Components/TradeParameters/Stake/__tests__/stake.spec.tsx b/packages/trader/src/AppV2/Components/TradeParameters/Stake/__tests__/stake.spec.tsx index 1354fe488e..2b73854a3c 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/Stake/__tests__/stake.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/Stake/__tests__/stake.spec.tsx @@ -5,7 +5,6 @@ import { mockStore } from '@deriv/stores'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useDtraderQuery } from 'AppV2/Hooks/useDtraderQuery'; import ModulesProvider from 'Stores/Providers/modules-providers'; import TraderProviders from '../../../../../trader-providers'; @@ -33,6 +32,7 @@ jest.mock('AppV2/Hooks/useContractsFor', () => ({ }, })), })); + jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), WS: { @@ -42,13 +42,14 @@ jest.mock('@deriv/shared', () => ({ }, }, })); -jest.mock('AppV2/Hooks/useDtraderQuery', () => ({ - ...jest.requireActual('AppV2/Hooks/useDtraderQuery'), - useDtraderQuery: jest.fn(() => ({ + +jest.mock('AppV2/Hooks/useProposal', () => ({ + useProposal: jest.fn(() => ({ data: { proposal: {}, - error: {}, }, + error: null, + isFetching: false, })), })); @@ -85,8 +86,8 @@ describe('Stake', () => { }, trade_type_tab: 'CALL', validation_params: { - [CONTRACT_TYPES.CALL]: { payout: { max: '50000.00' } }, - [CONTRACT_TYPES.PUT]: { payout: { max: '50000.00' } }, + [CONTRACT_TYPES.CALL]: { stake: { max: '50000.00', min: '0.35' } }, + [CONTRACT_TYPES.PUT]: { stake: { max: '50000.00', min: '0.35' } }, }, }, }, @@ -206,31 +207,23 @@ describe('Stake', () => { ); await userEvent.click(screen.getByText(stake_param_label)); - // Temporarily expecting fallback message since parameterized errors are commented out - expect(screen.getByText('An error occurred. Please try again later.')).toBeInTheDocument(); expect(screen.getByText('Stop out')).toBeInTheDocument(); expect(screen.getByText('Commission')).toBeInTheDocument(); }); it('shows error in case of a validation error if input is non-empty', async () => { - const error_text = "Please enter a stake amount that's at least 0.35."; default_mock_store.modules.trade.contract_type = TRADE_TYPES.HIGH_LOW; default_mock_store.modules.trade.trade_type_tab = 'CALL'; - default_mock_store.modules.trade.proposal_info = { - CALL: { has_error: true, message: error_text, error_field: 'amount' }, + default_mock_store.modules.trade.validation_params = { + CALL: { stake: { max: '50000.00', min: '0.35' } }, }; - (useDtraderQuery as jest.Mock).mockReturnValue({ - data: { - error: { has_error: true, message: error_text, details: { error_field: 'amount' } }, - proposal: {}, - }, - }); render(); await userEvent.click(screen.getByText(stake_param_label)); - expect(screen.getByText(error_text)).toBeInTheDocument(); - expect(screen.getByText('- USD')).toBeInTheDocument(); + + // Verify the acceptable range message is shown + expect(screen.getByText(/Acceptable range:/i)).toBeInTheDocument(); }); it('disables trade param if is_market_closed == true', () => { diff --git a/packages/trader/src/AppV2/Components/TradeParameters/Stake/stake-input.tsx b/packages/trader/src/AppV2/Components/TradeParameters/Stake/stake-input.tsx index 43f26f579f..5fff7db388 100644 --- a/packages/trader/src/AppV2/Components/TradeParameters/Stake/stake-input.tsx +++ b/packages/trader/src/AppV2/Components/TradeParameters/Stake/stake-input.tsx @@ -1,21 +1,26 @@ import React from 'react'; import { observer } from 'mobx-react-lite'; -import { formatMoney, getCurrencyDisplayCode, getDecimalPlaces, trackAnalyticsEvent, mapErrorMessage } from '@deriv/shared'; +import { TPriceProposalResponse, TSocketError } from '@deriv/api'; +import { + formatMoney, + getCurrencyDisplayCode, + getDecimalPlaces, + trackAnalyticsEvent, + mapErrorMessage, +} from '@deriv/shared'; import { ActionSheet, TextFieldWithSteppers } from '@deriv-com/quill-ui'; import { Localize, useTranslations } from '@deriv-com/translations'; -import { useFetchProposalData } from 'AppV2/Hooks/useFetchProposalData'; import useIsVirtualKeyboardOpen from 'AppV2/Hooks/useIsVirtualKeyboardOpen'; +import { useProposal } from 'AppV2/Hooks/useProposal'; import { getPayoutInfo } from 'AppV2/Utils/trade-params-utils'; import { getDisplayedContractTypes } from 'AppV2/Utils/trade-types-utils'; import { ExpandedProposal, getProposalInfo } from 'Stores/Modules/Trading/Helpers/proposal'; import { useTraderStore } from 'Stores/useTraderStores'; -import { TTradeStore } from 'Types'; import StakeDetails from './stake-details'; -type TResponse = Parameters[0]; type TStakeInput = { onClose: () => void; is_open?: boolean; @@ -132,8 +137,8 @@ const createInitialState = (trade_store: ReturnType, deci error_2: second_payout_error, first_contract_payout, second_contract_payout, - is_first_payout_exceeded: !!first_payout_error && first_contract_payout > max_payout, - is_second_payout_exceeded: !!second_payout_error && second_contract_payout > max_payout, + is_first_payout_exceeded: !!first_payout_error && first_contract_payout > Number(max_payout), + is_second_payout_exceeded: !!second_payout_error && second_contract_payout > Number(max_payout), max_payout, max_stake, min_stake, @@ -198,25 +203,35 @@ const StakeInput = observer(({ onClose, is_open }: TStakeInput) => { const should_show_stake_error = !should_send_multiple_proposals || (should_send_multiple_proposals && has_both_errors); - const { data: response_1, is_fetching: is_fetching_1 } = useFetchProposalData({ + const { + data: response_1, + error: error_1, + isFetching: is_fetching_1, + } = useProposal({ trade_store, proposal_request_values, contract_type: contract_types[0], - contract_types, - is_enabled: is_open, + is_enabled: is_open && proposal_request_values.amount !== '', }); - const { data: response_2, is_fetching: is_fetching_2 } = useFetchProposalData({ + const { + data: response_2, + error: error_2, + isFetching: is_fetching_2, + } = useProposal({ trade_store, proposal_request_values, contract_type: contract_types[1], - contract_types, - is_enabled: is_open && should_send_multiple_proposals, + is_enabled: is_open && should_send_multiple_proposals && proposal_request_values.amount !== '', }); const is_loading_proposal = is_fetching_1 || (should_send_multiple_proposals && is_fetching_2); - const handleProposalResponse = (response: TResponse, contractType: 'first' | 'second') => { - const { error, proposal } = response; + const handleProposalResponse = ( + data: TPriceProposalResponse | undefined, + queryError: TSocketError<'proposal'>['error'] | undefined, + contractType: 'first' | 'second' + ) => { + const proposal = data?.proposal; // In case if the value is empty we are showing custom error text from FE (in onSave function) if (proposal_request_values.amount === '') { @@ -225,38 +240,48 @@ const StakeInput = observer(({ onClose, is_open }: TStakeInput) => { } // Handle edge cases for Vanilla contracts - if (is_vanilla && error?.details?.barrier_choices) { - const { barrier_choices } = error.details; + if (is_vanilla && queryError?.details?.barrier_choices && Array.isArray(queryError.details.barrier_choices)) { + const { barrier_choices } = queryError.details; if (!barrier_choices?.includes(barrier_1)) { const index = Math.floor(barrier_choices.length / 2); dispatch({ type: 'SET_PROPOSAL_VALUES', - payload: { barrier_1: barrier_choices[index] }, + payload: { barrier_1: barrier_choices[index] as number }, }); return; } } // Handle edge cases for Turbo contracts - if (is_turbos && error?.details?.payout_per_point_choices && error?.details?.field === 'payout_per_point') { - const { payout_per_point_choices } = error.details; + if ( + is_turbos && + queryError?.details?.payout_per_point_choices && + Array.isArray(queryError.details.payout_per_point_choices) && + queryError?.details?.field === 'payout_per_point' + ) { + const { payout_per_point_choices } = queryError.details; const index = Math.floor(payout_per_point_choices.length / 2); dispatch({ type: 'SET_PROPOSAL_VALUES', - payload: { payout_per_point: payout_per_point_choices[index] }, + payload: { payout_per_point: payout_per_point_choices[index] as number }, }); return; } // Set proposal error - const new_error = error ? mapErrorMessage(error) : ''; + const new_error = queryError ? mapErrorMessage(queryError) : ''; const is_error_field_match = - ['amount', 'stake'].includes(error?.details?.field ?? '') || !error?.details?.field; + ['amount', 'stake'].includes(queryError?.details?.field ?? '') || !queryError?.details?.field; dispatch({ type: 'SET_STAKE_ERROR', payload: is_error_field_match ? new_error : '' }); // Handle old contracts with payout (Rise/Fall, Higher/Lower, Touch/No Touch, Digits) if (should_show_payout_details) { - const new_proposal = getProposalInfo(trade_store, response as Parameters[1]); + // Combine data and error to match getProposalInfo expected format + const combined_response = { + ...data, + error: queryError || undefined, + }; + const new_proposal = getProposalInfo(trade_store, combined_response); const { contract_payout, max_payout, error } = getPayoutInfo(new_proposal); dispatch({ @@ -264,20 +289,20 @@ const StakeInput = observer(({ onClose, is_open }: TStakeInput) => { payload: { ...(max_payout ? { max_payout } : {}), [`${contractType}_contract_payout`]: contract_payout || 0, - [`is_${contractType}_payout_exceeded`]: !!error && contract_payout > max_payout, + [`is_${contractType}_payout_exceeded`]: !!error && contract_payout > Number(max_payout), [`error_${contractType === 'first' ? 1 : 2}`]: error, }, }); } else { // Recovery for minimum and maximum allowed values in case of errors - if ((!details.min_stake || !details.max_stake) && error?.details) { - const { max_stake, min_stake } = error.details; + if ((!details.min_stake || !details.max_stake) && queryError?.details) { + const { max_stake, min_stake } = queryError.details; if (max_stake && min_stake) { dispatch({ type: 'UPDATE_DETAILS', payload: { - max_stake, - min_stake, + max_stake: max_stake as string | number, + min_stake: min_stake as string | number, }, }); } @@ -306,14 +331,14 @@ const StakeInput = observer(({ onClose, is_open }: TStakeInput) => { }; React.useEffect(() => { - if (response_1) handleProposalResponse(response_1, 'first'); + if (response_1 || error_1) handleProposalResponse(response_1, error_1 || undefined, 'first'); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [response_1]); + }, [response_1, error_1]); React.useEffect(() => { - if (response_2) handleProposalResponse(response_2, 'second'); + if (response_2 || error_2) handleProposalResponse(response_2, error_2 || undefined, 'second'); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [response_2]); + }, [response_2, error_2]); const getInputMessage = () => !!details.min_stake && @@ -435,6 +460,9 @@ const StakeInput = observer(({ onClose, is_open }: TStakeInput) => { content: , onAction: onSave, }} + isPrimaryButtonDisabled={ + is_loading_proposal || !!fe_stake_error || !!(should_show_stake_error && stake_error) + } /> ); diff --git a/packages/trader/src/AppV2/Containers/Trade/__tests__/trade.spec.tsx b/packages/trader/src/AppV2/Containers/Trade/__tests__/trade.spec.tsx index bb598a7d71..a4b914e938 100644 --- a/packages/trader/src/AppV2/Containers/Trade/__tests__/trade.spec.tsx +++ b/packages/trader/src/AppV2/Containers/Trade/__tests__/trade.spec.tsx @@ -31,6 +31,7 @@ jest.mock('@deriv/components', () => ({ })); jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), useLocalStorageData: jest.fn(() => [{ trade_page: false }]), })); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useActiveSymbols.spec.tsx b/packages/trader/src/AppV2/Hooks/__tests__/useActiveSymbols.spec.tsx index 2dba8904a0..0a8bf7d921 100644 --- a/packages/trader/src/AppV2/Hooks/__tests__/useActiveSymbols.spec.tsx +++ b/packages/trader/src/AppV2/Hooks/__tests__/useActiveSymbols.spec.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { CONTRACT_TYPES, TRADE_TYPES, WS } from '@deriv/shared'; +import { useQuery } from '@deriv/api'; +import { CONTRACT_TYPES, TRADE_TYPES } from '@deriv/shared'; import { mockStore } from '@deriv/stores'; import { waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import TraderProviders from '../../../trader-providers'; import useActiveSymbols from '../useActiveSymbols'; -import { invalidateDTraderCache } from '../useDtraderQuery'; const not_logged_in_active_symbols = [ { symbol: 'EURUSD', display_name: 'EUR/USD', exchange_is_open: 1 }, @@ -20,21 +20,19 @@ const logged_in_active_symbols = [ { symbol: '1HZ300', display_name: 'Volatility 300', exchange_is_open: 0 }, ]; +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useQuery: jest.fn(() => ({ + data: { + active_symbols: not_logged_in_active_symbols, + }, + error: null, + isLoading: false, + })), +})); + jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), - WS: { - authorized: { - send: jest.fn(() => { - return Promise.resolve({ - active_symbols: mocked_store.client.is_logged_in - ? logged_in_active_symbols - : not_logged_in_active_symbols, - error: null, - }); - }), - }, - send: jest.fn(() => Promise.resolve({ active_symbols: not_logged_in_active_symbols, error: null })), - }, pickDefaultSymbol: jest.fn(() => Promise.resolve('EURUSD')), })); @@ -82,15 +80,16 @@ describe('useActiveSymbols', () => { }); afterEach(() => { - invalidateDTraderCache([ - 'active_symbols', - mocked_store.client.loginid ?? '', - mocked_store.modules.trade.contract_type, - mocked_store.common.current_language, - ]); + jest.clearAllMocks(); }); it('should fetch active symbols when not logged in', async () => { + (useQuery as jest.Mock).mockReturnValue({ + data: { active_symbols: not_logged_in_active_symbols }, + error: null, + isLoading: false, + }); + const { result } = renderHook(() => useActiveSymbols(), { wrapper, }); @@ -100,8 +99,16 @@ describe('useActiveSymbols', () => { }); it('should fetch active symbols when logged in', async () => { mocked_store.client.is_logged_in = true; + mocked_store.client.loginid = 'CR123456'; mocked_store.modules.trade.active_symbols = logged_in_active_symbols; mocked_store.modules.trade.has_symbols_for_v2 = true; + + (useQuery as jest.Mock).mockReturnValue({ + data: { active_symbols: logged_in_active_symbols }, + error: null, + isLoading: false, + }); + const { result } = renderHook(() => useActiveSymbols(), { wrapper, }); @@ -109,76 +116,60 @@ describe('useActiveSymbols', () => { expect(result.current.activeSymbols).toEqual(logged_in_active_symbols); }); }); - it('should set active symbols from store when is_logged_in and contract_type are unchanged', async () => { - mocked_store.modules.trade.active_symbols = [{ symbol: 'fromStore' }]; + it('should return empty array when no response from query', async () => { + const storeSymbols = [{ symbol: 'fromStore' }]; + mocked_store.modules.trade.active_symbols = storeSymbols; mocked_store.modules.trade.has_symbols_for_v2 = true; + + (useQuery as jest.Mock).mockReturnValue({ + data: null, // No response yet + error: null, + isLoading: true, + }); + const { result } = renderHook(() => useActiveSymbols(), { wrapper, }); await waitFor(() => { - expect(result.current.activeSymbols).toEqual([{ symbol: 'fromStore' }]); + // Hook returns data from React Query, not from store + expect(result.current.activeSymbols).toEqual([]); + expect(result.current.isLoading).toBe(true); }); }); - it('should call active_symbols API for Vanillas if previous contract is not Vanillas', async () => { + it('should call useQuery with correct payload for Vanillas', async () => { mocked_store.modules.trade.is_vanilla = true; - const active_symbols_call_spy = jest.spyOn(WS.authorized, 'send'); - renderHook(() => useActiveSymbols(), { wrapper }); - await waitFor(() => { - expect(active_symbols_call_spy).toBeCalledWith({ - active_symbols: 'brief', - contract_type: [CONTRACT_TYPES.VANILLA.CALL, CONTRACT_TYPES.VANILLA.PUT], - }); - }); - }); - it('should not call active_symbols API for Vanillas if previous contract is Vanillas', async () => { - mocked_store.modules.trade.contract_type = 'vanillascall'; - mocked_store.modules.trade.is_vanilla = true; - const active_symbols_call_spy = jest.spyOn(WS.authorized, 'send'); renderHook(() => useActiveSymbols(), { wrapper }); - mocked_store.modules.trade.contract_type = 'vanillasput'; - await waitFor(() => { - expect(active_symbols_call_spy).toBeCalledTimes(1); - }); - }); - it('should call active_symbols API for Turbos if previous contract is not Turbos', async () => { - mocked_store.modules.trade.is_turbos = true; - const active_symbols_call_spy = jest.spyOn(WS.authorized, 'send'); - renderHook(() => useActiveSymbols(), { wrapper }); - - await waitFor(() => { - expect(active_symbols_call_spy).toBeCalledWith({ - active_symbols: 'brief', - contract_type: [CONTRACT_TYPES.TURBOS.LONG, CONTRACT_TYPES.TURBOS.SHORT], + expect(useQuery).toHaveBeenCalledWith('active_symbols', { + payload: { + active_symbols: 'brief', + contract_type: [CONTRACT_TYPES.VANILLA.CALL, CONTRACT_TYPES.VANILLA.PUT], + }, + options: { + cacheTime: 10 * 60 * 1000, + }, }); }); }); - it('should not call active_symbols API for Turbos if previous contract is Turbos', async () => { - mocked_store.modules.trade.contract_type = 'turboslong'; - mocked_store.modules.trade.is_turbos = true; - const active_symbols_call_spy = jest.spyOn(WS.authorized, 'send'); - renderHook(() => useActiveSymbols(), { wrapper }); - - mocked_store.modules.trade.contract_type = 'turbosshort'; - - await waitFor(() => { - expect(active_symbols_call_spy).toBeCalledTimes(1); - }); - }); - it('should call active_symbols API for Turbos if contract is changed', async () => { - mocked_store.modules.trade.contract_type = 'contract_type1'; + it('should call useQuery with correct payload for Turbos', async () => { mocked_store.modules.trade.is_turbos = true; - const active_symbols_call_spy = jest.spyOn(WS.authorized, 'send'); - renderHook(() => useActiveSymbols(), { wrapper }); - mocked_store.modules.trade.contract_type = 'contract_type2'; + renderHook(() => useActiveSymbols(), { wrapper }); await waitFor(() => { - expect(active_symbols_call_spy).toBeCalledTimes(2); + expect(useQuery).toHaveBeenCalledWith('active_symbols', { + payload: { + active_symbols: 'brief', + contract_type: [CONTRACT_TYPES.TURBOS.LONG, CONTRACT_TYPES.TURBOS.SHORT], + }, + options: { + cacheTime: 10 * 60 * 1000, + }, + }); }); }); }); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useContractsFor.spec.tsx b/packages/trader/src/AppV2/Hooks/__tests__/useContractsFor.spec.tsx index 30d37b2fb1..1e30df2a37 100644 --- a/packages/trader/src/AppV2/Hooks/__tests__/useContractsFor.spec.tsx +++ b/packages/trader/src/AppV2/Hooks/__tests__/useContractsFor.spec.tsx @@ -1,25 +1,28 @@ import React from 'react'; -import { cloneObject, getContractCategoriesConfig, getContractTypesConfig, WS } from '@deriv/shared'; +import { useQuery } from '@deriv/api'; +import { cloneObject, getContractCategoriesConfig, getContractTypesConfig } from '@deriv/shared'; import { mockStore } from '@deriv/stores'; import { waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import TraderProviders from '../../../trader-providers'; import useContractsFor from '../useContractsFor'; -import { invalidateDTraderCache } from '../useDtraderQuery'; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useQuery: jest.fn(() => ({ + data: null, + error: null, + isLoading: false, + })), +})); jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), getContractCategoriesConfig: jest.fn(), getContractTypesConfig: jest.fn(), cloneObject: jest.fn(), - WS: { - send: jest.fn(), - authorized: { - send: jest.fn(), - }, - }, })); describe('useContractsFor', () => { @@ -62,18 +65,22 @@ describe('useContractsFor', () => { }); afterEach(() => { - invalidateDTraderCache(['contracts_for', mocked_store.client.loginid ?? '', mocked_store.modules.trade.symbol]); + jest.clearAllMocks(); }); it('should fetch and set contract types for the company successfully', async () => { - WS.authorized.send.mockResolvedValue({ - contracts_for: { - available: [ - { contract_type: 'type_1', underlying_symbol: 'EURUSD' }, - { contract_type: 'type_2', underlying_symbol: 'GBPUSD' }, - ], - hit_count: 2, + (useQuery as jest.Mock).mockReturnValue({ + data: { + contracts_for: { + available: [ + { contract_type: 'type_1', underlying_symbol: 'EURUSD', default_stake: 10 }, + { contract_type: 'type_2', underlying_symbol: 'GBPUSD', default_stake: 20 }, + ], + hit_count: 2, + }, }, + error: null, + isLoading: false, }); const { result } = renderHook(() => useContractsFor(), { wrapper }); @@ -91,8 +98,10 @@ describe('useContractsFor', () => { }); it('should handle API errors gracefully', async () => { - WS.authorized.send.mockResolvedValue({ + (useQuery as jest.Mock).mockReturnValue({ + data: null, error: { message: 'Some error' }, + isLoading: false, }); const { result } = renderHook(() => useContractsFor(), { wrapper }); @@ -104,100 +113,91 @@ describe('useContractsFor', () => { }); it('should not set unsupported contract types', async () => { - WS.authorized.send.mockResolvedValue({ - contracts_for: { - available: [{ contract_type: 'unsupported_type', underlying_symbol: 'UNSUPPORTED' }], - hit_count: 1, + (useQuery as jest.Mock).mockReturnValue({ + data: { + contracts_for: { + available: [{ contract_type: 'unsupported_type', underlying_symbol: 'UNSUPPORTED' }], + hit_count: 1, + }, }, + error: null, + isLoading: false, }); const { result } = renderHook(() => useContractsFor(), { wrapper }); await waitFor(() => { - expect(result.current.contract_types_list).toEqual([]); - expect(mocked_store.modules.trade.setContractTypesListV2).not.toHaveBeenCalled(); + expect(result.current.trade_types).toEqual([]); }); }); describe('Symbol validation fix', () => { - it('should prevent API calls when symbol is undefined', async () => { - // Set up store with undefined symbol + it('should prevent query when symbol is undefined', async () => { mocked_store.modules.trade.symbol = undefined; - const { result } = renderHook(() => useContractsFor(), { wrapper }); - - // Wait a bit to ensure no API call is made - await new Promise(resolve => setTimeout(resolve, 100)); + renderHook(() => useContractsFor(), { wrapper }); - // Verify that no API call was made - expect(WS.authorized.send).not.toHaveBeenCalled(); - expect(result.current.contract_types_list).toEqual([]); + await waitFor(() => { + expect(useQuery).toHaveBeenCalledWith( + 'contracts_for', + expect.objectContaining({ + options: expect.objectContaining({ + enabled: false, + }), + }) + ); + }); }); - it('should prevent API calls when symbol is null', async () => { - // Set up store with null symbol + it('should prevent query when symbol is null', async () => { mocked_store.modules.trade.symbol = null; - const { result } = renderHook(() => useContractsFor(), { wrapper }); + renderHook(() => useContractsFor(), { wrapper }); - // Wait a bit to ensure no API call is made - await new Promise(resolve => setTimeout(resolve, 100)); - - // Verify that no API call was made - expect(WS.authorized.send).not.toHaveBeenCalled(); - expect(result.current.contract_types_list).toEqual([]); + await waitFor(() => { + expect(useQuery).toHaveBeenCalledWith( + 'contracts_for', + expect.objectContaining({ + options: expect.objectContaining({ + enabled: false, + }), + }) + ); + }); }); - it('should prevent API calls when symbol is empty string', async () => { - // Set up store with empty string symbol + it('should prevent query when symbol is empty string', async () => { mocked_store.modules.trade.symbol = ''; - const { result } = renderHook(() => useContractsFor(), { wrapper }); - - // Wait a bit to ensure no API call is made - await new Promise(resolve => setTimeout(resolve, 100)); - - // Verify that no API call was made - expect(WS.authorized.send).not.toHaveBeenCalled(); - expect(result.current.contract_types_list).toEqual([]); - }); - - it('should allow API calls when symbol is present regardless of loginid status', async () => { - // Set up store with undefined loginid but valid symbol - mocked_store.client.loginid = undefined; - mocked_store.modules.trade.symbol = 'R_100'; - - WS.authorized.send.mockResolvedValue({ - contracts_for: { - available: [{ contract_type: 'type_1', underlying_symbol: 'R_100' }], - hit_count: 1, - }, - }); - - const { result } = renderHook(() => useContractsFor(), { wrapper }); + renderHook(() => useContractsFor(), { wrapper }); await waitFor(() => { - // Verify that API call was made with correct symbol - expect(WS.authorized.send).toHaveBeenCalledWith( + expect(useQuery).toHaveBeenCalledWith( + 'contracts_for', expect.objectContaining({ - contracts_for: 'R_100', + options: expect.objectContaining({ + enabled: false, + }), }) ); }); }); - it('should prevent API calls when no symbol is provided', async () => { - // Set up store with no symbol - mocked_store.modules.trade.symbol = ''; - - const { result } = renderHook(() => useContractsFor(), { wrapper }); + it('should allow query when symbol is present', async () => { + mocked_store.modules.trade.symbol = 'R_100'; - // Wait a bit to ensure no API call is made - await new Promise(resolve => setTimeout(resolve, 100)); + renderHook(() => useContractsFor(), { wrapper }); - // Verify that no API call was made due to switching - expect(WS.authorized.send).not.toHaveBeenCalled(); - expect(result.current.contract_types_list).toEqual([]); + await waitFor(() => { + expect(useQuery).toHaveBeenCalledWith('contracts_for', { + payload: { + contracts_for: 'R_100', + }, + options: { + enabled: true, + }, + }); + }); }); }); }); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useDTraderQuery.spec.ts b/packages/trader/src/AppV2/Hooks/__tests__/useDTraderQuery.spec.ts deleted file mode 100644 index 83977d423f..0000000000 --- a/packages/trader/src/AppV2/Hooks/__tests__/useDTraderQuery.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { WS } from '@deriv/shared'; -import { act, renderHook } from '@testing-library/react-hooks'; - -import { invalidateDTraderCache, useDtraderQuery } from '../useDtraderQuery'; - -jest.mock('@deriv/shared', () => ({ - WS: { - send: jest.fn(), - authorized: { - send: jest.fn(), - }, - }, -})); - -describe('useDtraderQuery', () => { - const mockRequest = { some_request: 'test' }; - const mockResponse = { data: 'some data' }; - const mockError = { message: 'An error occurred' }; - - beforeEach(() => { - invalidateDTraderCache(['some-key']); - jest.clearAllMocks(); - }); - - it('should return initial state correctly', () => { - const { result } = renderHook(() => useDtraderQuery(['some-key'], mockRequest)); - - expect(result.current.data).toBeNull(); - expect(result.current.error).toBeNull(); - expect(result.current.is_fetching).toBe(true); - }); - - it('should fetch data successfully', async () => { - (WS.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - const { result, waitForNextUpdate } = renderHook(() => - useDtraderQuery(['some-key'], mockRequest, { wait_for_authorize: false }) - ); - - await waitForNextUpdate(); - - expect(WS.send).toHaveBeenCalledWith(mockRequest); - expect(result.current.data).toEqual(mockResponse); - expect(result.current.error).toBeNull(); - expect(result.current.is_fetching).toBe(false); - }); - - it('should fetch data using authorized send when wait_for_authorize is true', async () => { - (WS.authorized.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - const { result, waitForNextUpdate } = renderHook(() => - useDtraderQuery(['some-key'], mockRequest, { wait_for_authorize: true }) - ); - - await waitForNextUpdate(); - - expect(WS.authorized.send).toHaveBeenCalledWith(mockRequest); - expect(result.current.data).toEqual(mockResponse); - expect(result.current.error).toBeNull(); - expect(result.current.is_fetching).toBe(false); - }); - - it('should handle errors correctly', async () => { - (WS.authorized.send as jest.Mock).mockRejectedValueOnce(mockError); - - const { result, waitForNextUpdate } = renderHook(() => useDtraderQuery(['some-key'], mockRequest)); - - await waitForNextUpdate(); - - expect(result.current.data).toBeNull(); - expect(result.current.error).toEqual(mockError); - expect(result.current.is_fetching).toBe(false); - }); - - it('should cache the result', async () => { - (WS.authorized.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - const { result, waitForNextUpdate } = renderHook(() => useDtraderQuery(['some-key'], mockRequest)); - - await waitForNextUpdate(); - - expect(result.current.data).toEqual(mockResponse); - expect(result.current.is_fetching).toBe(false); - - const { result: cachedResult } = renderHook(() => useDtraderQuery(['some-key'], mockRequest)); - - expect(cachedResult.current.data).toEqual(mockResponse); - expect(cachedResult.current.is_fetching).toBe(false); - expect(WS.authorized.send).toHaveBeenCalledTimes(1); - }); - - it('should refetch data on calling refetch', async () => { - (WS.authorized.send as jest.Mock).mockResolvedValueOnce(mockResponse); - - const { result, waitForNextUpdate } = renderHook(() => useDtraderQuery(['some-key'], mockRequest)); - - await waitForNextUpdate(); - - expect(result.current.data).toEqual(mockResponse); - - const newMockResponse = { data: 'new data' }; - (WS.authorized.send as jest.Mock).mockResolvedValueOnce(newMockResponse); - - act(() => { - result.current.refetch(); - }); - - await waitForNextUpdate(); - - expect(result.current.data).toEqual(newMockResponse); - }); - - it('should not fetch data if enabled is false', () => { - const { result } = renderHook(() => useDtraderQuery(['some-key'], mockRequest, { enabled: false })); - - expect(result.current.data).toBeNull(); - expect(result.current.is_fetching).toBe(false); - expect(WS.send).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useFetchProposalData.spec.tsx b/packages/trader/src/AppV2/Hooks/__tests__/useFetchProposalData.spec.tsx deleted file mode 100644 index ce344ea647..0000000000 --- a/packages/trader/src/AppV2/Hooks/__tests__/useFetchProposalData.spec.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; - -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook } from '@testing-library/react-hooks'; - -import { getProposalRequestObject } from 'AppV2/Utils/trade-params-utils'; -import { useTraderStore } from 'Stores/useTraderStores'; -import type { TTradeStore } from 'Types'; - -import { useFetchProposalData } from '../useFetchProposalData'; - -jest.mock('Stores/useTraderStores', () => ({ - useTraderStore: jest.fn(), -})); -jest.mock('@deriv/shared', () => ({ - WS: { - send: jest.fn(), - authorized: { - send: jest.fn(), - }, - }, -})); -jest.mock('AppV2/Utils/trade-params-utils', () => ({ - getProposalRequestObject: jest.fn(), -})); - -const mockUseTraderStore = useTraderStore as jest.Mock; -const mockGetProposalRequestObject = getProposalRequestObject as jest.Mock; - -describe('useFetchProposalData', () => { - let queryClient: QueryClient, - wrapper: React.FC<{ children: React.ReactNode }>, - mockTradeStore: Partial; - - beforeEach(() => { - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - cacheTime: 0, - }, - }, - }); - wrapper = ({ children }) => {children}; - - mockTradeStore = { - amount: 10, - basis: 'stake', - currency: 'USD', - duration: 5, - duration_unit: 'm', - symbol: 'frxEURUSD', - trade_types: { - CALL: 'Rise', - PUT: 'Fall', - }, - }; - - mockUseTraderStore.mockReturnValue(mockTradeStore); - }); - - afterEach(() => { - jest.clearAllMocks(); - queryClient.clear(); - }); - - it('calls getProposalRequestObject with correct parameters', () => { - const proposal_request_values = { - amount: 20, - barrier_1: '100', - }; - const contract_type = 'CALL'; - const contract_types = ['CALL', 'PUT']; - - mockGetProposalRequestObject.mockReturnValue({ - proposal: 1, - subscribe: 1, - amount: 20, - barrier: '100', - contract_type: 'CALL', - }); - - renderHook( - () => - useFetchProposalData({ - trade_store: mockTradeStore as TTradeStore, - proposal_request_values, - contract_type, - contract_types, - is_enabled: true, - }), - { wrapper } - ); - - expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ - new_values: proposal_request_values, - trade_store: mockTradeStore, - trade_type: contract_type, - }); - }); - - it('returns correct flag if is_enabled is false', () => { - const proposal_request_values = { - amount: 20, - }; - const contract_type = 'CALL'; - const contract_types = ['CALL', 'PUT']; - - mockGetProposalRequestObject.mockReturnValue({ - proposal: 1, - subscribe: 1, - amount: 20, - contract_type: 'CALL', - }); - - const { result } = renderHook( - () => - useFetchProposalData({ - trade_store: mockTradeStore as TTradeStore, - proposal_request_values, - contract_type, - contract_types, - is_enabled: false, - }), - { wrapper } - ); - - expect(result.current.is_fetching).toBe(false); - }); - - it('handles empty proposal_request_values', () => { - const contract_type = 'CALL'; - const contract_types = ['CALL', 'PUT']; - - mockGetProposalRequestObject.mockReturnValue({ - proposal: 1, - subscribe: 1, - contract_type: 'CALL', - }); - - const { result } = renderHook( - () => - useFetchProposalData({ - trade_store: mockTradeStore as TTradeStore, - proposal_request_values: {}, - contract_type, - contract_types, - is_enabled: true, - }), - { wrapper } - ); - - const { data, error } = result.current; - expect(data).toBe(null); - expect(error).toBe(null); - }); -}); diff --git a/packages/trader/src/AppV2/Hooks/__tests__/useProposal.spec.tsx b/packages/trader/src/AppV2/Hooks/__tests__/useProposal.spec.tsx new file mode 100644 index 0000000000..267fe4155a --- /dev/null +++ b/packages/trader/src/AppV2/Hooks/__tests__/useProposal.spec.tsx @@ -0,0 +1,359 @@ +import React from 'react'; + +import { APIProvider } from '@deriv/api'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; + +import { getProposalRequestObject } from 'AppV2/Utils/trade-params-utils'; +import { useTraderStore } from 'Stores/useTraderStores'; +import type { TTradeStore } from 'Types'; + +import { useProposal } from '../useProposal'; + +jest.mock('Stores/useTraderStores', () => ({ + useTraderStore: jest.fn(), +})); +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + WS: { + send: jest.fn(), + authorized: { + send: jest.fn(), + }, + }, + useWS: jest.fn(() => ({ + send: jest.fn(), + authorized: { + send: jest.fn(), + }, + })), +})); +jest.mock('AppV2/Utils/trade-params-utils', () => ({ + getProposalRequestObject: jest.fn(), +})); + +const mockUseTraderStore = useTraderStore as jest.Mock; +const mockGetProposalRequestObject = getProposalRequestObject as jest.Mock; + +describe('useProposal', () => { + let queryClient: QueryClient, + wrapper: React.FC<{ children: React.ReactNode }>, + mockTradeStore: Partial; + + beforeEach(() => { + queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, + }); + wrapper = ({ children }) => ( + + {children} + + ); + + mockTradeStore = { + amount: 10, + basis: 'stake', + currency: 'USD', + duration: 5, + duration_unit: 'm', + symbol: 'frxEURUSD', + trade_types: { + CALL: 'Rise', + PUT: 'Fall', + }, + }; + + mockUseTraderStore.mockReturnValue(mockTradeStore); + }); + + afterEach(() => { + jest.clearAllMocks(); + queryClient.clear(); + }); + + it('calls getProposalRequestObject with correct parameters', () => { + const proposal_request_values = { + amount: 20, + barrier_1: '100', + }; + const contract_type = 'CALL'; + const contract_types = ['CALL', 'PUT']; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + amount: 20, + barrier: '100', + contract_type: 'CALL', + }); + + renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: true, + }), + { wrapper } + ); + + expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ + new_values: proposal_request_values, + trade_store: mockTradeStore, + trade_type: contract_type, + }); + }); + + it('returns correct flag if is_enabled is false', () => { + const proposal_request_values = { + amount: 20, + }; + const contract_type = 'CALL'; + const contract_types = ['CALL', 'PUT']; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + amount: 20, + contract_type: 'CALL', + }); + + const { result } = renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: false, + }), + { wrapper } + ); + + expect(result.current.isFetching).toBe(false); + }); + + it('handles empty proposal_request_values', () => { + const contract_type = 'CALL'; + const contract_types = ['CALL', 'PUT']; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + contract_type: 'CALL', + }); + + const { result } = renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values: {}, + contract_type, + is_enabled: true, + }), + { wrapper } + ); + + const { data, error } = result.current; + expect(data).toBeUndefined(); + expect(error).toBe(null); + }); + + it('removes take_profit from limit_order when should_skip_validation is "take_profit"', () => { + const proposal_request_values = { + amount: 20, + has_take_profit: true, + take_profit: '50', + has_stop_loss: true, + stop_loss: '10', + }; + const contract_type = 'CALL'; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + amount: 20, + contract_type: 'CALL', + limit_order: { + take_profit: '50', + stop_loss: '10', + }, + }); + + renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: true, + should_skip_validation: 'take_profit', + }), + { wrapper } + ); + + // Verify that getProposalRequestObject was called + expect(mockGetProposalRequestObject).toHaveBeenCalled(); + + // The memoized function should have removed take_profit from limit_order + // We can't directly test the mutation, but we verify the function was called with correct params + expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ + new_values: proposal_request_values, + trade_store: mockTradeStore, + trade_type: contract_type, + }); + }); + + it('removes stop_loss from limit_order when should_skip_validation is "stop_loss"', () => { + const proposal_request_values = { + amount: 20, + has_take_profit: true, + take_profit: '50', + has_stop_loss: true, + stop_loss: '10', + }; + const contract_type = 'CALL'; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + amount: 20, + contract_type: 'CALL', + limit_order: { + take_profit: '50', + stop_loss: '10', + }, + }); + + renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: true, + should_skip_validation: 'stop_loss', + }), + { wrapper } + ); + + expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ + new_values: proposal_request_values, + trade_store: mockTradeStore, + trade_type: contract_type, + }); + }); + + it('handles trade_store with missing/undefined values', () => { + const incompleteTradeStore = { + symbol: 'frxEURUSD', + // Missing other required fields + } as Partial; + + const proposal_request_values = { + amount: 20, + }; + const contract_type = 'CALL'; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + contract_type: 'CALL', + }); + + renderHook( + () => + useProposal({ + trade_store: incompleteTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: true, + }), + { wrapper } + ); + + // Should still call getProposalRequestObject even with incomplete store + expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ + new_values: proposal_request_values, + trade_store: incompleteTradeStore, + trade_type: contract_type, + }); + }); + + it('handles undefined values in proposal_request_values', () => { + const proposal_request_values = { + amount: undefined, + barrier_1: undefined, + take_profit: undefined, + }; + const contract_type = 'CALL'; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + contract_type: 'CALL', + }); + + const { result } = renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: true, + }), + { wrapper } + ); + + expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ + new_values: proposal_request_values, + trade_store: mockTradeStore, + trade_type: contract_type, + }); + }); + + it('does not remove limit_order properties when should_skip_validation is undefined', () => { + const proposal_request_values = { + amount: 20, + has_take_profit: true, + take_profit: '50', + has_stop_loss: true, + stop_loss: '10', + }; + const contract_type = 'CALL'; + + mockGetProposalRequestObject.mockReturnValue({ + proposal: 1, + subscribe: 1, + amount: 20, + contract_type: 'CALL', + limit_order: { + take_profit: '50', + stop_loss: '10', + }, + }); + + renderHook( + () => + useProposal({ + trade_store: mockTradeStore as TTradeStore, + proposal_request_values, + contract_type, + is_enabled: true, + // should_skip_validation is undefined + }), + { wrapper } + ); + + expect(mockGetProposalRequestObject).toHaveBeenCalledWith({ + new_values: proposal_request_values, + trade_store: mockTradeStore, + trade_type: contract_type, + }); + }); +}); diff --git a/packages/trader/src/AppV2/Hooks/useActiveSymbols.ts b/packages/trader/src/AppV2/Hooks/useActiveSymbols.ts index 0054a5ce66..a70b94ec80 100644 --- a/packages/trader/src/AppV2/Hooks/useActiveSymbols.ts +++ b/packages/trader/src/AppV2/Hooks/useActiveSymbols.ts @@ -1,90 +1,46 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect } from 'react'; -import { TActiveSymbolsResponse } from '@deriv/api'; -import { CONTRACT_TYPES, getContractTypesConfig, isTurbosContract, isVanillaContract } from '@deriv/shared'; +import { TActiveSymbolsRequest, useQuery } from '@deriv/api'; +import { CONTRACT_TYPES, getContractTypesConfig } from '@deriv/shared'; import { useStore } from '@deriv/stores'; import { localize } from '@deriv-com/translations'; import { useTraderStore } from 'Stores/useTraderStores'; -import { useDtraderQuery } from './useDtraderQuery'; +type TContractTypesList = NonNullable; -// LocalStorage persistence for navigation -const STORAGE_KEY = 'dtrader_v2_active_symbols'; -const EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes - -const getStoredSymbols = () => { - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - const { symbols, timestamp } = JSON.parse(stored); - if (Date.now() - timestamp < EXPIRY_TIME) return symbols; - } - } catch { - // Ignore localStorage errors (e.g., quota exceeded, disabled) - } - return null; -}; - -const storeSymbols = (symbols: NonNullable) => { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ symbols, timestamp: Date.now() })); - } catch { - // Ignore localStorage errors (e.g., quota exceeded, disabled) - } -}; +// Cache configuration for active symbols query +const ACTIVE_SYMBOLS_CACHE_CONFIG = { + CACHE_TIME: 10 * 60 * 1000, // 10 minutes - keep in cache even if unused +} as const; +/** + * Hook to fetch and manage active symbols for trading + */ const useActiveSymbols = () => { - const { client, common } = useStore(); - const { loginid } = client; - const { showError, current_language } = common; - const { - active_symbols: symbols_from_store, - contract_type, - is_vanilla, - is_turbos, - has_symbols_for_v2, - setActiveSymbolsV2, - } = useTraderStore(); - const [activeSymbols, setActiveSymbols] = useState | []>( - () => { - const stored = getStoredSymbols(); - return stored?.length ? stored : symbols_from_store || []; - } - ); - - const getContractTypesList = () => { - if (is_turbos) return [CONTRACT_TYPES.TURBOS.LONG, CONTRACT_TYPES.TURBOS.SHORT]; - if (is_vanilla) return [CONTRACT_TYPES.VANILLA.CALL, CONTRACT_TYPES.VANILLA.PUT]; - return getContractTypesConfig()[contract_type]?.trade_types ?? []; - }; - - const isQueryEnabled = useCallback(() => { - // Remove dependency on available_contract_types to break circular dependency - // Active symbols should load independently to provide data for other hooks - // Removed switching logic for single account model - return true; - }, []); - - const getContractType = () => { - if (isTurbosContract(contract_type)) { - return 'turbos'; - } else if (isVanillaContract(contract_type)) { - return 'vanilla'; - } - return contract_type; + const { common } = useStore(); + const { showError } = common; + const { contract_type, is_vanilla, is_turbos, setActiveSymbolsV2 } = useTraderStore(); + + const getContractTypesList = (): TContractTypesList => { + if (is_turbos) return [CONTRACT_TYPES.TURBOS.LONG, CONTRACT_TYPES.TURBOS.SHORT] as TContractTypesList; + if (is_vanilla) return [CONTRACT_TYPES.VANILLA.CALL, CONTRACT_TYPES.VANILLA.PUT] as TContractTypesList; + return (getContractTypesConfig()[contract_type]?.trade_types ?? []) as TContractTypesList; }; - const { data: response, error: queryError } = useDtraderQuery( - ['active_symbols', loginid ?? '', getContractType(), current_language], - { + const { + data: response, + error: queryError, + isLoading, + } = useQuery('active_symbols', { + payload: { active_symbols: 'brief', contract_type: getContractTypesList(), }, - { - enabled: isQueryEnabled(), - } - ); + options: { + cacheTime: ACTIVE_SYMBOLS_CACHE_CONFIG.CACHE_TIME, + }, + }); // Handle query errors useEffect(() => { @@ -93,40 +49,26 @@ const useActiveSymbols = () => { } }, [queryError, showError]); - // Use store symbols when available and valid, but only for unchanged contract types - useEffect(() => { - if (has_symbols_for_v2 && symbols_from_store?.length && !response) { - setActiveSymbols(symbols_from_store); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [has_symbols_for_v2, response]); - + // Update MobX store when data is received (for trade-store internal operations) useEffect(() => { if (!response) return; - const { active_symbols = [], error } = response; + const { active_symbols = [] } = response; - if (error || !active_symbols?.length) { - // Fallback to stored or store symbols - const stored = getStoredSymbols(); - const fallback = stored?.length ? stored : symbols_from_store; - if (fallback?.length) { - setActiveSymbols(fallback); - setActiveSymbolsV2(fallback); - } else { - showError({ message: localize('Trading is unavailable at this time.') }); - setActiveSymbols([]); - } + if (!active_symbols?.length) { + showError({ message: localize('Trading is unavailable at this time.') }); + setActiveSymbolsV2([]); } else { - // Success: store and update - storeSymbols(active_symbols); - setActiveSymbols(active_symbols); + // Update store with fresh data setActiveSymbolsV2(active_symbols); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [response]); - return { activeSymbols }; + return { + activeSymbols: response?.active_symbols || [], + isLoading, + }; }; export default useActiveSymbols; diff --git a/packages/trader/src/AppV2/Hooks/useContractsFor.ts b/packages/trader/src/AppV2/Hooks/useContractsFor.ts index 0abf2fd304..61740e7222 100644 --- a/packages/trader/src/AppV2/Hooks/useContractsFor.ts +++ b/packages/trader/src/AppV2/Hooks/useContractsFor.ts @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useRef } from 'react'; +import { useQuery } from '@deriv/api'; import { cloneObject, getContractCategoriesConfig, getContractTypesConfig, setTradeURLParams } from '@deriv/shared'; import { useStore } from '@deriv/stores'; @@ -9,24 +10,6 @@ import { TContractType } from 'Modules/Trading/Components/Form/ContractType/type import { useTraderStore } from 'Stores/useTraderStores'; import { TConfig, TContractTypesList } from 'Types'; -import { useDtraderQuery } from './useDtraderQuery'; - -type TContractsForResponse = { - contracts_for: { - available: { - contract_category: string; - contract_type: string; - default_stake: number; - sentiment: string; - underlying_symbol?: string; // New field (symbol → underlying_symbol) - barrier?: string; - barriers?: number; - exchange_name?: string; - }[]; - hit_count: number; - }; -}; - const useContractsFor = () => { const [contract_types_list, setContractTypesList] = React.useState([]); @@ -69,16 +52,15 @@ const useContractsFor = () => { const { data: response, error, - is_fetching, - } = useDtraderQuery( - ['contracts_for', loginid ?? '', underlying_symbol], - { + isLoading, + } = useQuery('contracts_for', { + payload: { contracts_for: underlying_symbol, // Use underlying_symbol from active_symbols lookup }, - { + options: { enabled: isQueryEnabled(), - } - ); + }, + }); const contract_categories = getContractCategoriesConfig(); const available_categories = cloneObject(contract_categories); @@ -87,7 +69,7 @@ const useContractsFor = () => { ReturnType | undefined >(); - const is_fetching_ref = useRef(is_fetching); + const is_fetching_ref = useRef(isLoading); const isContractTypeAvailable = useCallback( (trade_types: TContractType[]) => { @@ -140,7 +122,7 @@ const useContractsFor = () => { try { const { contracts_for } = response || {}; const available_contract_types: ReturnType = {}; - is_fetching_ref.current = false; + is_fetching_ref.current = isLoading; if (!error && contracts_for?.available.length) { contracts_for.available.forEach(contract => { diff --git a/packages/trader/src/AppV2/Hooks/useDtraderQuery.ts b/packages/trader/src/AppV2/Hooks/useDtraderQuery.ts deleted file mode 100644 index 05a920e4de..0000000000 --- a/packages/trader/src/AppV2/Hooks/useDtraderQuery.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; - -import { WS } from '@deriv/shared'; - -import { TServerError } from 'Types'; - -type QueryResult = { - data: null | T; - error: TServerError | null; - is_fetching: boolean; - refetch: () => void; -}; - -type QueryOptions = { - wait_for_authorize?: boolean; - enabled?: boolean; - timeout?: number; // timeout in milliseconds, default 30 seconds -}; - -// Cache object to store the results -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const cache: Record = {}; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const ongoing_requests: Record | undefined> = {}; - -const getKey = (keys: string | string[]) => (Array.isArray(keys) ? keys.join('-') : keys); - -export const useDtraderQuery = ( - keys: string | string[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - request: Record, - options: QueryOptions = {} -): QueryResult => { - const key = getKey(keys); - const { enabled = true, timeout: _timeout = 30000 } = options; - const [data, setData] = useState(cache[key] ?? null); - const [error, setError] = useState(null); - const [is_fetching, setIsFetching] = useState(!(key in cache) && enabled); - const is_mounted = useRef(false); - const request_string = JSON.stringify(request); - - const { wait_for_authorize = true } = options; - - useEffect(() => { - is_mounted.current = true; - - return () => { - is_mounted.current = false; - }; - }, []); - - const fetchData = useCallback(() => { - setIsFetching(true); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let send_promise: Promise | undefined; - - if (ongoing_requests[key]) { - send_promise = ongoing_requests[key]; - } else { - const request = JSON.parse(request_string); - send_promise = wait_for_authorize ? WS.authorized.send(request) : WS.send(request); - ongoing_requests[key] = send_promise; - } - - // Add timeout to the promise - const timeout_ms = options?.timeout || 30000; // Default 30 seconds - const timeout_promise = new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Request timeout after ${timeout_ms}ms`)), timeout_ms); - }); - - Promise.race([send_promise, timeout_promise]) - ?.then((result: Response) => { - if (!is_mounted.current) return; - - cache[key] = result; - setData(result); - setIsFetching(false); - }) - .catch((err: TServerError | Error) => { - if (!is_mounted.current) return; - - setData(null); - setError(err as TServerError); - setIsFetching(false); - }) - .finally(() => { - delete ongoing_requests[key]; - }); - }, [key, request_string, wait_for_authorize, options?.timeout]); - - useEffect(() => { - if (enabled && !(key in cache)) { - fetchData(); - } - }, [key, fetchData, enabled]); - - useEffect(() => { - if (enabled && data !== cache[key]) { - setData(cache[key] ?? null); - } - }, [enabled, key, data]); - - const refetch = useCallback(() => { - delete cache[key]; - fetchData(); - }, [fetchData, key]); - - return { data, error, is_fetching, refetch }; -}; - -export const invalidateDTraderCache = (keys: string | string[]) => { - const key = getKey(keys); - delete cache[key]; -}; diff --git a/packages/trader/src/AppV2/Hooks/useFetchProposalData.tsx b/packages/trader/src/AppV2/Hooks/useFetchProposalData.tsx deleted file mode 100644 index d0dd923bd8..0000000000 --- a/packages/trader/src/AppV2/Hooks/useFetchProposalData.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -import { getProposalRequestObject } from 'AppV2/Utils/trade-params-utils'; -import { useTraderStore } from 'Stores/useTraderStores'; -import { TTradeStore } from 'Types'; - -import { useDtraderQuery } from './useDtraderQuery'; - -type TOnProposalResponse = TTradeStore['onProposalResponse']; -type TNewValues = { - amount?: string | number; - payout_per_point?: string | number; - barrier_1?: string | number; -}; -// TODO: We can reuse it in TakeProfitAndStopLoss and PayoutPerPoint components. -export const useFetchProposalData = ({ - trade_store, - proposal_request_values, - contract_type, - contract_types, - is_enabled, -}: { - trade_store: ReturnType; - proposal_request_values: TNewValues; - contract_type: string; - contract_types: string[]; - is_enabled?: boolean; -}) => { - const proposal_request = getProposalRequestObject({ - new_values: proposal_request_values, - trade_store, - trade_type: contract_type, - }); - - const entries = proposal_request_values ? Object.entries(proposal_request_values) : []; - const query_key = [ - 'proposal', - ...entries.flat().join('-'), - `${proposal_request_values?.amount ?? ''}`, - JSON.stringify(proposal_request), - contract_types.join('-'), - ]; - - return useDtraderQuery[0]>(query_key, proposal_request, { - enabled: is_enabled, - }); -}; diff --git a/packages/trader/src/AppV2/Hooks/useProposal.tsx b/packages/trader/src/AppV2/Hooks/useProposal.tsx new file mode 100644 index 0000000000..b146443f15 --- /dev/null +++ b/packages/trader/src/AppV2/Hooks/useProposal.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { TPriceProposalRequest, useQuery } from '@deriv/api'; + +import { getProposalRequestObject } from 'AppV2/Utils/trade-params-utils'; +import { useTraderStore } from 'Stores/useTraderStores'; + +type TNewValues = { + amount?: string | number; + payout_per_point?: string | number; + barrier_1?: string | number; + has_take_profit?: boolean; + has_stop_loss?: boolean; + take_profit?: string | number; + stop_loss?: string | number; +}; + +export const useProposal = ({ + trade_store, + proposal_request_values, + contract_type, + is_enabled, + should_skip_validation, +}: { + trade_store: ReturnType; + proposal_request_values: TNewValues; + contract_type: string; + is_enabled?: boolean; + should_skip_validation?: 'take_profit' | 'stop_loss'; +}) => { + const proposal_request = React.useMemo(() => { + const request = getProposalRequestObject({ + new_values: proposal_request_values, + trade_store, + trade_type: contract_type, + }); + + // We need to exclude tp in case if validating sl and vice versa to validate them independently + if (should_skip_validation === 'take_profit' && request.limit_order?.take_profit) { + delete request.limit_order.take_profit; + } + if (should_skip_validation === 'stop_loss' && request.limit_order?.stop_loss) { + delete request.limit_order.stop_loss; + } + + return request; + }, [proposal_request_values, trade_store, contract_type, should_skip_validation]); + + return useQuery('proposal', { + payload: proposal_request as TPriceProposalRequest, + options: { + enabled: is_enabled, + cacheTime: 0, + retry: false, + }, + }); +}; diff --git a/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts b/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts index 4452ba0e18..83a123dd2f 100644 --- a/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts +++ b/packages/trader/src/Stores/Modules/Trading/Helpers/proposal.ts @@ -1,4 +1,4 @@ -import { TPriceProposalResponse } from '@deriv/api'; +import { TPriceProposalResponse, TSocketError } from '@deriv/api'; import { convertToUnix, getDecimalPlaces, @@ -11,7 +11,7 @@ import { TRADE_TYPES, } from '@deriv/shared'; -import { TError, TTradeStore } from 'Types'; +import { TTradeStore } from 'Types'; import { isRiseFallContractType } from './allow-equals'; @@ -83,8 +83,7 @@ export const getProposalErrorField = (response: TPriceProposalResponse) => { export const getProposalInfo = ( store: TTradeStore, - response: TPriceProposalResponse & TError, - obj_prev_contract_basis?: TObjContractBasis + response: TPriceProposalResponse & { error?: TSocketError<'proposal'>['error'] } ) => { const proposal: ExpandedProposal = response.proposal || ({} as ExpandedProposal); const profit = (proposal.payout || 0) - (proposal.ask_price || 0); diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.ts b/packages/trader/src/Stores/Modules/Trading/trade-store.ts index 16d4e177e6..b8a721bc26 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.ts +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.ts @@ -31,7 +31,6 @@ import { getCurrencyDisplayCode, getMarketName, getMinPayout, - getPropertyValue, getTradeNotificationMessage, getTradeTypeName, getTradeURLParams, @@ -59,7 +58,6 @@ import { WS, } from '@deriv/shared'; import { safeParse } from '@deriv/utils'; -import type { TEvents } from '@deriv-com/analytics'; import { localize } from '@deriv-com/translations'; import { isDigitContractType, isDigitTradeType } from 'Modules/Trading/Helpers/digits'; @@ -113,13 +111,7 @@ type TickSpotData = NonNullable; type History = NonNullable; export type TProposalResponse = TPriceProposalResponse & { - proposal: TPriceProposalResponse['proposal'] & { - payout_choices: string[]; - barrier_spot_distance: string; - contract_details: { - barrier: string; - }; - }; + proposal: TPriceProposalResponse['proposal']; error?: TPriceProposalResponse['error'] & { code: string; message: string; @@ -815,7 +807,6 @@ export default class TradeStore extends BaseStore { this.should_show_active_symbols_loading = should_show_loading; await this.setActiveSymbols(); - await this.root_store.active_symbols.setActiveSymbols(); const { symbol, showModal } = getTradeURLParams({ active_symbols: this.active_symbols }); if (showModal && should_show_loading && !this.root_store.client.is_logging_in) { @@ -1696,14 +1687,12 @@ export default class TradeStore extends BaseStore { // eslint-disable-next-line class-methods-use-this getTurbosChartBarrier(response: TProposalResponse) { return (Number(response.proposal?.contract_details?.barrier) - Number(response.proposal?.spot)).toFixed( - getBarrierPipSize(response.proposal?.contract_details?.barrier) + getBarrierPipSize(response.proposal?.contract_details?.barrier ?? '') ); } onProposalResponse(response: TResponse) { const { contract_type } = response.echo_req; - const prev_proposal_info = getPropertyValue(this.proposal_info, contract_type) || {}; - const obj_prev_contract_basis = getPropertyValue(prev_proposal_info, 'obj_contract_basis') || {}; // add/update expiration or date_expiry for crypto indices from proposal const date_expiry = response.proposal?.date_expiry; @@ -1715,7 +1704,7 @@ export default class TradeStore extends BaseStore { this.proposal_info = { ...this.proposal_info, - [contract_type]: getProposalInfo(this, response, obj_prev_contract_basis), + [contract_type]: getProposalInfo(this, response), }; this.validation_params[contract_type] = this.proposal_info[contract_type].validation_params; @@ -1766,7 +1755,7 @@ export default class TradeStore extends BaseStore { if (!this.main_barrier || this.main_barrier?.shade) { if (this.is_turbos) { if (response.proposal) { - const chart_barrier = response.proposal.barrier_spot_distance; + const chart_barrier = response.proposal.contract_details?.barrier_spot_distance; this.setMainBarrier({ ...response.echo_req, barrier: String(chart_barrier), @@ -1873,6 +1862,7 @@ export default class TradeStore extends BaseStore { } } else if (this.is_turbos) { const { max_stake, min_stake, payout_choices } = response.proposal ?? {}; + const { barrier_spot_distance } = response.proposal?.contract_details ?? {}; if (payout_choices) { if (this.payout_per_point == '') { this.onChange({ @@ -1884,7 +1874,7 @@ export default class TradeStore extends BaseStore { } this.setPayoutChoices(payout_choices as string[]); this.setStakeBoundary(contract_type, min_stake, max_stake); - this.barrier_1 = response.proposal.barrier_spot_distance; + this.barrier_1 = barrier_spot_distance ?? ''; } } } diff --git a/packages/trader/src/trader-providers.tsx b/packages/trader/src/trader-providers.tsx index a67ab3dc83..81011a843c 100644 --- a/packages/trader/src/trader-providers.tsx +++ b/packages/trader/src/trader-providers.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { APIProvider } from '@deriv/api'; import { StoreProvider } from '@deriv/stores'; import type { TCoreStores } from '@deriv/stores/types'; @@ -8,7 +9,9 @@ import { TraderStoreProvider } from 'Stores/useTraderStores'; export const TraderProviders = ({ children, store }: React.PropsWithChildren<{ store: TCoreStores }>) => { return ( - {children} + + {children} + ); };