From b2e10c00c0a01d3c7146b08a08252c6d7bcc1ed3 Mon Sep 17 00:00:00 2001 From: Shayan Khaleghparast <100833613+shayan-deriv@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:24:51 +0800 Subject: [PATCH] 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 --- package-lock.json | 20 ++++- package.json | 3 +- .../AppHeader/AccountSwitcher/index.tsx | 22 +++-- src/components/AppHeader/AppHeader.tsx | 85 +++++++++++-------- .../AppHeader/SkeletonLoader/index.tsx | 41 +++++++++ .../AppHeader/__tests__/AppHeader.spec.tsx | 27 +++++- src/hooks/custom-hooks/useRedirectToOauth.ts | 10 ++- 7 files changed, 151 insertions(+), 57 deletions(-) create mode 100644 src/components/AppHeader/SkeletonLoader/index.tsx diff --git a/package-lock.json b/package-lock.json index 960116ac..bcde4d39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "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", @@ -30,6 +30,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", @@ -2604,9 +2605,9 @@ } }, "node_modules/@deriv-com/api-hooks": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@deriv-com/api-hooks/-/api-hooks-1.1.3.tgz", - "integrity": "sha512-qHti+b1iWyeV51EqqWMQTrzUasF3rzXlTD0IqRwdxyZ8gSt6PtfqKP41n6SFbfoz3MzSYVFcwFEULqBwUrv54Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@deriv-com/api-hooks/-/api-hooks-1.1.5.tgz", + "integrity": "sha512-RrqldP9DmTh1/bs/DE1qvKpM1j4gyJzfrfbbEiwE6IA5j16XbrkdBkoi5Ac72Tom36myrvUITf50zF6hN3znGA==", "dependencies": { "@deriv-com/utils": "^0.0.25", "@deriv/api-types": "^1.0.667", @@ -16167,6 +16168,17 @@ } } }, + "node_modules/react-content-loader": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-7.0.2.tgz", + "integrity": "sha512-773S98JTyC8VB2nu7LXUhpHx8tZMieGxMcx3qTe7IkohT6Br7d9AXnIXs/wQ6IhlUdKQcw6JLKk1QKigYCWDRA==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/package.json b/package.json index db43162c..fdf04682 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/components/AppHeader/AccountSwitcher/index.tsx b/src/components/AppHeader/AccountSwitcher/index.tsx index 83c39e3d..7808af67 100644 --- a/src/components/AppHeader/AccountSwitcher/index.tsx +++ b/src/components/AppHeader/AccountSwitcher/index.tsx @@ -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['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: , isActive: true, - isVirtual: Boolean(data?.is_virtual), - loginid: data?.loginid || '', + isVirtual: Boolean(account?.is_virtual), + loginid: account?.loginid || '', }; - return data && ; + return account && ; }; diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index c3ee2a2e..f5c9074a 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -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'; @@ -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 ; + } + + if (activeLoginid) { + return ( + <> + + {isDesktop && ( + + + + )} + + + + ); + } + + return ( + + ); + }; + return (
@@ -26,41 +75,7 @@ const AppHeader = () => { {isDesktop && } - - {activeLoginid ? ( - <> - - {isDesktop && ( - - - - )} - - - - ) : ( - - )} - + {renderAccountSection()}
); }; diff --git a/src/components/AppHeader/SkeletonLoader/index.tsx b/src/components/AppHeader/SkeletonLoader/index.tsx new file mode 100644 index 00000000..eacb7b18 --- /dev/null +++ b/src/components/AppHeader/SkeletonLoader/index.tsx @@ -0,0 +1,41 @@ +import ContentLoader from 'react-content-loader'; + +type TAccountsInfoLoaderProps = { + isLoggedIn: boolean; + isMobile: boolean; + speed: number; +}; + +const LoggedInPreloader = ({ isMobile }: Pick) => ( + <> + {isMobile ? ( + <> + + + + + ) : ( + <> + + + + + + + + )} + +); + +export const AccountsInfoLoader = ({ isMobile, speed }: TAccountsInfoLoaderProps) => ( + + + +); diff --git a/src/components/AppHeader/__tests__/AppHeader.spec.tsx b/src/components/AppHeader/__tests__/AppHeader.spec.tsx index 8816060c..b022462a 100644 --- a/src/components/AppHeader/__tests__/AppHeader.spec.tsx +++ b/src/components/AppHeader/__tests__/AppHeader.spec.tsx @@ -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'; @@ -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: { @@ -76,6 +77,21 @@ jest.mock('@deriv-com/ui', () => ({ useDevice: jest.fn(() => ({ isDesktop: true })), })); +const mockUseActiveAccountValues = { + data: undefined, +} as ReturnType; + +jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), + api: { + account: { + useActiveAccount: jest.fn(() => ({ + ...mockUseActiveAccountValues, + })), + }, + }, +})); + describe('', () => { window.open = jest.fn(); @@ -83,7 +99,7 @@ describe('', () => { 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( @@ -91,13 +107,16 @@ describe('', () => { ); - 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['data']; Object.defineProperty(window, 'matchMedia', { value: jest.fn().mockImplementation(query => ({ diff --git a/src/hooks/custom-hooks/useRedirectToOauth.ts b/src/hooks/custom-hooks/useRedirectToOauth.ts index 8584a5e9..db35f0ba 100644 --- a/src/hooks/custom-hooks/useRedirectToOauth.ts +++ b/src/hooks/custom-hooks/useRedirectToOauth.ts @@ -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,