Skip to content

Commit

Permalink
Shayan/feq 2275/add skeleton loader to header (#137)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shayan-deriv authored Jun 19, 2024
1 parent 2bff9b7 commit b2e10c0
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 57 deletions.
20 changes: 16 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@babel/preset-env": "^7.24.5",
"@deriv-com/api-hooks": "^1.1.3",
"@deriv-com/api-hooks": "^1.1.5",
"@deriv-com/translations": "^1.2.4",
"@deriv-com/ui": "^1.28.3",
"@deriv-com/utils": "^0.0.25",
Expand All @@ -36,6 +36,7 @@
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-calendar": "^5.0.0",
"react-content-loader": "^7.0.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.51.1",
Expand Down
22 changes: 13 additions & 9 deletions src/components/AppHeader/AccountSwitcher/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { api } from '@/hooks';
import { useActiveAccount } from '@/hooks/api/account';
import { CurrencyUsdIcon } from '@deriv/quill-icons';
import { AccountSwitcher as UIAccountSwitcher } from '@deriv-com/ui';
import { FormatUtils } from '@deriv-com/utils';

export const AccountSwitcher = () => {
const { data } = api.account.useActiveAccount();
type TActiveAccount = NonNullable<ReturnType<typeof useActiveAccount>['data']>;
type TAccountSwitcherProps = {
account: TActiveAccount;
};

export const AccountSwitcher = ({ account }: TAccountSwitcherProps) => {
const activeAccount = {
balance: FormatUtils.formatMoney(data?.balance ?? 0),
currency: data?.currency || 'USD',
currencyLabel: data?.currency || 'US Dollar',
balance: FormatUtils.formatMoney(account?.balance ?? 0),
currency: account?.currency || 'USD',
currencyLabel: account?.currency || 'US Dollar',
icon: <CurrencyUsdIcon iconSize='sm' />,
isActive: true,
isVirtual: Boolean(data?.is_virtual),
loginid: data?.loginid || '',
isVirtual: Boolean(account?.is_virtual),
loginid: account?.loginid || '',
};
return data && <UIAccountSwitcher activeAccount={activeAccount} buttonClassName='mr-4' isDisabled />;
return account && <UIAccountSwitcher activeAccount={activeAccount} buttonClassName='mr-4' isDisabled />;
};
85 changes: 50 additions & 35 deletions src/components/AppHeader/AppHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getOauthUrl } from '@/constants';
import { api } from '@/hooks';
import { getCurrentRoute } from '@/utils';
import { StandaloneCircleUserRegularIcon } from '@deriv/quill-icons';
import { useAuthData } from '@deriv-com/api-hooks';
import { useTranslations } from '@deriv-com/translations';
Expand All @@ -9,15 +11,62 @@ import { MenuItems } from './MenuItems';
import { MobileMenu } from './MobileMenu';
import { Notifications } from './Notifications';
import { PlatformSwitcher } from './PlatformSwitcher';
import { AccountsInfoLoader } from './SkeletonLoader';
import './AppHeader.scss';

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

const renderAccountSection = () => {
if (!isEndpointPage && !activeAccount) {
return <AccountsInfoLoader isLoggedIn isMobile={!isDesktop} speed={3} />;
}

if (activeLoginid) {
return (
<>
<Notifications />
{isDesktop && (
<TooltipMenuIcon
as='a'
className='pr-3 border-r-[0.1rem] h-[3.2rem]'
disableHover
href='https://app.deriv.com/account/personal-details'
tooltipContainerClassName='z-20'
tooltipContent={localize('Manage account settings')}
tooltipPosition='bottom'
>
<StandaloneCircleUserRegularIcon fill='#626262' />
</TooltipMenuIcon>
)}
<AccountSwitcher account={activeAccount!} />
<Button className='mr-6' onClick={logout} size='md'>
<Text size='sm' weight='bold'>
{localize('Logout')}
</Text>
</Button>
</>
);
}

return (
<Button
className='w-36'
color='primary-light'
onClick={() => window.open(oauthUrl, '_self')}
variant='ghost'
>
<Text weight='bold'>{localize('Log in')}</Text>
</Button>
);
};

return (
<Header className={!isDesktop ? 'h-[40px]' : ''}>
<Wrapper variant='left'>
Expand All @@ -26,41 +75,7 @@ const AppHeader = () => {
{isDesktop && <PlatformSwitcher />}
<MenuItems />
</Wrapper>
<Wrapper variant='right'>
{activeLoginid ? (
<>
<Notifications />
{isDesktop && (
<TooltipMenuIcon
as='a'
className='pr-3 border-r-[1px] h-[32px]'
disableHover
href='https://app.deriv.com/account/personal-details'
tooltipContainerClassName='z-20'
tooltipContent={localize('Manage account settings')}
tooltipPosition='bottom'
>
<StandaloneCircleUserRegularIcon fill='#626262' />
</TooltipMenuIcon>
)}
<AccountSwitcher />
<Button className='mr-6' onClick={logout} size='md'>
<Text size='sm' weight='bold'>
{localize('Logout')}
</Text>
</Button>
</>
) : (
<Button
className='w-36'
color='primary-light'
onClick={() => window.open(oauthUrl, '_self')}
variant='ghost'
>
<Text weight='bold'>{localize('Log in')}</Text>
</Button>
)}
</Wrapper>
<Wrapper variant='right'>{renderAccountSection()}</Wrapper>
</Header>
);
};
Expand Down
41 changes: 41 additions & 0 deletions src/components/AppHeader/SkeletonLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ContentLoader from 'react-content-loader';

type TAccountsInfoLoaderProps = {
isLoggedIn: boolean;
isMobile: boolean;
speed: number;
};

const LoggedInPreloader = ({ isMobile }: Pick<TAccountsInfoLoaderProps, 'isMobile'>) => (
<>
{isMobile ? (
<>
<circle cx='14' cy='22' r='13' />
<rect height='7' rx='4' ry='4' width='76' x='35' y='19' />
<rect height='32' rx='4' ry='4' width='82' x='120' y='6' />
</>
) : (
<>
<circle cx='14' cy='22' r='12' />
<circle cx='58' cy='22' r='12' />
<rect height='7' rx='4' ry='4' width='76' x='150' y='20' />
<circle cx='118' cy='24' r='13' />
<rect height='30' rx='4' ry='4' width='1' x='87' y='8' />
<rect height='32' rx='4' ry='4' width='82' x='250' y='8' />
</>
)}
</>
);

export const AccountsInfoLoader = ({ isMobile, speed }: TAccountsInfoLoaderProps) => (
<ContentLoader
backgroundColor={'#f2f3f4'}
data-testid='dt_accounts_info_loader'
foregroundColor={'#e6e9e9'}
height={isMobile ? 42 : 46}
speed={speed}
width={isMobile ? 216 : 350}
>
<LoggedInPreloader isMobile={isMobile} />
</ContentLoader>
);
27 changes: 23 additions & 4 deletions src/components/AppHeader/__tests__/AppHeader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactNode } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
import { useActiveAccount } from '@/hooks/api/account';
import { useAuthData } from '@deriv-com/api-hooks';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
Expand All @@ -20,7 +21,7 @@ jest.mock('@deriv-com/api-hooks', () => ({
},
],
})),
useAuthData: jest.fn(() => ({ activeLoginid: null, logout: jest.fn() })),
useAuthData: jest.fn(() => ({ activeLoginid: null, error: null, logout: jest.fn() })),
useBalance: jest.fn(() => ({
data: {
balance: {
Expand Down Expand Up @@ -76,28 +77,46 @@ jest.mock('@deriv-com/ui', () => ({
useDevice: jest.fn(() => ({ isDesktop: true })),
}));

const mockUseActiveAccountValues = {
data: undefined,
} as ReturnType<typeof useActiveAccount>;

jest.mock('@/hooks', () => ({
...jest.requireActual('@/hooks'),
api: {
account: {
useActiveAccount: jest.fn(() => ({
...mockUseActiveAccountValues,
})),
},
},
}));

describe('<AppHeader/>', () => {
window.open = jest.fn();

afterEach(() => {
jest.clearAllMocks();
});

it('should render the header and handle login when there are no P2P accounts', async () => {
it('should show loader when active account data is not fetched yet', async () => {
render(
<BrowserRouter>
<QueryParamProvider adapter={ReactRouter5Adapter}>
<AppHeader />
</QueryParamProvider>
</BrowserRouter>
);
await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
const loaderElement = screen.getByTestId('dt_accounts_info_loader');

expect(window.open).toHaveBeenCalledWith(expect.any(String), '_self');
expect(loaderElement).toBeInTheDocument();
});

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

Object.defineProperty(window, 'matchMedia', {
value: jest.fn().mockImplementation(query => ({
Expand Down
10 changes: 6 additions & 4 deletions src/hooks/custom-hooks/useRedirectToOauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ import { useAuthData } from '@deriv-com/api-hooks';

const useRedirectToOauth = () => {
const [shouldRedirect, setShouldRedirect] = useState(false);
const { isAuthorized, isAuthorizing } = useAuthData();
const { error, isAuthorized, isAuthorizing } = useAuthData();
const isEndpointPage = getCurrentRoute() === 'endpoint';

const oauthUrl = getOauthUrl();
const redirectToOauth = useCallback(() => {
shouldRedirect && window.open(oauthUrl, '_self');
}, [oauthUrl, shouldRedirect]);

useEffect(() => {
if (!isEndpointPage && !isAuthorized && !isAuthorizing) {
if (
(!isEndpointPage && !isAuthorized && !isAuthorizing) ||
(!isEndpointPage && error?.code === 'InvalidToken')
) {
setShouldRedirect(true);
}
}, [isAuthorized, isAuthorizing, isEndpointPage, oauthUrl]);
}, [error, isAuthorized, isAuthorizing, isEndpointPage, oauthUrl]);

return {
redirectToOauth,
Expand Down

0 comments on commit b2e10c0

Please sign in to comment.