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 && } ) : (