Skip to content

Commit

Permalink
[DTRA] Henry/dtra 1443/market closed message v2 (#16196)
Browse files Browse the repository at this point in the history
* fix: stop scrolling to center on symbol selection for all category

* fix: flex-start tabs list

* fix: market is closed message

* fix: type error

* fix: add tests

* fix: add tests

* fix: add a mock for closedmarketmessage

* fix: remove unused import

* fix: comment
  • Loading branch information
henry-deriv authored Aug 1, 2024
1 parent 56dea63 commit f2a33e4
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
display: flex;
flex-direction: column;
overflow-y: auto;

.tab-list--container {
justify-content: flex-start;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.closed-market-message {
&--container {
display: flex;
width: 100%;
padding: var(--core-spacing-800);
justify-content: space-between;
align-items: center;
background-color: var(--semantic-color-slate-solid-surface-inverse-highest);
z-index: 3;
bottom: 0;
position: sticky;
}
&--left {
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--core-spacing-200);

&-message {
color: var(--semantic-color-slate-solid-surface-normal-lowest);
}
}
&--loading {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React from 'react';
import { useIsMounted, WS, isMarketClosed, toMoment } from '@deriv/shared';
import { Localize } from '@deriv/translations';
import { observer, useStore } from '@deriv/stores';
import { useTraderStore } from 'Stores/useTraderStores';
import { TradingTimesRequest } from '@deriv/api-types';
import useActiveSymbols from 'AppV2/Hooks/useActiveSymbols';
import MarketOpeningTimeBanner from '../MarketOpeningTimeBanner';
import MarketCountdownTimer from '../MarketCountdownTimer';
import { CaptionText } from '@deriv-com/quill-ui';
import clsx from 'clsx';
import { calculateTimeLeft, getSymbol } from 'AppV2/Utils/closed-market-message-utils';

type TWhenMarketOpens = {
days_offset: number;
opening_time: string;
remaining_time_to_open: number;
};

const days_to_check_before_exit = 7;

const getTradingTimes = async (target_time: TradingTimesRequest['trading_times']) => {
const data = await WS.tradingTimes(target_time);
if (data.error) {
return { api_initial_load_error: data.error.message };
}
return data;
};

const ClosedMarketMessage = observer(() => {
const { common } = useStore();
const { current_language } = common;
const { symbol, prepareTradeStore } = useTraderStore();
const { activeSymbols } = useActiveSymbols({});

const isMounted = useIsMounted();
const [when_market_opens, setWhenMarketOpens] = React.useState<TWhenMarketOpens>({} as TWhenMarketOpens);
const [time_left, setTimeLeft] = React.useState(calculateTimeLeft(when_market_opens?.remaining_time_to_open));
const [is_loading, setLoading] = React.useState(true);

React.useEffect(() => {
if (isMarketClosed(activeSymbols, symbol)) {
setLoading(true);
const whenMarketOpens = async (
days_offset: number,
target_symbol: string
): Promise<void | Record<string, unknown>> => {
if (days_offset > days_to_check_before_exit) return {};
let remaining_time_to_open;
const target_date = toMoment(new Date()).add(days_offset, 'days');
const api_response = await getTradingTimes(target_date.format('YYYY-MM-DD'));
if (!api_response.api_initial_load_error) {
const returned_symbol = getSymbol(target_symbol, api_response.trading_times);
const open = returned_symbol?.times.open as string[];
const close = returned_symbol?.times.close as string[];
const is_closed_all_day = open?.length === 1 && open[0] === '--' && close[0] === '--';
if (is_closed_all_day) {
return whenMarketOpens(days_offset + 1, target_symbol);
}
const date_str = target_date.toISOString().substring(0, 11);
const getUTCDate = (hour: string) => new Date(`${date_str}${hour}Z`);
for (let i = 0; i < open?.length; i++) {
const diff = +getUTCDate(open[i]) - Date.now();
if (diff > 0) {
remaining_time_to_open = +getUTCDate(open[i]);
if (isMounted() && target_symbol === symbol) {
return setWhenMarketOpens({
days_offset,
opening_time: open[i],
remaining_time_to_open,
});
}
}
}
whenMarketOpens(days_offset + 1, target_symbol);
}
};

whenMarketOpens(0, symbol);
}
setTimeLeft({});
setWhenMarketOpens({} as TWhenMarketOpens);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [symbol, activeSymbols]);

React.useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (when_market_opens?.remaining_time_to_open) {
timer = setTimeout(() => {
setTimeLeft(calculateTimeLeft(when_market_opens.remaining_time_to_open));
if (+new Date(when_market_opens.remaining_time_to_open) - +new Date() < 1000) {
setLoading(true);
prepareTradeStore(false);
}
}, 1000);
}
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [time_left, when_market_opens, prepareTradeStore]);

if (!(when_market_opens && Object.keys(time_left).length)) return null;

const { opening_time, days_offset } = when_market_opens;

if (is_loading) setLoading(false);

return (
<div className={clsx('closed-market-message--container', { 'closed-market-message--loading': is_loading })}>
<div className='closed-market-message--left'>
<CaptionText className='closed-market-message--left-message'>
<Localize i18n_default_text='This market will reopen at' />
</CaptionText>
<MarketOpeningTimeBanner
opening_time={opening_time}
days_offset={days_offset}
current_language={current_language}
/>
</div>
<MarketCountdownTimer time_left={time_left} />
</div>
);
});

export default ClosedMarketMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './closed-market-message.scss';
import ClosedMarketMessage from './closed-market-message';

export default ClosedMarketMessage;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tab, Text, CaptionText } from '@deriv-com/quill-ui';
import MarketCategoryItem from '../MarketCategoryItem';
import { ActiveSymbols } from '@deriv/api-types';
import FavoriteSymbols from '../FavoriteSymbols';
import { usePrevious } from '@deriv/components';

type TMarketCategory = {
category: MarketGroup;
Expand All @@ -16,17 +17,24 @@ type TMarketCategory = {
const MarketCategory = ({ category, selectedSymbol, setSelectedSymbol, setIsOpen, isOpen }: TMarketCategory) => {
const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const prevSymbol = usePrevious(selectedSymbol);

useEffect(() => {
if (isOpen && category.market === 'all' && selectedSymbol && itemRefs.current[selectedSymbol]) {
if (
isOpen &&
category.market === 'all' &&
selectedSymbol &&
itemRefs.current[selectedSymbol] &&
prevSymbol === selectedSymbol
) {
timerRef.current = setTimeout(() => {
itemRefs.current[selectedSymbol]?.scrollIntoView({ block: 'center' });
}, 50);
}
return () => {
clearTimeout(timerRef.current);
};
}, [isOpen, category.market, selectedSymbol]);
}, [isOpen, category.market, selectedSymbol, prevSymbol]);

return (
<Tab.Panel key={category.market_display_name}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'; // for the `toBeInTheDocument` matcher
import MarketCountdownTimer from '..';

describe('MarketCountDownTimer', () => {
it('renders correctly with given time left', () => {
render(<MarketCountdownTimer time_left={{ days: 1, hours: 2, minutes: 3, seconds: 4 }} />);

expect(screen.getByText('26:03:04')).toBeInTheDocument();
});
it('renders correctly when there is no time left', () => {
render(<MarketCountdownTimer time_left={{ days: 0, hours: 0, minutes: 0, seconds: 0 }} />);

expect(screen.getByText('00:00:00')).toBeInTheDocument();
});
it('renders correctly with edge case values', () => {
render(<MarketCountdownTimer time_left={{ days: 0, hours: 1, minutes: 1, seconds: 1 }} />);

expect(screen.getByText('01:01:01')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './market-countdown-timer.scss';
import MarketCountdownTimer from './market-countdown-timer';

export default MarketCountdownTimer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.market-countdown-timer {
display: flex;
gap: var(--core-spacing-200);

&-text {
color: var(--semantic-color-slate-solid-surface-normal-lowest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { Text } from '@deriv-com/quill-ui';
import { LabelPairedStopwatchMdBoldIcon } from '@deriv/quill-icons';

type TMarketCountDownTimer = {
time_left: {
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
};
};

const MarketCountdownTimer = ({ time_left }: TMarketCountDownTimer) => {
let timer_components = '';

if (Object.keys(time_left).length) {
const hours = (Number(time_left.days) * 24 + Number(time_left.hours)).toString().padStart(2, '0');
const minutes = Number(time_left.minutes).toString().padStart(2, '0');
const seconds = Number(time_left.seconds).toString().padStart(2, '0');
timer_components = `${hours}:${minutes}:${seconds}`;
}

return (
<div className='market-countdown-timer'>
<LabelPairedStopwatchMdBoldIcon fill='var(--semantic-color-slate-solid-surface-normal-lowest)' />
<Text bold size='md' className='market-countdown-timer-text'>
{timer_components}
</Text>
</div>
);
};

export default MarketCountdownTimer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import MarketOpeningTimeBanner from '../market-opening-time-banner';

jest.mock('@deriv/shared', () => ({
convertTimeFormat: jest.fn(time => `Formatted: ${time}`),
toMoment: jest.fn(() => ({
locale: jest.fn().mockReturnThis(),
add: jest.fn().mockReturnThis(),
format: jest.fn(() => '01 Jan 2024'),
})),
}));

describe('<MarketOpeningTimeBanner />', () => {
it('renders with correct formatted time and date', () => {
render(<MarketOpeningTimeBanner opening_time='12:34' days_offset={1} current_language='en' />);

expect(screen.getByText('Formatted: 12:34 (GMT), 01 Jan 2024')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './market-opening-time-banner.scss';
import MarketOpeningTimeBanner from './market-opening-time-banner';

export default MarketOpeningTimeBanner;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.market-opening-time-banner {
color: var(--semantic-color-slate-solid-surface-normal-lowest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { convertTimeFormat, toMoment } from '@deriv/shared';
import { Localize } from '@deriv/translations';
import { CaptionText } from '@deriv-com/quill-ui';

type TMarketOpeningTimeBanner = {
opening_time: string;
days_offset: number;
current_language: string;
};

const MarketOpeningTimeBanner = ({ opening_time, days_offset, current_language }: TMarketOpeningTimeBanner) => {
const formatted_opening_time = convertTimeFormat(opening_time);
const target_date = toMoment(new Date()).locale(current_language.toLowerCase()).add(days_offset, 'days');
const opening_date = target_date.format('DD MMM YYYY');

return (
<CaptionText bold className='market-opening-time-banner'>
<Localize
i18n_default_text='{{formatted_opening_time}} (GMT), {{opening_date}}'
values={{
formatted_opening_time,
opening_date,
}}
/>
</CaptionText>
);
};

export default MarketOpeningTimeBanner;
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ const MarketSelector = observer(() => {
const [isOpen, setIsOpen] = useState(false);
const { default_symbol, activeSymbols } = useActiveSymbols();
const { symbol: storeSymbol, tick_data } = useTraderStore();
const currentSymbol = activeSymbols.find(
symbol => symbol.symbol === storeSymbol || symbol.symbol === default_symbol
);
const currentSymbol =
activeSymbols.find(({ symbol }) => symbol === storeSymbol) ??
activeSymbols.find(({ symbol }) => symbol === default_symbol);

const { pip_size, quote } = tick_data ?? {};
const current_spot = quote?.toFixed(pip_size);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jest.mock('AppV2/Components/BottomNav', () =>
</div>
))
);
jest.mock('AppV2/Components/ClosedMarketMessage', () => jest.fn(() => <div>ClosedMarketMessage</div>));
jest.mock('AppV2/Components/CurrentSpot', () => jest.fn(() => <div>Current Spot</div>));
jest.mock('AppV2/Components/PurchaseButton', () => jest.fn(() => <div>Purchase Button</div>));
jest.mock('../trade-types', () => jest.fn(() => <div>Trade Types Selection</div>));
Expand Down
2 changes: 2 additions & 0 deletions packages/trader/src/AppV2/Containers/Trade/trade.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { observer } from 'mobx-react';
import { Loading } from '@deriv/components';
import ClosedMarketMessage from 'AppV2/Components/ClosedMarketMessage';
import { TRADE_TYPES } from '@deriv/shared';
import { useTraderStore } from 'Stores/useTraderStores';
import BottomNav from 'AppV2/Components/BottomNav';
Expand Down Expand Up @@ -90,6 +91,7 @@ const Trade = observer(() => {
) : (
<Loading.DTraderV2 />
)}
<ClosedMarketMessage />
</BottomNav>
);
});
Expand Down
Loading

0 comments on commit f2a33e4

Please sign in to comment.