Skip to content

Commit

Permalink
[WALL] george / WALL-4420 / Persist selected accounts tab (CFDs or Op…
Browse files Browse the repository at this point in the history
…tions) in Mobile view (deriv-com#15822)

* fix(wallets): 🚑 persist selected accounts tab

* chore(wallets): 👽 apply comments

* test(wallets): 🧪 fix tests

* chore(wallets): 🎨 fix active tav index

* fix: 💄 align dropdown scss file
  • Loading branch information
heorhi-deriv authored Jul 11, 2024
1 parent 98e540a commit a4875e4
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 20 deletions.
21 changes: 18 additions & 3 deletions packages/wallets/src/components/AccountsList/AccountsList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,13 +7,28 @@ import { TSubscribedBalance } from '../../types';
import { OptionsAndMultipliersListing } from '../OptionsAndMultipliersListing';
import './AccountsList.scss';

const AccountsList: FC<TSubscribedBalance> = ({ balance }) => {
const tabs = ['CFDs', 'Options'];

type TProps = {
accountsActiveTabIndex?: number;
balance: TSubscribedBalance['balance'];
onTabClickHandler?: React.Dispatch<React.SetStateAction<number>>;
};

const AccountsList: FC<TProps> = ({ accountsActiveTabIndex, balance, onTabClickHandler }) => {
const { isMobile } = useDevice();
const { t } = useTranslation();

const onChangeTabHandler = useCallback((activeTab: number) => onTabClickHandler?.(activeTab), [onTabClickHandler]);

if (isMobile) {
return (
<Tabs activeTab='CFDs' className='wallets-accounts-list__tabs' wrapperClassName='wallets-accounts-list'>
<Tabs
activeTab={tabs[accountsActiveTabIndex ?? 0]}
className='wallets-accounts-list__tabs'
onChange={onChangeTabHandler}
wrapperClassName='wallets-accounts-list'
>
<Tab className='wallets-accounts-list__tab' title={t('CFDs')}>
<CFDPlatformsList />
<Divider color='var(--wallets-banner-border-color)' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ describe('AccountsList', () => {
isMobile: true,
isTablet: false,
});
render(<AccountsList balance={mockBalanceData} />, { wrapper });

render(<AccountsList accountsActiveTabIndex={0} balance={mockBalanceData} onTabClickHandler={jest.fn()} />, {
wrapper,
});

expect(screen.getByText('CFDs')).toBeInTheDocument();
expect(screen.getByText('Options')).toBeInTheDocument();
expect(screen.getByText('Compare accounts')).toBeInTheDocument();
Expand All @@ -82,7 +86,11 @@ describe('AccountsList', () => {
isMobile: true,
isTablet: false,
});
render(<AccountsList balance={mockBalanceData} />, { wrapper });

render(<AccountsList accountsActiveTabIndex={0} balance={mockBalanceData} onTabClickHandler={jest.fn()} />, {
wrapper,
});

expect(screen.getByText('CFDs')).toBeInTheDocument();
expect(screen.getAllByText('Options')[0]).toBeInTheDocument();

Expand All @@ -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(
<AccountsList accountsActiveTabIndex={0} balance={mockBalanceData} onTabClickHandler={onTabClickHandler} />,
{
wrapper,
}
);

screen.getAllByText('Options')[0].click();
expect(onTabClickHandler).toHaveBeenCalledWith(1);
});

it('should render account list in desktop view', () => {
mockUseDevice.mockReturnValue({
isDesktop: true,
Expand All @@ -114,7 +140,9 @@ describe('AccountsList', () => {
isMobile: true,
isTablet: false,
});
render(<AccountsList balance={mockBalanceData} />, { wrapper });
render(<AccountsList accountsActiveTabIndex={0} balance={mockBalanceData} onTabClickHandler={jest.fn()} />, {
wrapper,
});
expect(mockWalletTourGuide);
});

Expand All @@ -124,7 +152,9 @@ describe('AccountsList', () => {
isMobile: true,
isTablet: false,
});
render(<AccountsList balance={mockBalanceData} />, { wrapper });
render(<AccountsList accountsActiveTabIndex={0} balance={mockBalanceData} onTabClickHandler={jest.fn()} />, {
wrapper,
});
expect(mockWalletTourGuide);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -45,7 +49,7 @@ const getWalletHeaderButtons = (isDemo?: boolean) => {
return filteredButtons;
};

const WalletListCardActions = () => {
const WalletListCardActions: React.FC<TProps> = ({ accountsActiveTabIndex }) => {
const { data: activeWallet } = useActiveWalletAccount();
const { isMobile } = useDevice();
const history = useHistory();
Expand All @@ -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'
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jest.mock('@deriv/api-v2', () => ({
})),
}));

jest.mock('.../../../hooks/useDevice');
jest.mock('../../../hooks/useDevice');
const mockedUseDevice = useDevice as jest.MockedFunction<typeof useDevice>;

const history = createMemoryHistory();
Expand Down Expand Up @@ -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(<WalletListCardActions accountsActiveTabIndex={1} />, {
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(<WalletListCardActions accountsActiveTabIndex={1} />, {
wrapper,
});

demoWalletButtons.forEach(button => {
screen.getByRole('button', { name: button }).click();
expect(history.location.pathname).toBe(`/wallet/${button}`);
expect(history.location.state).toStrictEqual({ accountsActiveTabIndex: 1 });
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,6 +12,8 @@ const WalletsCarousel: React.FC<TSubscribedBalance> = ({ 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;

Expand Down Expand Up @@ -62,10 +65,14 @@ const WalletsCarousel: React.FC<TSubscribedBalance> = ({ balance }) => {
/>
)}
<div ref={contentRef}>
<WalletsCarouselContent />
<WalletsCarouselContent accountsActiveTabIndex={accountsActiveTabIndex} />
</div>
</div>
<AccountsList balance={balance} />
<AccountsList
accountsActiveTabIndex={accountsActiveTabIndex}
balance={balance}
onTabClickHandler={setAccountsActiveTabIndex}
/>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<TProps> = ({ accountsActiveTabIndex }) => {
const switchWalletAccount = useWalletAccountSwitcher();
const history = useHistory();

Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -278,7 +284,7 @@ const WalletsCarouselContent: React.FC = () => {
</div>
</div>
</div>
<WalletListCardActions />
<WalletListCardActions accountsActiveTabIndex={accountsActiveTabIndex} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const WalletCashierHeader: React.FC<TProps> = ({ hideWalletDetails }) => {
const activeTabRef = useRef<HTMLButtonElement>(null);
const history = useHistory();
const location = useLocation();
const accountsActiveTabIndexRef = useRef<number>(location.state?.accountsActiveTabIndex ?? 0);

const tabs = activeWallet?.is_virtual ? virtualAccountTabs : realAccountTabs;
const isDemo = activeWallet?.is_virtual;
Expand Down Expand Up @@ -140,8 +141,11 @@ const WalletCashierHeader: React.FC<TProps> = ({ 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 })
}
/>
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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: '/' }),
}));

Expand Down Expand Up @@ -128,4 +131,13 @@ describe('<WalletCashierHeader/>', () => {
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(<WalletCashierHeader hideWalletDetails={false} />, { wrapper });

const closeBtn = screen.getByTestId('dt_close_btn');
userEvent.click(closeBtn);

expect(mockPush).toHaveBeenCalledWith('/', { accountsActiveTabIndex: 0 });
});
});
16 changes: 14 additions & 2 deletions packages/wallets/src/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down

0 comments on commit a4875e4

Please sign in to comment.