Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DTRA] Henry/dtra 1443/market closed message v2 #16196

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qs: what's the reason why we want this logic to apply?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, seems like it is copied from another component, but the comment is missing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah its copied from production


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.symbol === storeSymbol) ??
activeSymbols.find(symbol => symbol.symbol === default_symbol);
henry-deriv marked this conversation as resolved.
Show resolved Hide resolved

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

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
Loading