From a4875e402751eacf4b7085aee94f456be46a61fd Mon Sep 17 00:00:00 2001 From: George Usynin <103181646+heorhi-deriv@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:17:44 +0300 Subject: [PATCH] [WALL] george / WALL-4420 / Persist selected accounts tab (CFDs or Options) in Mobile view (#15822) * fix(wallets): :ambulance: persist selected accounts tab * chore(wallets): :alien: apply comments * test(wallets): :test_tube: fix tests * chore(wallets): :art: fix active tav index * fix: :lipstick: align dropdown scss file --- .../components/AccountsList/AccountsList.tsx | 21 ++++++-- .../__tests__/AccountsList.spec.tsx | 38 ++++++++++++-- .../WalletListCardActions.tsx | 8 ++- .../__tests__/WalletListCardActions.spec.tsx | 52 ++++++++++++++++++- .../WalletsCarousel/WalletsCarousel.tsx | 11 +++- .../WalletsCarouselContent.tsx | 14 +++-- .../WalletCashierHeader.tsx | 6 ++- .../__tests__/WalletCashierHeader.spec.tsx | 14 ++++- packages/wallets/src/routes/Router.tsx | 16 +++++- 9 files changed, 160 insertions(+), 20 deletions(-) diff --git a/packages/wallets/src/components/AccountsList/AccountsList.tsx b/packages/wallets/src/components/AccountsList/AccountsList.tsx index 10681343790f..72326a5d357c 100644 --- a/packages/wallets/src/components/AccountsList/AccountsList.tsx +++ b/packages/wallets/src/components/AccountsList/AccountsList.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Divider, Tab, Tabs } from '@deriv-com/ui'; import { CFDPlatformsList } from '../../features'; @@ -7,13 +7,28 @@ import { TSubscribedBalance } from '../../types'; import { OptionsAndMultipliersListing } from '../OptionsAndMultipliersListing'; import './AccountsList.scss'; -const AccountsList: FC = ({ balance }) => { +const tabs = ['CFDs', 'Options']; + +type TProps = { + accountsActiveTabIndex?: number; + balance: TSubscribedBalance['balance']; + onTabClickHandler?: React.Dispatch>; +}; + +const AccountsList: FC = ({ accountsActiveTabIndex, balance, onTabClickHandler }) => { const { isMobile } = useDevice(); const { t } = useTranslation(); + const onChangeTabHandler = useCallback((activeTab: number) => onTabClickHandler?.(activeTab), [onTabClickHandler]); + if (isMobile) { return ( - + diff --git a/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx b/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx index 3fb27d701a97..e7f9ab15bd2d 100644 --- a/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx +++ b/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx @@ -70,7 +70,11 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render(, { wrapper }); + + render(, { + wrapper, + }); + expect(screen.getByText('CFDs')).toBeInTheDocument(); expect(screen.getByText('Options')).toBeInTheDocument(); expect(screen.getByText('Compare accounts')).toBeInTheDocument(); @@ -82,7 +86,11 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render(, { wrapper }); + + render(, { + wrapper, + }); + expect(screen.getByText('CFDs')).toBeInTheDocument(); expect(screen.getAllByText('Options')[0]).toBeInTheDocument(); @@ -95,6 +103,24 @@ describe('AccountsList', () => { expect(screen.getByText('Deriv GO')).toBeInTheDocument(); }); + it('should trigger `onTabClickHandler` with proper tab index when the user switches the tab', () => { + const onTabClickHandler = jest.fn(); + mockUseDevice.mockReturnValue({ + isDesktop: false, + isMobile: true, + isTablet: false, + }); + render( + , + { + wrapper, + } + ); + + screen.getAllByText('Options')[0].click(); + expect(onTabClickHandler).toHaveBeenCalledWith(1); + }); + it('should render account list in desktop view', () => { mockUseDevice.mockReturnValue({ isDesktop: true, @@ -114,7 +140,9 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render(, { wrapper }); + render(, { + wrapper, + }); expect(mockWalletTourGuide); }); @@ -124,7 +152,9 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render(, { wrapper }); + render(, { + wrapper, + }); expect(mockWalletTourGuide); }); }); diff --git a/packages/wallets/src/components/WalletListCardActions/WalletListCardActions.tsx b/packages/wallets/src/components/WalletListCardActions/WalletListCardActions.tsx index 234c82e9cd8b..a94e61a75d82 100644 --- a/packages/wallets/src/components/WalletListCardActions/WalletListCardActions.tsx +++ b/packages/wallets/src/components/WalletListCardActions/WalletListCardActions.tsx @@ -11,6 +11,10 @@ import useDevice from '../../hooks/useDevice'; import { IconButton, WalletButton, WalletText } from '../Base'; import './WalletListCardActions.scss'; +type TProps = { + accountsActiveTabIndex?: number; +}; + const getWalletHeaderButtons = (isDemo?: boolean) => { const buttons = [ { @@ -45,7 +49,7 @@ const getWalletHeaderButtons = (isDemo?: boolean) => { return filteredButtons; }; -const WalletListCardActions = () => { +const WalletListCardActions: React.FC = ({ accountsActiveTabIndex }) => { const { data: activeWallet } = useActiveWalletAccount(); const { isMobile } = useDevice(); const history = useHistory(); @@ -65,7 +69,7 @@ const WalletListCardActions = () => { color={button.color} icon={button.icon} onClick={() => { - history.push(`/wallet/${button.name}`); + history.push(`/wallet/${button.name}`, { accountsActiveTabIndex }); }} size='lg' /> diff --git a/packages/wallets/src/components/WalletListCardActions/__tests__/WalletListCardActions.spec.tsx b/packages/wallets/src/components/WalletListCardActions/__tests__/WalletListCardActions.spec.tsx index 9e98469c48ec..64b99f9a03da 100644 --- a/packages/wallets/src/components/WalletListCardActions/__tests__/WalletListCardActions.spec.tsx +++ b/packages/wallets/src/components/WalletListCardActions/__tests__/WalletListCardActions.spec.tsx @@ -19,7 +19,7 @@ jest.mock('@deriv/api-v2', () => ({ })), })); -jest.mock('.../../../hooks/useDevice'); +jest.mock('../../../hooks/useDevice'); const mockedUseDevice = useDevice as jest.MockedFunction; const history = createMemoryHistory(); @@ -146,4 +146,54 @@ describe('WalletListCardActions', () => { screen.getByRole('button', { name: 'reset-balance' }).click(); expect(history.location.pathname).toBe('/wallet/reset-balance'); }); + + it('passes `accountsActiveTabIndex` in history state, when we redirect the user to the new page in mobile view for REAL wallet', () => { + const realWalletButtons = ['deposit', 'withdrawal', 'account-transfer']; + (useActiveWalletAccount as jest.Mock).mockReturnValue({ + data: { + currency: 'USD', + display_login: 'CRW123456', + email: '', + is_active: true, + is_virtual: false, + loginid: 'CRW123456', + }, + }); + mockedUseDevice.mockReturnValue({ isDesktop: false, isMobile: true, isTablet: false }); + + render(, { + wrapper, + }); + + realWalletButtons.forEach(button => { + screen.getByRole('button', { name: button }).click(); + expect(history.location.pathname).toBe(`/wallet/${button}`); + expect(history.location.state).toStrictEqual({ accountsActiveTabIndex: 1 }); + }); + }); + + it('passes `accountsActiveTabIndex` in history state, when we redirect the user to the new page in mobile view for DEMO wallet', () => { + const demoWalletButtons = ['reset-balance', 'account-transfer']; + (useActiveWalletAccount as jest.Mock).mockReturnValue({ + data: { + currency: 'USD', + display_login: 'VRW123456', + email: '', + is_active: true, + is_virtual: true, + loginid: 'VRW123456', + }, + }); + mockedUseDevice.mockReturnValue({ isDesktop: false, isMobile: true, isTablet: false }); + + render(, { + wrapper, + }); + + demoWalletButtons.forEach(button => { + screen.getByRole('button', { name: button }).click(); + expect(history.location.pathname).toBe(`/wallet/${button}`); + expect(history.location.state).toStrictEqual({ accountsActiveTabIndex: 1 }); + }); + }); }); diff --git a/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx b/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx index ea308f1fef9b..e6e073631159 100644 --- a/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx +++ b/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { useActiveWalletAccount } from '@deriv/api-v2'; import { displayMoney } from '@deriv/api-v2/src/utils'; import { TSubscribedBalance } from '../../types'; @@ -11,6 +12,8 @@ const WalletsCarousel: React.FC = ({ balance }) => { const { data: activeWallet, isLoading: isActiveWalletLoading } = useActiveWalletAccount(); const [hideWalletsCarouselHeader, setHideWalletsCarouselHeader] = useState(true); const contentRef = useRef(null); + const location = useLocation(); + const [accountsActiveTabIndex, setAccountsActiveTabIndex] = useState(location.state?.accountsActiveTabIndex ?? 0); const { data: balanceData, isLoading: isBalanceLoading } = balance; @@ -62,10 +65,14 @@ const WalletsCarousel: React.FC = ({ balance }) => { /> )}
- +
- + ); }; diff --git a/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx b/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx index 3a9ef1cf6c4e..5ad82927fd57 100644 --- a/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx +++ b/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx @@ -16,6 +16,10 @@ import { WalletCard } from '../WalletCard'; import { WalletListCardActions } from '../WalletListCardActions'; import './WalletsCarouselContent.scss'; +type TProps = { + accountsActiveTabIndex: number; +}; + const numberWithinRange = (number: number, min: number, max: number): number => Math.min(Math.max(number, min), max); // scale based on the width difference between active wallet (288px) and inactive wallets + padding (240px + 16px) @@ -27,7 +31,7 @@ const TRANSITION_FACTOR_SCALE = 1 - 25.6 / 28.8; * - Embla is the SINGLE SOURCE OF TRUTH for current active card, so the state flow / data flow is simple * - everything else gets in sync with Embla eventually */ -const WalletsCarouselContent: React.FC = () => { +const WalletsCarouselContent: React.FC = ({ accountsActiveTabIndex }) => { const switchWalletAccount = useWalletAccountSwitcher(); const history = useHistory(); @@ -125,9 +129,11 @@ const WalletsCarouselContent: React.FC = () => { walletsCarouselEmblaApi?.scrollTo(index); walletAccountsList && setSelectedLoginId(walletAccountsList[index].loginid); account.is_active && - (account.is_virtual ? history.push('/wallet/reset-balance') : history.push('/wallet/deposit')); + (account.is_virtual + ? history.push('/wallet/reset-balance', { accountsActiveTabIndex }) + : history.push('/wallet/deposit', { accountsActiveTabIndex })); }, - [walletsCarouselEmblaApi, walletAccountsList, history] + [walletsCarouselEmblaApi, walletAccountsList, history, accountsActiveTabIndex] ); useEffect(() => { @@ -278,7 +284,7 @@ const WalletsCarouselContent: React.FC = () => { - + ); }; diff --git a/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx b/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx index ffa2d0d387d9..a7ad9699d68f 100644 --- a/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx +++ b/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx @@ -69,6 +69,7 @@ const WalletCashierHeader: React.FC = ({ hideWalletDetails }) => { const activeTabRef = useRef(null); const history = useHistory(); const location = useLocation(); + const accountsActiveTabIndexRef = useRef(location.state?.accountsActiveTabIndex ?? 0); const tabs = activeWallet?.is_virtual ? virtualAccountTabs : realAccountTabs; const isDemo = activeWallet?.is_virtual; @@ -140,8 +141,11 @@ const WalletCashierHeader: React.FC = ({ hideWalletDetails }) => { className={classNames('wallets-cashier-header__close-icon', { 'wallets-cashier-header__close-icon--white': isDemo, })} + data-testid='dt_close_btn' iconSize='xs' - onClick={() => history.push('/')} + onClick={() => + history.push('/', { accountsActiveTabIndex: accountsActiveTabIndexRef?.current }) + } /> diff --git a/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx b/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx index 448d97cb18a7..5651ab69f4a3 100644 --- a/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx +++ b/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { APIProvider, useActiveWalletAccount, useBalanceSubscription } from '@deriv/api-v2'; import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import WalletsAuthProvider from '../../../../../AuthProvider'; import WalletCashierHeader from '../WalletCashierHeader'; @@ -10,9 +11,11 @@ jest.mock('@deriv/api-v2', () => ({ useBalanceSubscription: jest.fn(), })); +const mockPush = jest.fn(); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useHistory: () => ({ history: {} }), + useHistory: () => ({ push: mockPush }), useLocation: () => ({ pathname: '/' }), })); @@ -128,4 +131,13 @@ describe('', () => { expect(screen.getByText('Transfer')).toBeInTheDocument(); expect(screen.getByText('Transactions')).toBeInTheDocument(); }); + + it('redirects to the root route with `accountsActiveTabIndex` history state, when the user closes cashier overlay', () => { + render(, { wrapper }); + + const closeBtn = screen.getByTestId('dt_close_btn'); + userEvent.click(closeBtn); + + expect(mockPush).toHaveBeenCalledWith('/', { accountsActiveTabIndex: 0 }); + }); }); diff --git a/packages/wallets/src/routes/Router.tsx b/packages/wallets/src/routes/Router.tsx index c32630a8746c..8f096400485e 100644 --- a/packages/wallets/src/routes/Router.tsx +++ b/packages/wallets/src/routes/Router.tsx @@ -28,8 +28,20 @@ export type TRoute = '/endpoint' | `?${string}` | `${TWalletsRoute}`; // wallets routes which have their states interface WalletsRouteState { - '/wallet/account-transfer': { shouldSelectDefaultWallet: boolean; toAccountLoginId: string }; - '/wallet/transactions': { showPending: boolean; transactionType: 'deposit' | 'withdrawal' }; + '/': { accountsActiveTabIndex: number }; + '/wallet/account-transfer': { + accountsActiveTabIndex: number; + shouldSelectDefaultWallet: boolean; + toAccountLoginId: string; + }; + '/wallet/deposit': { accountsActiveTabIndex: number }; + '/wallet/reset-balance': { accountsActiveTabIndex: number }; + '/wallet/transactions': { + accountsActiveTabIndex: number; + showPending: boolean; + transactionType: 'deposit' | 'withdrawal'; + }; + '/wallet/withdrawal': { accountsActiveTabIndex: number }; } type TStatefulRoute = TRoute & `${keyof WalletsRouteState}`;