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

Akmal | Henry / feat: dtrader guides improvements #17806

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
70645f9
Merge with master (#17763)
akmal-deriv Dec 9, 2024
d547813
Revert "Merge with master (#17763)" (#17764)
akmal-deriv Dec 9, 2024
ad044aa
Merge branch 'master' into feature/dtrader-guides-improvements
akmal-deriv Dec 9, 2024
4cbcd2a
Henry/grwt 2580/onboarding guide improvement (#17741)
henry-deriv Dec 12, 2024
ba22157
feat: update welcome modal (#17770)
akmal-deriv Dec 12, 2024
d46c7c9
Henry/grwt 2580/onboarding guide improvement (#17808)
henry-deriv Dec 12, 2024
f3a242d
feat: update trade type selection guide (#17809)
akmal-deriv Dec 13, 2024
0728fce
feat: trade page guide updates (#17824)
akmal-deriv Dec 17, 2024
2005c14
Henry/grwt 2580/onboarding guide improvement (#17813)
henry-deriv Dec 18, 2024
1f1627d
Akmal / fix: do not reset local storage at every guide step (#17848)
akmal-deriv Dec 18, 2024
a7bfc84
Henry/grwt 2580/onboarding guide improvement (#17849)
henry-deriv Dec 19, 2024
2c117db
Akmal / fix: issues with trade page guide (#17862)
akmal-deriv Dec 19, 2024
55458b3
Henry/grwt 2580/onboarding guide improvement (#17865)
henry-deriv Dec 23, 2024
835c76e
feat: merge with master
akmal-deriv Dec 23, 2024
449f6af
Revert "feat: merge with master"
akmal-deriv Dec 23, 2024
a21adfe
feat: merge with master
akmal-deriv Dec 23, 2024
91ed23a
Henry/grwt 2580/onboarding guide improvement (#17881)
henry-deriv Dec 23, 2024
617e047
fix: guide review suggestions #1 (#17887)
akmal-deriv Dec 24, 2024
e0419b5
Henry/grwt 2580/onboarding guide improvement (#17890)
henry-deriv Dec 24, 2024
797d465
Merge branch 'master' into feature/dtrader-guides-improvements
akmal-deriv Dec 27, 2024
bcf57e7
fix: add missing import
akmal-deriv Dec 27, 2024
4550b5f
Henry/grwt 2580/onboarding guide improvement (#17944)
henry-deriv Jan 8, 2025
9f2dbe1
chore: merge with master
akmal-deriv Jan 8, 2025
73d86dc
Update types.ts
akmal-deriv Jan 8, 2025
00d40d6
Update i18next.ts
akmal-deriv Jan 8, 2025
08d9d5b
Henry/grwt 2580/onboarding guide improvement (#18002)
henry-deriv Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import { screen, render, act } from '@testing-library/react';
import ActiveSymbolsList from '../active-symbols-list';
import TraderProviders from '../../../../trader-providers';
import { TCoreStores } from '@deriv/stores/types';
import { mockStore } from '@deriv/stores';
import { TRADE_TYPES } from '@deriv/shared';
import userEvent from '@testing-library/user-event';
import useGuideStates from 'AppV2/Hooks/useGuideStates';

const input_placeholder_text = 'Search markets on Rise/Fall';
const symbol_search_results = 'MockedSymbolSearchResults';
Expand All @@ -14,6 +15,27 @@ const market_categories = 'MockedMarketCategories';
jest.mock('AppV2/Components/SymbolSearchResults', () => jest.fn(() => <div>{symbol_search_results}</div>));
jest.mock('AppV2/Components/MarketCategories', () => jest.fn(() => <div>{market_categories}</div>));

jest.mock('AppV2/Components/OnboardingGuide/GuideForPages/guide-container', () => ({
__esModule: true,
default: jest.fn(({ should_run, callback }) => (
<div data-testid='mock-guide-container' data-should-run={should_run}>
<button onClick={callback} data-testid='guide-callback-button'>
Close Guide
</button>
</div>
)),
}));

jest.mock('AppV2/Hooks/useGuideStates', () => ({
__esModule: true,
default: jest.fn(() => ({
guideStates: {
should_run_market_selector_guide: false,
},
setGuideState: jest.fn(),
})),
}));

jest.mock('AppV2/Hooks/useActiveSymbols', () => ({
...jest.requireActual('AppV2/Hooks/useActiveSymbols'),
__esModule: true,
Expand Down Expand Up @@ -86,4 +108,71 @@ describe('<ActiveSymbolsList />', () => {
expect(default_mock_store.modules.trade.setTickData).toBeCalledWith(null);
expect(default_mock_store.modules.trade.setDigitStats).toBeCalledWith([]);
});
it('should show guide when should_run_market_selector_guide is true and component is open', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
setGuideState: jest.fn(),
});

render(MockActiveSymbolsList(default_mock_store));

act(() => {
jest.advanceTimersByTime(300);
});

const guideContainer = screen.getByTestId('mock-guide-container');
expect(guideContainer).toBeInTheDocument();
});
it('should not show guide when should_run_market_selector_guide is false', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: false,
},
setGuideState: jest.fn(),
});

render(MockActiveSymbolsList(default_mock_store));

act(() => {
jest.advanceTimersByTime(300);
});

const guideContainer = screen.getByTestId('mock-guide-container');
expect(guideContainer).toHaveAttribute('data-should-run', 'false');
});
it('should call setGuideState when guide is closed', async () => {
const mockSetGuideState = jest.fn();
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
setGuideState: mockSetGuideState,
});

render(MockActiveSymbolsList(default_mock_store));

act(() => {
jest.advanceTimersByTime(300);
});

await userEvent.click(screen.getByTestId('guide-callback-button'));
expect(mockSetGuideState).toHaveBeenCalledWith('should_run_market_selector_guide', false);
});
it('should clean up timer and hide guide on unmount', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
setGuideState: jest.fn(),
});

const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
const { unmount } = render(MockActiveSymbolsList(default_mock_store));

unmount();

expect(clearTimeoutSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import MarketCategories from '../MarketCategories';
import SymbolSearchResults from '../SymbolSearchResults';
import { useTraderStore } from 'Stores/useTraderStores';
import { sendMarketTypeToAnalytics } from '../../../Analytics';
import GuideContainer from '../OnboardingGuide/GuideForPages/guide-container';
import { Localize } from '@deriv/translations';
import useGuideStates from 'AppV2/Hooks/useGuideStates';
import { Step } from 'react-joyride';

type TActiveSymbolsList = {
isOpen: boolean;
Expand All @@ -17,6 +21,38 @@ const ActiveSymbolsList = observer(({ isOpen, setIsOpen }: TActiveSymbolsList) =
const [isSearching, setIsSearching] = useState(false);
const [selectedSymbol, setSelectedSymbol] = useState(symbol);
const [searchValue, setSearchValue] = useState('');
const { guideStates, setGuideState } = useGuideStates();
akmal-deriv marked this conversation as resolved.
Show resolved Hide resolved
const { should_run_market_selector_guide } = guideStates;

const STEPS = [
nijil-deriv marked this conversation as resolved.
Show resolved Hide resolved
{
content: <Localize i18n_default_text='Explore available markets here.' />,
placement: 'top' as Step['placement'],
target: '.joyride-element',
title: <Localize i18n_default_text='Select a market' />,
disableBeacon: true,
offset: 0,
spotlightPadding: 4,
},
];

const [show_guide, setShowGuide] = useState(false);
const timerRef = useRef<NodeJS.Timeout>();

useEffect(() => {
if (should_run_market_selector_guide && isOpen) {
timerRef.current = setTimeout(() => {
setShowGuide(true);
}, 300);
}

return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setShowGuide(false);
};
}, [should_run_market_selector_guide, isOpen]);

const marketCategoriesRef = useRef<HTMLDivElement>(null);

Expand All @@ -30,6 +66,11 @@ const ActiveSymbolsList = observer(({ isOpen, setIsOpen }: TActiveSymbolsList) =
<React.Fragment>
<ActionSheet.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>
<ActionSheet.Portal shouldCloseOnDrag fullHeightOnOpen>
<GuideContainer
should_run={show_guide}
steps={STEPS}
callback={() => setGuideState('should_run_market_selector_guide', false)}
/>
<SymbolsSearchField
searchValue={searchValue}
setSearchValue={setSearchValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ $BOTTOM_NAV_HEIGHT: var(--core-spacing-2800);
}
&-selection {
flex: 1;
overflow: auto;
overflow: auto !important;
-ms-overflow-style: none;
scrollbar-width: none;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,40 @@ import React from 'react';
import { screen, render } from '@testing-library/react';
import MarketCategory from '../market-category';
import { ActiveSymbols } from '@deriv/api-types';
import { mockStore } from '@deriv/stores';
import { TCoreStores } from '@deriv/stores/types';
import TraderProviders from '../../../../trader-providers';
import useGuideStates from 'AppV2/Hooks/useGuideStates';

jest.mock('AppV2/Components/MarketCategoryItem', () =>
jest.fn(props => <div ref={props.ref}>MockedMarketCategoryItem</div>)
);

jest.mock('AppV2/Components/FavoriteSymbols', () => jest.fn(() => <div>MockedFavoriteSymbols</div>));

jest.mock('AppV2/Hooks/useGuideStates', () => ({
__esModule: true,
default: jest.fn(() => ({
guideStates: {
should_run_market_selector_guide: false,
},
})),
}));

const mockedMarketCategory = (mocked_store: TCoreStores, mock_props: React.ComponentProps<typeof MarketCategory>) => {
return (
<TraderProviders store={mocked_store}>
<MarketCategory {...mock_props} />
</TraderProviders>
);
};
describe('<MarketCategory />', () => {
const default_mock_store = {
client: {
is_logged_in: false,
},
};

const mocked_props = {
category: {
market: 'forex',
Expand Down Expand Up @@ -54,7 +80,7 @@ describe('<MarketCategory />', () => {
};

it('should render correct labels', () => {
render(<MarketCategory {...mocked_props} />);
render(mockedMarketCategory(mockStore(default_mock_store), mocked_props));
expect(screen.getByText('Major Pairs')).toBeInTheDocument();
expect(screen.getAllByText('MockedMarketCategoryItem')).toHaveLength(2);
});
Expand All @@ -67,7 +93,37 @@ describe('<MarketCategory />', () => {
subgroups: {},
},
};
render(<MarketCategory {...favoriteProps} />);
render(mockedMarketCategory(mockStore(default_mock_store), favoriteProps));
expect(screen.getByText('MockedFavoriteSymbols')).toBeInTheDocument();
});
it('should not render joyride-element when user is not logged in', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
});

render(mockedMarketCategory(mockStore({ client: { is_logged_in: false } }), mocked_props));
expect(screen.queryByTestId('joyride-element')).not.toBeInTheDocument();
});
it('should not render joyride-element when should_run_market_selector_guide is false', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: false,
},
});

render(mockedMarketCategory(mockStore({ client: { is_logged_in: true } }), mocked_props));
expect(screen.queryByTestId('joyride-element')).not.toBeInTheDocument();
});
it('should render joyride-element when user is logged in and should_run_market_selector_guide is true', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
});

render(mockedMarketCategory(mockStore({ client: { is_logged_in: true } }), mocked_props));
expect(screen.getByTestId('joyride-element')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
$HANDLEBAR_HEIGHT: 2rem;
$SEARCHFIELD_HEIGHT: 4.8rem;
$TABS_HEIGHT: 4.3rem;

.market-category {
&-content {
&__container {
Expand Down Expand Up @@ -25,3 +29,11 @@
flex-direction: column;
}
}

.joyride-element {
position: fixed;
top: calc(100dvh - 90dvh + $HANDLEBAR_HEIGHT + $SEARCHFIELD_HEIGHT + $TABS_HEIGHT);
width: 100%;
background-color: transparent;
height: 34rem;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import MarketCategoryItem from '../MarketCategoryItem';
import { ActiveSymbols } from '@deriv/api-types';
import FavoriteSymbols from '../FavoriteSymbols';
import { usePrevious } from '@deriv/components';
import { useLocalStorageData } from '@deriv/hooks';
import useGuideStates from 'AppV2/Hooks/useGuideStates';
import { useStore } from '@deriv/stores';

type TMarketCategory = {
category: MarketGroup;
Expand All @@ -17,15 +20,39 @@ type TMarketCategory = {
const MarketCategory = ({ category, selectedSymbol, setSelectedSymbol, setIsOpen, isOpen }: TMarketCategory) => {
const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const prevSymbol = usePrevious(selectedSymbol);
const [guide_dtrader_v2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const { guideStates } = useGuideStates();
const { should_run_market_selector_guide } = guideStates;
const {
client: { is_logged_in },
} = useStore();

useEffect(() => {
if (isOpen && category.market === 'all' && selectedSymbol && itemRefs.current[selectedSymbol] && !prevSymbol) {
if (
isOpen &&
category.market === 'all' &&
selectedSymbol &&
itemRefs.current[selectedSymbol] &&
!prevSymbol &&
!should_run_market_selector_guide &&
guide_dtrader_v2?.market_selector
) {
itemRefs.current[selectedSymbol]?.scrollIntoView({ block: 'center' });
}
}, [isOpen, category.market, selectedSymbol, prevSymbol]);
}, [
isOpen,
category.market,
selectedSymbol,
prevSymbol,
should_run_market_selector_guide,
guide_dtrader_v2?.market_selector,
]);

return (
<Tab.Panel key={category.market_display_name}>
{should_run_market_selector_guide && is_logged_in && (
<div className='joyride-element' data-testid='joyride-element' />
)}
{category.market !== 'favorites' ? (
Object.entries(category.subgroups).map(([subgroupKey, subgroup]) => (
<div key={subgroupKey} className='market-category-content__container'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import { Localize } from '@deriv/translations';
import { LabelPairedChevronDownMdRegularIcon } from '@deriv/quill-icons';
import { observer } from '@deriv/stores';
import { useTraderStore } from 'Stores/useTraderStores';
import { useLocalStorageData } from '@deriv/hooks';
import { getLocalStorage } from '@deriv/utils';
import useGuideStates from 'AppV2/Hooks/useGuideStates';

const MarketSelector = observer(() => {
const [isOpen, setIsOpen] = useState(false);
const { activeSymbols } = useActiveSymbols();
const { symbol: storeSymbol, tick_data, is_market_closed } = useTraderStore();
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const { setGuideState } = useGuideStates();

const currentSymbol = activeSymbols.find(({ symbol }) => symbol === storeSymbol);
const { pip_size, quote } = tick_data ?? {};
Expand All @@ -26,9 +31,20 @@ const MarketSelector = observer(() => {
if (typeof currentSymbol?.exchange_is_open === 'undefined')
return <Skeleton.Square height={42} width={240} rounded />;

const onClick = () => {
if (guide_dtrader_v2?.market_selector) {
setGuideState('should_run_market_selector_guide', false);
} else {
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
akmal-deriv marked this conversation as resolved.
Show resolved Hide resolved
setGuideDtraderV2({ ...latest_guide_dtrader_v2, market_selector: true });
setGuideState('should_run_market_selector_guide', true);
}
setIsOpen(true);
};

return (
<React.Fragment>
<div className='market-selector__container' onClick={() => setIsOpen(true)}>
<div className='market-selector__container' onClick={onClick}>
<div className='market-selector'>
<SymbolIconsMapper symbol={storeSymbol} />
<div className='market-selector-info'>
Expand Down Expand Up @@ -58,4 +74,4 @@ const MarketSelector = observer(() => {
);
});

export default MarketSelector;
export default React.memo(MarketSelector);
Loading
Loading