diff --git a/package-lock.json b/package-lock.json
index 89282a07618d..3d9768ecff6d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,7 @@
"@datadog/browser-rum": "^5.11.0",
"@deriv-com/analytics": "1.12.1",
"@deriv-com/quill-tokens": "^2.0.4",
- "@deriv-com/quill-ui": "1.16.4",
+ "@deriv-com/quill-ui": "1.16.5",
"@deriv-com/translations": "1.3.5",
"@deriv-com/ui": "1.29.10",
"@deriv-com/utils": "^0.0.25",
@@ -2961,9 +2961,9 @@
}
},
"node_modules/@deriv-com/quill-ui": {
- "version": "1.16.4",
- "resolved": "https://registry.npmjs.org/@deriv-com/quill-ui/-/quill-ui-1.16.4.tgz",
- "integrity": "sha512-A+Xbe/3H/2T+pwCQnANY9hwY7jgFIyMRGxJI6Ba4VyRlR/isCj5ltf/Mrb+z8JpIK7kMK5vjA1einNkhT/dIzQ==",
+ "version": "1.16.5",
+ "resolved": "https://registry.npmjs.org/@deriv-com/quill-ui/-/quill-ui-1.16.5.tgz",
+ "integrity": "sha512-AJJ3F0bO1MsZ4qVdG2xV5RQTf32ghDKCejrFv6Bgpf93mOopXVtPuHtAgMct+J8x0NJyRXqR8k7gUqNv9tO0zQ==",
"dependencies": {
"@deriv-com/quill-tokens": "^2.0.10",
"@deriv/quill-icons": "^1.22.10",
diff --git a/packages/account/package.json b/packages/account/package.json
index 34d3877fb0d8..882ab24c2ef9 100644
--- a/packages/account/package.json
+++ b/packages/account/package.json
@@ -35,7 +35,7 @@
"@deriv-com/utils": "^0.0.25",
"@deriv-com/ui": "1.29.10",
"@deriv/api": "^1.0.0",
- "@deriv-com/quill-ui": "1.16.4",
+ "@deriv-com/quill-ui": "1.16.5",
"@deriv/components": "^1.0.0",
"@deriv/hooks": "^1.0.0",
"@deriv/integration": "1.0.0",
diff --git a/packages/core/package.json b/packages/core/package.json
index e086ee97c5a8..9946bc5cbb5c 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -98,7 +98,7 @@
"@datadog/browser-rum": "^5.11.0",
"@deriv-com/analytics": "1.12.1",
"@deriv-com/quill-tokens": "^2.0.4",
- "@deriv-com/quill-ui": "1.16.4",
+ "@deriv-com/quill-ui": "1.16.5",
"@deriv-com/translations": "1.3.5",
"@deriv-com/ui": "1.29.10",
"@deriv-com/utils": "^0.0.25",
diff --git a/packages/core/src/public/videos/user-onboarding-guide-trade-page.mp4 b/packages/core/src/public/videos/user-onboarding-guide-trade-page.mp4
new file mode 100644
index 000000000000..ef211f26b71f
Binary files /dev/null and b/packages/core/src/public/videos/user-onboarding-guide-trade-page.mp4 differ
diff --git a/packages/trader/package.json b/packages/trader/package.json
index 40e302523b53..83fae6619b5f 100644
--- a/packages/trader/package.json
+++ b/packages/trader/package.json
@@ -90,7 +90,7 @@
"dependencies": {
"@deriv-com/analytics": "1.12.1",
"@deriv-com/quill-tokens": "^2.0.4",
- "@deriv-com/quill-ui": "1.16.4",
+ "@deriv-com/quill-ui": "1.16.5",
"@deriv-com/utils": "^0.0.25",
"@deriv-com/ui": "1.29.10",
"@deriv/api-types": "1.0.172",
@@ -121,6 +121,7 @@
"react-beautiful-dnd": "^13.1.1",
"react-content-loader": "^6.2.0",
"react-dom": "^17.0.2",
+ "react-joyride": "^2.5.3",
"react-loadable": "^5.5.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss
index ec00c6903d8f..3563d7a7b820 100644
--- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss
+++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss
@@ -30,5 +30,17 @@ $BOTTOM_NAV_HEIGHT: var(--core-spacing-2800);
fill: var(--core-color-opacity-coral-600);
}
}
+ &--positions {
+ position: relative;
+
+ .user-guide__anchor {
+ position: absolute;
+ height: var(--core-size-2500);
+ width: var(--core-size-4600);
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
}
}
diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx
index cb0ff3079a90..620ba0c46616 100644
--- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx
+++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx
@@ -54,7 +54,12 @@ const BottomNav = observer(({ children, className, onScroll }: BottomNavProps) =
fill='var(--semantic-color-monochrome-textIcon-normal-high)'
/>
),
- label: ,
+ label: (
+
+
+
+
+ ),
path: routes.trader_positions,
},
];
@@ -80,7 +85,11 @@ const BottomNav = observer(({ children, className, onScroll }: BottomNavProps) =
label={item.label}
selected={index === selectedIndex}
showLabel
- className={clsx('bottom-nav-item', index === selectedIndex && 'bottom-nav-item--active')}
+ className={clsx(
+ 'bottom-nav-item',
+ index === selectedIndex && 'bottom-nav-item--active',
+ item.path === routes.trader_positions && 'bottom-nav-item--positions'
+ )}
/>
))}
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/guide-container.spec.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/guide-container.spec.tsx
new file mode 100644
index 000000000000..5909522d99c4
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/guide-container.spec.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { CallBackProps } from 'react-joyride';
+import GuideContainer from '../guide-container';
+
+jest.mock('react-joyride', () => ({
+ __esModule: true,
+ default: jest.fn(({ callback }: { callback: (data: CallBackProps) => void }) => (
+
+
Joyride
+
+ )),
+ STATUS: { SKIPPED: 'skipped', FINISHED: 'finished' },
+}));
+
+const mock_props = {
+ should_run: true,
+ onFinishGuide: jest.fn(),
+};
+
+describe('GuideContainer', () => {
+ it('should render component', () => {
+ render();
+
+ expect(screen.getByText('Joyride')).toBeInTheDocument();
+ });
+
+ it('should call onFinishGuide inside of callbackHandle if passed status is equal to "skipped" or "finished"', () => {
+ render();
+ userEvent.click(screen.getByRole('button'));
+
+ expect(mock_props.onFinishGuide).toBeCalled();
+ });
+});
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/guide-tooltip.spec.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/guide-tooltip.spec.tsx
new file mode 100644
index 000000000000..a70b926c6107
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/guide-tooltip.spec.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import GuideTooltip, { GuideTooltipProps } from '../guide-tooltip';
+
+jest.mock('react-joyride', () => jest.fn(() => Joyride
));
+
+const mock_props = {
+ isLastStep: false,
+ primaryProps: {
+ title: 'Title',
+ },
+ skipProps: {
+ title: 'Title',
+ },
+ step: {
+ title: 'Title',
+ content: 'Step content',
+ },
+ setStepIndex: jest.fn(),
+ tooltipProps: {},
+} as unknown as GuideTooltipProps;
+
+describe('GuideTooltip', () => {
+ it('should render correct content for tooltip if isLastStep === false', () => {
+ render();
+
+ expect(screen.getByText('Title')).toBeInTheDocument();
+ expect(screen.getByText('Step content')).toBeInTheDocument();
+ expect(screen.getByText('Next')).toBeInTheDocument();
+ expect(screen.queryByText('Done')).not.toBeInTheDocument();
+ });
+
+ it('should render correct content for tooltip if isLastStep === true', () => {
+ render();
+
+ expect(screen.getByText('Title')).toBeInTheDocument();
+ expect(screen.getByText('Step content')).toBeInTheDocument();
+ expect(screen.getByText('Done')).toBeInTheDocument();
+ expect(screen.queryByText('Next')).not.toBeInTheDocument();
+ });
+
+ it('should render scroll icon if title of the step is scroll-icon', () => {
+ mock_props.step.title = 'scroll-icon';
+ render();
+ expect(screen.getByText('Swipe up to see the chart')).toBeInTheDocument();
+ });
+});
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/onboarding-guide.spec.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/onboarding-guide.spec.tsx
new file mode 100644
index 000000000000..425e76a26625
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/onboarding-guide.spec.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import OnboardingGuide from '../onboarding-guide';
+
+const trading_modal_text = 'Welcome to the new Deriv Trader';
+const positions_modal_text = 'View your positions';
+const guide_container = 'GuideContainer';
+
+jest.mock('../guide-container', () =>
+ jest.fn(({ should_run }: { should_run?: boolean }) => {should_run && guide_container}
)
+);
+jest.mock('../onboarding-video', () => jest.fn(() => OnboardingVideo
));
+
+describe('OnboardingGuide', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('should render Modal with correct content for trading page after 800ms after mounting', async () => {
+ jest.useFakeTimers();
+ render();
+
+ await waitFor(() => jest.advanceTimersByTime(800));
+
+ expect(screen.getByText('OnboardingVideo')).toBeInTheDocument();
+ expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
+ expect(screen.getByText("Let's begin")).toBeInTheDocument();
+
+ jest.useRealTimers();
+ });
+
+ it('should render Modal with correct content for positions page after 800ms after mounting', async () => {
+ jest.useFakeTimers();
+ render();
+
+ await waitFor(() => jest.advanceTimersByTime(800));
+
+ expect(screen.queryByText('OnboardingVideo')).not.toBeInTheDocument();
+ expect(screen.getByText(positions_modal_text)).toBeInTheDocument();
+ expect(screen.getByText('Got it')).toBeInTheDocument();
+
+ jest.useRealTimers();
+ });
+
+ it('should close the Modal for trading page and start the guide after user clicks on "Let\'s begin" button', async () => {
+ jest.useFakeTimers();
+ render();
+
+ await waitFor(() => jest.advanceTimersByTime(800));
+
+ expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
+ expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
+
+ userEvent.click(screen.getByRole('button'));
+ await waitFor(() => jest.advanceTimersByTime(300));
+
+ expect(screen.queryByText(trading_modal_text)).not.toBeInTheDocument();
+ expect(screen.getByText(guide_container)).toBeInTheDocument();
+
+ jest.useRealTimers();
+ });
+
+ it('should close the Modal for positions page, set flag to localStorage equal to true and do NOT start the guide after user clicks on "Got it" button', async () => {
+ const key = 'guide_dtrader_v2_positions_page';
+ jest.useFakeTimers();
+ render();
+
+ await waitFor(() => jest.advanceTimersByTime(800));
+
+ expect(screen.getByText(positions_modal_text)).toBeInTheDocument();
+ expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
+ expect(localStorage.getItem(key)).toBe('false');
+
+ userEvent.click(screen.getByRole('button'));
+ await waitFor(() => jest.advanceTimersByTime(300));
+
+ expect(screen.queryByText(positions_modal_text)).not.toBeInTheDocument();
+ expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
+ expect(localStorage.getItem(key)).toBe('true');
+
+ jest.useRealTimers();
+ });
+
+ it('should close the Modal for trading page and set flag to localStorage equal to true if user clicks on overlay and do NOT start the guide', async () => {
+ const key = 'guide_dtrader_v2_trade_page';
+ jest.useFakeTimers();
+ render();
+
+ await waitFor(() => jest.advanceTimersByTime(800));
+
+ expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
+ expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
+ expect(localStorage.getItem(key)).toBe('false');
+
+ userEvent.click(screen.getByTestId('dt-actionsheet-overlay'));
+ await waitFor(() => jest.advanceTimersByTime(300));
+
+ expect(screen.queryByText(trading_modal_text)).not.toBeInTheDocument();
+ expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
+ expect(localStorage.getItem(key)).toBe('true');
+
+ jest.useRealTimers();
+ });
+});
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/onboarding-video.spec.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/onboarding-video.spec.tsx
new file mode 100644
index 000000000000..4170470593b7
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/__tests__/onboarding-video.spec.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import OnboardingVideo from '../onboarding-video';
+
+const dt_video = 'dt_onboarding_guide_video';
+const dt_loader = 'square-skeleton';
+
+jest.mock('@deriv/shared', () => ({
+ ...jest.requireActual('@deriv/shared'),
+ getUrlBase: jest.fn(() => 'video_src.mp4'),
+}));
+
+describe('OnboardingVideo', () => {
+ beforeAll(() => {
+ Object.defineProperty(HTMLMediaElement.prototype, 'muted', {
+ set: jest.fn(),
+ });
+ });
+
+ it('should render loader and video if the data is not fully loaded', () => {
+ render();
+
+ expect(screen.getByTestId(dt_loader)).toBeInTheDocument();
+ expect(screen.getByTestId(dt_video)).toBeInTheDocument();
+ });
+
+ it('should render only video tag if the data has already loaded', () => {
+ render();
+
+ const video = screen.getByTestId(dt_video);
+ fireEvent.loadedData(video);
+
+ expect(screen.queryByTestId(dt_loader)).not.toBeInTheDocument();
+ expect(video).toBeInTheDocument();
+ });
+});
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/guide-container.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/guide-container.tsx
new file mode 100644
index 000000000000..8aac4f15220f
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/guide-container.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import Joyride, { CallBackProps, STATUS } from 'react-joyride';
+import GuideTooltip from './guide-tooltip';
+import STEPS from './steps-config';
+
+type TGuideContainerProps = {
+ should_run: boolean;
+ onFinishGuide: () => void;
+};
+
+type TFinishedStatuses = CallBackProps['status'][];
+
+const GuideContainer = ({ should_run, onFinishGuide }: TGuideContainerProps) => {
+ const [step_index, setStepIndex] = React.useState(0);
+
+ const callbackHandle = (data: CallBackProps) => {
+ const { status } = data;
+ const finished_statuses: TFinishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED];
+
+ if (finished_statuses.includes(status)) onFinishGuide();
+ };
+
+ return (
+ }
+ />
+ );
+};
+
+export default React.memo(GuideContainer);
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/guide-tooltip.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/guide-tooltip.tsx
new file mode 100644
index 000000000000..4d4521301db1
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/guide-tooltip.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { Button, CaptionText, IconButton, Text } from '@deriv-com/quill-ui';
+import { LabelPairedChevronsUpXlBoldIcon, LabelPairedXmarkSmBoldIcon } from '@deriv/quill-icons';
+import { Localize } from '@deriv/translations';
+import { TooltipRenderProps } from 'react-joyride';
+import { useSwipeable } from 'react-swipeable';
+
+export interface GuideTooltipProps extends TooltipRenderProps {
+ setStepIndex: React.Dispatch>;
+}
+
+const GuideTooltip = ({ isLastStep, primaryProps, skipProps, step, tooltipProps, setStepIndex }: GuideTooltipProps) => {
+ const swipe_handlers = useSwipeable({
+ onSwipedUp: () => {
+ document.querySelector('.trade__chart')?.scrollIntoView();
+ setStepIndex((prev: number) => prev + 1);
+ },
+ preventDefaultTouchmoveEvent: true,
+ trackTouch: true,
+ trackMouse: true,
+ });
+
+ if (step.title === 'scroll-icon') {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {step.title && (
+
+
+ {step.title}
+
+
+ }
+ className='guide-tooltip__close'
+ size='sm'
+ color='white-black'
+ variant='tertiary'
+ />
+
+ )}
+ {step.content &&
{step.content}}
+
+
+ );
+};
+
+export default GuideTooltip;
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/index.ts b/packages/trader/src/AppV2/Components/OnboardingGuide/index.ts
new file mode 100644
index 000000000000..df057ea5fbd9
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/index.ts
@@ -0,0 +1,4 @@
+import './onboarding-guide.scss';
+import OnboardingGuide from './onboarding-guide';
+
+export default OnboardingGuide;
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-guide.scss b/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-guide.scss
new file mode 100644
index 000000000000..328a95890429
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-guide.scss
@@ -0,0 +1,96 @@
+.guide-tooltip {
+ &__wrapper {
+ background-color: var(--component-textIcon-normal-prominent);
+ border-radius: var(--core-borderRadius-400);
+ padding: var(--core-size-800);
+ display: flex;
+ gap: var(--core-size-800);
+ flex-direction: column;
+ align-items: flex-start;
+ min-width: var(--core-size-4300);
+
+ &-scroll {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &-text {
+ color: var(--core-color-solid-slate-50);
+ }
+ }
+ }
+
+ &__button {
+ align-self: flex-end;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+
+ &__title {
+ color: var(--component-textIcon-inverse-prominent);
+ }
+ }
+
+ &__content {
+ margin-inline-end: var(--core-spacing-1200);
+ color: var(--component-textIcon-inverse-prominent);
+ }
+
+ &__close {
+ position: relative;
+ inset-inline-start: var(--core-spacing-500);
+ inset-block-end: var(--core-spacing-500);
+ }
+
+ @keyframes bounce {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+ }
+
+ &--bounce {
+ height: var(--core-spacing-3200);
+ width: var(--core-spacing-2400);
+ fill: var(--core-color-solid-slate-50);
+ animation: bounce var(--core-motion-duration-400) ease-in-out var(--core-motion-duration-100) infinite;
+ }
+}
+
+.guide {
+ &__player {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ &__wrapper {
+ position: relative;
+ width: 100%;
+ padding-top: 56.25%; // 16:9 aspect ratio (9/16 = 0.5625 or 56.25%)
+ overflow: hidden;
+ }
+ }
+}
+
+//TODO: remove after video for position page guide will be ready
+.video-placeholder {
+ position: relative;
+ height: 218.5px;
+ width: 100%;
+ background-color: var(--core-color-solid-slate-750);
+
+ &:before {
+ content: 'Placeholder. Video will arrive soon';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-guide.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-guide.tsx
new file mode 100644
index 000000000000..49e66f051783
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-guide.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { Modal } from '@deriv-com/quill-ui';
+import { useLocalStorageData } from '@deriv/hooks';
+import { Localize } from '@deriv/translations';
+import GuideContainer from './guide-container';
+import OnboardingVideo from './onboarding-video';
+
+type TOnboardingGuideProps = {
+ type?: 'trade_page' | 'positions_page';
+};
+
+const OnboardingGuide = ({ type = 'trade_page' }: TOnboardingGuideProps) => {
+ const [is_modal_open, setIsModalOpen] = React.useState(false);
+ const [should_run_guide, setShouldRunGuide] = React.useState(false);
+ const guide_timeout_ref = React.useRef>();
+ const is_button_clicked_ref = React.useRef(false);
+
+ const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData(`guide_dtrader_v2_${type}`, false);
+ const is_trade_page_guide = type === 'trade_page';
+
+ const onFinishGuide = React.useCallback(() => {
+ setShouldRunGuide(false);
+ setGuideDtraderV2(true);
+ }, [setGuideDtraderV2]);
+
+ const onGuideSkip = () => {
+ if (is_button_clicked_ref.current) return;
+ onFinishGuide();
+ setIsModalOpen(false);
+ };
+
+ const onGuideStart = () => {
+ is_button_clicked_ref.current = true;
+ setShouldRunGuide(true);
+ setIsModalOpen(false);
+ };
+
+ const modal_content = {
+ image: ,
+ title: ,
+ content: (
+
+ ),
+ button_label: ,
+ primaryButtonCallback: onGuideSkip,
+ ...(is_trade_page_guide
+ ? {
+ image: ,
+ title: ,
+ content: (
+
+ ),
+ button_label: ,
+ primaryButtonCallback: onGuideStart,
+ }
+ : {}),
+ };
+
+ React.useEffect(() => {
+ if (!guide_dtrader_v2) guide_timeout_ref.current = setTimeout(() => setIsModalOpen(true), 800);
+
+ return () => clearTimeout(guide_timeout_ref.current);
+ }, [guide_dtrader_v2]);
+
+ return (
+
+
+
+ {modal_content.content}
+
+ {is_trade_page_guide && }
+
+ );
+};
+
+export default React.memo(OnboardingGuide);
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-video.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-video.tsx
new file mode 100644
index 000000000000..ba17feaf70df
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/onboarding-video.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Skeleton } from '@deriv-com/quill-ui';
+import { getUrlBase } from '@deriv/shared';
+import { Localize } from '@deriv/translations';
+
+const OnboardingVideo = () => {
+ const [is_loading, setIsLoading] = React.useState(true);
+
+ // memoize file paths for videos and open the modal only after we get them
+ const getVideoSource = React.useCallback(
+ (extension: string) => getUrlBase(`/public/videos/user-onboarding-guide-trade-page.${extension}`),
+ []
+ );
+ const mp4_src = React.useMemo(() => getVideoSource('mp4'), [getVideoSource]);
+
+ return (
+
+ {is_loading && }
+
+
+ );
+};
+
+export default OnboardingVideo;
diff --git a/packages/trader/src/AppV2/Components/OnboardingGuide/steps-config.tsx b/packages/trader/src/AppV2/Components/OnboardingGuide/steps-config.tsx
new file mode 100644
index 000000000000..4ec500066254
--- /dev/null
+++ b/packages/trader/src/AppV2/Components/OnboardingGuide/steps-config.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { Step } from 'react-joyride';
+import { Localize } from '@deriv/translations';
+
+const STEPS = [
+ {
+ content: ,
+ disableBeacon: true,
+ offset: 0,
+ spotlightPadding: 2,
+ target: '.trade__trade-types',
+ title: ,
+ },
+ {
+ content: ,
+ disableBeacon: true,
+ offset: 4,
+ placement: 'bottom-start' as Step['placement'],
+ spotlightPadding: 8,
+ target: '.market-selector__container',
+ title: ,
+ },
+ {
+ content: ,
+ disableBeacon: true,
+ offset: 4,
+ spotlightPadding: 8,
+ target: '.trade-params',
+ title: ,
+ },
+ {
+ content: '',
+ disableBeacon: false,
+ offset: 0,
+ spotlightPadding: 0,
+ styles: {
+ spotlight: {
+ display: 'none',
+ },
+ arrow: {
+ display: 'none',
+ },
+ },
+ target: '.purchase-button__wrapper',
+ title: 'scroll-icon',
+ },
+ {
+ content: ,
+ disableBeacon: true,
+ spotlightPadding: 8,
+ offset: 4,
+ target: '.trade__chart-tooltip',
+ title: ,
+ placement: 'bottom' as Step['placement'],
+ },
+ {
+ content: ,
+ disableBeacon: true,
+ disableScrolling: false,
+ offset: -4,
+ target: '.trade__parameter',
+ title: ,
+ },
+ {
+ content: ,
+ disableBeacon: true,
+ offset: -4,
+ target: '.user-guide__anchor',
+ title: ,
+ },
+];
+
+export default STEPS;
diff --git a/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx b/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx
index 8d75724ea26c..6bb29f1ae925 100644
--- a/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx
+++ b/packages/trader/src/AppV2/Components/RiskManagementInfoModal/__tests__/risk-management-info-modal.spec.tsx
@@ -1,8 +1,7 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
-
-import RiskManagementInfoModal from '../risk-management-info-modal';
import userEvent from '@testing-library/user-event';
+import RiskManagementInfoModal from '../risk-management-info-modal';
jest.mock('@deriv/quill-icons', () => ({
LabelPairedCircleInfoSmRegularIcon: () => ,
diff --git a/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx b/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx
index 2364c10165ac..cad0b4a4cfed 100644
--- a/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx
+++ b/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx
@@ -9,9 +9,15 @@ import ModulesProvider from 'Stores/Providers/modules-providers';
import Positions from '../positions';
import userEvent from '@testing-library/user-event';
-const defaultMockStore = mockStore({});
+const defaultMockStore = mockStore({
+ modules: {
+ positions: { onUnmount: jest.fn() },
+ },
+ client: { is_logged_in: true },
+});
jest.mock('../positions-content', () => jest.fn(() => 'mockPositionsContent'));
+jest.mock('AppV2/Components/OnboardingGuide', () => jest.fn(() => 'OnboardingGuide'));
describe('Positions', () => {
const mockPositions = () => {
@@ -31,12 +37,9 @@ describe('Positions', () => {
Element.prototype.scrollTo = jest.fn();
});
- beforeAll(() => {
- Element.prototype.scrollTo = jest.fn();
- });
-
afterEach(() => {
jest.clearAllMocks();
+ localStorage.clear();
});
it('should render component', () => {
@@ -52,6 +55,7 @@ describe('Positions', () => {
expect(screen.getByText(utils.TAB_NAME.OPEN)).toBeInTheDocument();
expect(screen.getByText(utils.TAB_NAME.CLOSED)).toBeInTheDocument();
+ expect(screen.getByText('OnboardingGuide')).toBeInTheDocument();
});
it('should call setPositionURLParams with appropriate argument if user clicks on Closed tab', () => {
@@ -64,4 +68,19 @@ describe('Positions', () => {
userEvent.click(screen.getByText(utils.TAB_NAME.OPEN));
expect(mockSetPositionURLParams).toBeCalledWith(utils.TAB_NAME.OPEN.toLowerCase());
});
+
+ it('should not render OnboardingGuide if localStorage flag is equal to true', () => {
+ const key = 'guide_dtrader_v2_positions_page';
+ localStorage.setItem(key, 'true');
+ render(mockPositions());
+
+ expect(screen.queryByText('OnboardingGuide')).not.toBeInTheDocument();
+ });
+
+ it('should not render OnboardingGuide if client is not logged in', () => {
+ defaultMockStore.client.is_logged_in = false;
+ render(mockPositions());
+
+ expect(screen.queryByText('OnboardingGuide')).not.toBeInTheDocument();
+ });
});
diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx
index d288c64ba312..b13152e846ba 100644
--- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx
+++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx
@@ -1,19 +1,26 @@
import React from 'react';
import { Localize } from '@deriv/translations';
import { getPositionsV2TabIndexFromURL } from '@deriv/shared';
+import { useLocalStorageData } from '@deriv/hooks';
import { Tab } from '@deriv-com/quill-ui';
import { observer } from 'mobx-react';
+import { useStore } from '@deriv/stores';
import { useModulesStore } from 'Stores/useModulesStores';
import { setPositionURLParams, TAB_NAME } from 'AppV2/Utils/positions-utils';
import BottomNav from 'AppV2/Components/BottomNav';
import PositionsContent from './positions-content';
import { useHistory } from 'react-router-dom';
+import OnboardingGuide from 'AppV2/Components/OnboardingGuide';
const Positions = observer(() => {
const [hasButtonsDemo, setHasButtonsDemo] = React.useState(true);
const [activeTab, setActiveTab] = React.useState(getPositionsV2TabIndexFromURL());
+ const [guide_dtrader_v2] = useLocalStorageData('guide_dtrader_v2_positions_page', false);
const history = useHistory();
+ const {
+ client: { is_logged_in },
+ } = useStore();
const {
positions: { onUnmount },
} = useModulesStore();
@@ -68,6 +75,7 @@ const Positions = observer(() => {
+ {!guide_dtrader_v2 && is_logged_in && }
);
});
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 862dd700436c..918550cd5b21 100644
--- a/packages/trader/src/AppV2/Containers/Trade/__tests__/trade.spec.tsx
+++ b/packages/trader/src/AppV2/Containers/Trade/__tests__/trade.spec.tsx
@@ -41,8 +41,8 @@ jest.mock('AppV2/Utils/trade-types-utils', () => ({
jest.mock('@lottiefiles/dotlottie-react', () => ({
DotLottieReact: jest.fn(() => DotLottieReact
),
}));
+jest.mock('AppV2/Components/OnboardingGuide', () => jest.fn(() => 'OnboardingGuide'));
jest.mock('AppV2/Hooks/useContractsForCompany', () => ({
- ...jest.requireActual('AppV2/Hooks/useContractsForCompany'),
__esModule: true,
default: jest.fn(() => ({
contracts_for_company: mock_contract_data,
@@ -92,7 +92,9 @@ describe('Trade', () => {
],
},
},
+ client: { is_logged_in: true },
});
+ localStorage.clear();
});
const mockTrade = () => {
@@ -125,6 +127,7 @@ describe('Trade', () => {
expect(screen.getAllByText('Trade Parameters')).toHaveLength(2);
expect(screen.getByText('Chart')).toBeInTheDocument();
expect(screen.getByText('Purchase Button')).toBeInTheDocument();
+ expect(screen.getByText('OnboardingGuide')).toBeInTheDocument();
});
it('should render Current Spot component if it is digit contract type', () => {
@@ -142,4 +145,19 @@ describe('Trade', () => {
expect(spySetIsMinimizedParamsVisible).toBeCalled();
});
+
+ it('should not render OnboardingGuide if localStorage flag is equal to true', () => {
+ const key = 'guide_dtrader_v2_trade_page';
+ localStorage.setItem(key, 'true');
+ render(mockTrade());
+
+ expect(screen.queryByText('OnboardingGuide')).not.toBeInTheDocument();
+ });
+
+ it('should not render OnboardingGuide if client is not logged in', () => {
+ default_mock_store.client.is_logged_in = false;
+ render(mockTrade());
+
+ expect(screen.queryByText('OnboardingGuide')).not.toBeInTheDocument();
+ });
});
diff --git a/packages/trader/src/AppV2/Containers/Trade/trade.scss b/packages/trader/src/AppV2/Containers/Trade/trade.scss
index 75b54851c697..814031fb2817 100644
--- a/packages/trader/src/AppV2/Containers/Trade/trade.scss
+++ b/packages/trader/src/AppV2/Containers/Trade/trade.scss
@@ -50,4 +50,7 @@
position: relative;
}
}
+ &__parameter {
+ display: inline;
+ }
}
diff --git a/packages/trader/src/AppV2/Containers/Trade/trade.tsx b/packages/trader/src/AppV2/Containers/Trade/trade.tsx
index 8ea97ff057cd..35388149bbf4 100644
--- a/packages/trader/src/AppV2/Containers/Trade/trade.tsx
+++ b/packages/trader/src/AppV2/Containers/Trade/trade.tsx
@@ -1,6 +1,9 @@
import React from 'react';
import { observer } from 'mobx-react';
+import { useStore } from '@deriv/stores';
import { Loading } from '@deriv/components';
+import { isAccumulatorContract } from '@deriv/shared';
+import { useLocalStorageData } from '@deriv/hooks';
import ClosedMarketMessage from 'AppV2/Components/ClosedMarketMessage';
import { useTraderStore } from 'Stores/useTraderStores';
import BottomNav from 'AppV2/Components/BottomNav';
@@ -15,14 +18,17 @@ import TradeTypes from './trade-types';
import MarketSelector from 'AppV2/Components/MarketSelector';
import useContractsForCompany, { TContractTypesList } from 'AppV2/Hooks/useContractsForCompany';
import AccumulatorStats from 'AppV2/Components/AccumulatorStats';
-import { isAccumulatorContract } from '@deriv/shared';
+import OnboardingGuide from 'AppV2/Components/OnboardingGuide';
const Trade = observer(() => {
const [is_minimized_params_visible, setIsMinimizedParamsVisible] = React.useState(false);
const chart_ref = React.useRef(null);
-
+ const {
+ client: { is_logged_in },
+ } = useStore();
const { active_symbols, contract_type, onMount, onChange, onUnmount } = useTraderStore();
const { contract_types_list } = useContractsForCompany();
+ const [guide_dtrader_v2] = useLocalStorageData('guide_dtrader_v2_trade_page', false);
const trade_types = React.useMemo(() => {
return Array.isArray(contract_types_list) && contract_types_list.length === 0
@@ -85,15 +91,20 @@ const Trade = observer(() => {
-
+
+
+
{isAccumulatorContract(contract_type) && }
-
-
-
-
+
+ {!guide_dtrader_v2 && is_logged_in && }
) : (