Skip to content

Commit b2e10c0

Browse files
authored
Shayan/feq 2275/add skeleton loader to header (#137)
* feat: added preloader component [WIP] * feat: added preloader component [WIP] * feat: added skeleton loader for header * chore: removed nested ternary * fix: addressed pr comments * fix: addressed pr comments * fix: addressed pr comments * fix: fixed test issue * chore: removed console.log
1 parent 2bff9b7 commit b2e10c0

File tree

7 files changed

+151
-57
lines changed

7 files changed

+151
-57
lines changed

package-lock.json

+16-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"dependencies": {
1717
"@babel/preset-env": "^7.24.5",
18-
"@deriv-com/api-hooks": "^1.1.3",
18+
"@deriv-com/api-hooks": "^1.1.5",
1919
"@deriv-com/translations": "^1.2.4",
2020
"@deriv-com/ui": "^1.28.3",
2121
"@deriv-com/utils": "^0.0.25",
@@ -36,6 +36,7 @@
3636
"qrcode.react": "^3.1.0",
3737
"react": "^18.2.0",
3838
"react-calendar": "^5.0.0",
39+
"react-content-loader": "^7.0.2",
3940
"react-dom": "^18.2.0",
4041
"react-dropzone": "^14.2.3",
4142
"react-hook-form": "^7.51.1",
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
import { api } from '@/hooks';
1+
import { useActiveAccount } from '@/hooks/api/account';
22
import { CurrencyUsdIcon } from '@deriv/quill-icons';
33
import { AccountSwitcher as UIAccountSwitcher } from '@deriv-com/ui';
44
import { FormatUtils } from '@deriv-com/utils';
55

6-
export const AccountSwitcher = () => {
7-
const { data } = api.account.useActiveAccount();
6+
type TActiveAccount = NonNullable<ReturnType<typeof useActiveAccount>['data']>;
7+
type TAccountSwitcherProps = {
8+
account: TActiveAccount;
9+
};
10+
11+
export const AccountSwitcher = ({ account }: TAccountSwitcherProps) => {
812
const activeAccount = {
9-
balance: FormatUtils.formatMoney(data?.balance ?? 0),
10-
currency: data?.currency || 'USD',
11-
currencyLabel: data?.currency || 'US Dollar',
13+
balance: FormatUtils.formatMoney(account?.balance ?? 0),
14+
currency: account?.currency || 'USD',
15+
currencyLabel: account?.currency || 'US Dollar',
1216
icon: <CurrencyUsdIcon iconSize='sm' />,
1317
isActive: true,
14-
isVirtual: Boolean(data?.is_virtual),
15-
loginid: data?.loginid || '',
18+
isVirtual: Boolean(account?.is_virtual),
19+
loginid: account?.loginid || '',
1620
};
17-
return data && <UIAccountSwitcher activeAccount={activeAccount} buttonClassName='mr-4' isDisabled />;
21+
return account && <UIAccountSwitcher activeAccount={activeAccount} buttonClassName='mr-4' isDisabled />;
1822
};

src/components/AppHeader/AppHeader.tsx

+50-35
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { getOauthUrl } from '@/constants';
2+
import { api } from '@/hooks';
3+
import { getCurrentRoute } from '@/utils';
24
import { StandaloneCircleUserRegularIcon } from '@deriv/quill-icons';
35
import { useAuthData } from '@deriv-com/api-hooks';
46
import { useTranslations } from '@deriv-com/translations';
@@ -9,15 +11,62 @@ import { MenuItems } from './MenuItems';
911
import { MobileMenu } from './MobileMenu';
1012
import { Notifications } from './Notifications';
1113
import { PlatformSwitcher } from './PlatformSwitcher';
14+
import { AccountsInfoLoader } from './SkeletonLoader';
1215
import './AppHeader.scss';
1316

1417
// TODO: handle local storage values not updating after changing local storage values
1518
const AppHeader = () => {
1619
const { isDesktop } = useDevice();
20+
const isEndpointPage = getCurrentRoute() === 'endpoint';
1721
const { activeLoginid, logout } = useAuthData();
22+
const { data: activeAccount } = api.account.useActiveAccount();
1823
const { localize } = useTranslations();
1924
const oauthUrl = getOauthUrl();
2025

26+
const renderAccountSection = () => {
27+
if (!isEndpointPage && !activeAccount) {
28+
return <AccountsInfoLoader isLoggedIn isMobile={!isDesktop} speed={3} />;
29+
}
30+
31+
if (activeLoginid) {
32+
return (
33+
<>
34+
<Notifications />
35+
{isDesktop && (
36+
<TooltipMenuIcon
37+
as='a'
38+
className='pr-3 border-r-[0.1rem] h-[3.2rem]'
39+
disableHover
40+
href='https://app.deriv.com/account/personal-details'
41+
tooltipContainerClassName='z-20'
42+
tooltipContent={localize('Manage account settings')}
43+
tooltipPosition='bottom'
44+
>
45+
<StandaloneCircleUserRegularIcon fill='#626262' />
46+
</TooltipMenuIcon>
47+
)}
48+
<AccountSwitcher account={activeAccount!} />
49+
<Button className='mr-6' onClick={logout} size='md'>
50+
<Text size='sm' weight='bold'>
51+
{localize('Logout')}
52+
</Text>
53+
</Button>
54+
</>
55+
);
56+
}
57+
58+
return (
59+
<Button
60+
className='w-36'
61+
color='primary-light'
62+
onClick={() => window.open(oauthUrl, '_self')}
63+
variant='ghost'
64+
>
65+
<Text weight='bold'>{localize('Log in')}</Text>
66+
</Button>
67+
);
68+
};
69+
2170
return (
2271
<Header className={!isDesktop ? 'h-[40px]' : ''}>
2372
<Wrapper variant='left'>
@@ -26,41 +75,7 @@ const AppHeader = () => {
2675
{isDesktop && <PlatformSwitcher />}
2776
<MenuItems />
2877
</Wrapper>
29-
<Wrapper variant='right'>
30-
{activeLoginid ? (
31-
<>
32-
<Notifications />
33-
{isDesktop && (
34-
<TooltipMenuIcon
35-
as='a'
36-
className='pr-3 border-r-[1px] h-[32px]'
37-
disableHover
38-
href='https://app.deriv.com/account/personal-details'
39-
tooltipContainerClassName='z-20'
40-
tooltipContent={localize('Manage account settings')}
41-
tooltipPosition='bottom'
42-
>
43-
<StandaloneCircleUserRegularIcon fill='#626262' />
44-
</TooltipMenuIcon>
45-
)}
46-
<AccountSwitcher />
47-
<Button className='mr-6' onClick={logout} size='md'>
48-
<Text size='sm' weight='bold'>
49-
{localize('Logout')}
50-
</Text>
51-
</Button>
52-
</>
53-
) : (
54-
<Button
55-
className='w-36'
56-
color='primary-light'
57-
onClick={() => window.open(oauthUrl, '_self')}
58-
variant='ghost'
59-
>
60-
<Text weight='bold'>{localize('Log in')}</Text>
61-
</Button>
62-
)}
63-
</Wrapper>
78+
<Wrapper variant='right'>{renderAccountSection()}</Wrapper>
6479
</Header>
6580
);
6681
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ContentLoader from 'react-content-loader';
2+
3+
type TAccountsInfoLoaderProps = {
4+
isLoggedIn: boolean;
5+
isMobile: boolean;
6+
speed: number;
7+
};
8+
9+
const LoggedInPreloader = ({ isMobile }: Pick<TAccountsInfoLoaderProps, 'isMobile'>) => (
10+
<>
11+
{isMobile ? (
12+
<>
13+
<circle cx='14' cy='22' r='13' />
14+
<rect height='7' rx='4' ry='4' width='76' x='35' y='19' />
15+
<rect height='32' rx='4' ry='4' width='82' x='120' y='6' />
16+
</>
17+
) : (
18+
<>
19+
<circle cx='14' cy='22' r='12' />
20+
<circle cx='58' cy='22' r='12' />
21+
<rect height='7' rx='4' ry='4' width='76' x='150' y='20' />
22+
<circle cx='118' cy='24' r='13' />
23+
<rect height='30' rx='4' ry='4' width='1' x='87' y='8' />
24+
<rect height='32' rx='4' ry='4' width='82' x='250' y='8' />
25+
</>
26+
)}
27+
</>
28+
);
29+
30+
export const AccountsInfoLoader = ({ isMobile, speed }: TAccountsInfoLoaderProps) => (
31+
<ContentLoader
32+
backgroundColor={'#f2f3f4'}
33+
data-testid='dt_accounts_info_loader'
34+
foregroundColor={'#e6e9e9'}
35+
height={isMobile ? 42 : 46}
36+
speed={speed}
37+
width={isMobile ? 216 : 350}
38+
>
39+
<LoggedInPreloader isMobile={isMobile} />
40+
</ContentLoader>
41+
);

src/components/AppHeader/__tests__/AppHeader.spec.tsx

+23-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ReactNode } from 'react';
22
import { BrowserRouter } from 'react-router-dom';
33
import { QueryParamProvider } from 'use-query-params';
44
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
5+
import { useActiveAccount } from '@/hooks/api/account';
56
import { useAuthData } from '@deriv-com/api-hooks';
67
import { render, screen } from '@testing-library/react';
78
import userEvent from '@testing-library/user-event';
@@ -20,7 +21,7 @@ jest.mock('@deriv-com/api-hooks', () => ({
2021
},
2122
],
2223
})),
23-
useAuthData: jest.fn(() => ({ activeLoginid: null, logout: jest.fn() })),
24+
useAuthData: jest.fn(() => ({ activeLoginid: null, error: null, logout: jest.fn() })),
2425
useBalance: jest.fn(() => ({
2526
data: {
2627
balance: {
@@ -76,28 +77,46 @@ jest.mock('@deriv-com/ui', () => ({
7677
useDevice: jest.fn(() => ({ isDesktop: true })),
7778
}));
7879

80+
const mockUseActiveAccountValues = {
81+
data: undefined,
82+
} as ReturnType<typeof useActiveAccount>;
83+
84+
jest.mock('@/hooks', () => ({
85+
...jest.requireActual('@/hooks'),
86+
api: {
87+
account: {
88+
useActiveAccount: jest.fn(() => ({
89+
...mockUseActiveAccountValues,
90+
})),
91+
},
92+
},
93+
}));
94+
7995
describe('<AppHeader/>', () => {
8096
window.open = jest.fn();
8197

8298
afterEach(() => {
8399
jest.clearAllMocks();
84100
});
85101

86-
it('should render the header and handle login when there are no P2P accounts', async () => {
102+
it('should show loader when active account data is not fetched yet', async () => {
87103
render(
88104
<BrowserRouter>
89105
<QueryParamProvider adapter={ReactRouter5Adapter}>
90106
<AppHeader />
91107
</QueryParamProvider>
92108
</BrowserRouter>
93109
);
94-
await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
110+
const loaderElement = screen.getByTestId('dt_accounts_info_loader');
95111

96-
expect(window.open).toHaveBeenCalledWith(expect.any(String), '_self');
112+
expect(loaderElement).toBeInTheDocument();
97113
});
98114

99115
it('should render the desktop header and manage account actions when logged in', async () => {
100116
mockUseAuthData.mockReturnValue({ activeLoginid: '12345', logout: jest.fn() });
117+
mockUseActiveAccountValues.data = {
118+
currency: 'USD',
119+
} as ReturnType<typeof useActiveAccount>['data'];
101120

102121
Object.defineProperty(window, 'matchMedia', {
103122
value: jest.fn().mockImplementation(query => ({

src/hooks/custom-hooks/useRedirectToOauth.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import { useAuthData } from '@deriv-com/api-hooks';
55

66
const useRedirectToOauth = () => {
77
const [shouldRedirect, setShouldRedirect] = useState(false);
8-
const { isAuthorized, isAuthorizing } = useAuthData();
8+
const { error, isAuthorized, isAuthorizing } = useAuthData();
99
const isEndpointPage = getCurrentRoute() === 'endpoint';
10-
1110
const oauthUrl = getOauthUrl();
1211
const redirectToOauth = useCallback(() => {
1312
shouldRedirect && window.open(oauthUrl, '_self');
1413
}, [oauthUrl, shouldRedirect]);
1514

1615
useEffect(() => {
17-
if (!isEndpointPage && !isAuthorized && !isAuthorizing) {
16+
if (
17+
(!isEndpointPage && !isAuthorized && !isAuthorizing) ||
18+
(!isEndpointPage && error?.code === 'InvalidToken')
19+
) {
1820
setShouldRedirect(true);
1921
}
20-
}, [isAuthorized, isAuthorizing, isEndpointPage, oauthUrl]);
22+
}, [error, isAuthorized, isAuthorizing, isEndpointPage, oauthUrl]);
2123

2224
return {
2325
redirectToOauth,

0 commit comments

Comments
 (0)