From 3c8847911d196f5d417ff5b60bc294f1c9917c79 Mon Sep 17 00:00:00 2001 From: RickyRoller Date: Mon, 10 Mar 2025 09:10:20 -0600 Subject: [PATCH 1/3] Add error page and and re-direct user fetch errors --- src/index.tsx | 3 +- src/pages/error/error-component.tsx | 33 ++++++++++++++++ src/pages/error/error-container.tsx | 11 ++++++ src/pages/error/error.scss | 54 +++++++++++++++++++++++++++ src/pages/error/index.ts | 1 + src/pages/index.ts | 1 + src/store/authentication/api.ts | 9 +++-- src/store/authentication/saga.test.ts | 17 +++++++-- src/store/authentication/saga.ts | 6 +-- src/store/page-load/saga.test.ts | 23 +++++++++--- src/store/page-load/saga.ts | 12 ++++-- 11 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 src/pages/error/error-component.tsx create mode 100644 src/pages/error/error-container.tsx create mode 100644 src/pages/error/error.scss create mode 100644 src/pages/error/index.ts diff --git a/src/index.tsx b/src/index.tsx index 3f9954677..ca7ec5c7b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,7 +14,7 @@ import '@zer0-os/zos-component-library/dist/index.css'; import './index.scss'; import { Invite } from './invite'; import { ResetPassword } from './reset-password'; -import { LoginPage } from './pages'; +import { LoginPage, ErrorPage } from './pages'; import { getHistory } from './lib/browser'; import { ElectronTitlebar } from './components/electron-titlebar'; import { desktopInit } from './lib/desktop'; @@ -50,6 +50,7 @@ root.render( + diff --git a/src/pages/error/error-component.tsx b/src/pages/error/error-component.tsx new file mode 100644 index 000000000..1fbd590d9 --- /dev/null +++ b/src/pages/error/error-component.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { ThemeEngine, Themes } from '@zero-tech/zui/components/ThemeEngine'; +import { Button } from '@zero-tech/zui/components'; +import ZeroLogo from '../../zero-logo.svg?react'; + +import { bemClassName } from '../../lib/bem'; +import './error.scss'; + +const cn = bemClassName('error-page'); + +interface ErrorComponentProperties { + onRetry: () => void; +} + +export const ErrorComponent = ({ onRetry }: ErrorComponentProperties) => { + return ( + <> + +
+
+
+ +
+
+

There was an error loading ZERO, please try again.

+ +
+
+
+ + ); +}; diff --git a/src/pages/error/error-container.tsx b/src/pages/error/error-container.tsx new file mode 100644 index 000000000..7b9e18f84 --- /dev/null +++ b/src/pages/error/error-container.tsx @@ -0,0 +1,11 @@ +import { ErrorComponent } from './error-component'; + +export const ErrorPage = () => { + const handleRetry = () => { + window.location.href = '/'; + }; + + const error = 'There was an error loading ZERO, please try again.'; + + return ; +}; diff --git a/src/pages/error/error.scss b/src/pages/error/error.scss new file mode 100644 index 000000000..53389e5d0 --- /dev/null +++ b/src/pages/error/error.scss @@ -0,0 +1,54 @@ +@use '~@zero-tech/zui/styles/theme' as theme; + +@import '../../background'; +@import '../../glass'; + +.error-page { + @include root-background; + + position: absolute; + overflow-y: auto; + overflow-x: hidden; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + z-index: 10; + + &__content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 24px 0; + box-sizing: border-box; + } + + &__logo-container { + padding-left: 8px; + margin-bottom: 76px; + mix-blend-mode: screen; + } + + &__message-container { + @include glass-shadow-and-blur; + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + padding: 32px; + background: rgba(11, 7, 7, 0.75); + border-radius: 8px; + } + + &__message { + @include glass-text-primary-color; + text-align: center; + font-weight: 600; + font-size: 18px; + line-height: 22px; + margin: 0; + } +} diff --git a/src/pages/error/index.ts b/src/pages/error/index.ts new file mode 100644 index 000000000..3ce6fbb91 --- /dev/null +++ b/src/pages/error/index.ts @@ -0,0 +1 @@ +export { ErrorPage } from './error-container'; diff --git a/src/pages/index.ts b/src/pages/index.ts index 6cc1e6e20..eccda8df8 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1 +1,2 @@ export * from './login'; +export * from './error'; diff --git a/src/store/authentication/api.ts b/src/store/authentication/api.ts index 9e4dd3931..658564100 100644 --- a/src/store/authentication/api.ts +++ b/src/store/authentication/api.ts @@ -23,11 +23,12 @@ export async function fetchCurrentUser(): Promise { try { const response = await get('/api/users/current'); return response.body; - } catch (error) { - console.log(error); + } catch (error: any) { + if (error?.response?.status === 401) { + return null; + } + throw error; } - - return null; } export async function clearSession(): Promise { diff --git a/src/store/authentication/saga.test.ts b/src/store/authentication/saga.test.ts index 7b1cc1661..cdf13ae82 100644 --- a/src/store/authentication/saga.test.ts +++ b/src/store/authentication/saga.test.ts @@ -180,8 +180,8 @@ describe('terminate', () => { }); describe(getCurrentUser, () => { - it('sets the user state', async () => { - const { storeState } = await expectSaga(getCurrentUser) + it('sets the user state and returns success', async () => { + const { storeState, returnValue } = await expectSaga(getCurrentUser) .provide([ stubResponse(call(fetchCurrentUser), { stub: 'user-data' }), ...successResponses(), @@ -192,14 +192,23 @@ describe(getCurrentUser, () => { expect(storeState).toMatchObject({ user: { data: { stub: 'user-data' } }, }); + expect(returnValue).toEqual({ success: true }); }); - it('returns false if fetching the user fails. I.E., the user is not logged in.', async () => { + it('returns unauthenticated error when no user is found', async () => { + const { returnValue } = await expectSaga(getCurrentUser) + .provide([[matchers.call.fn(fetchCurrentUser), null]]) + .run(); + + expect(returnValue).toEqual({ success: false, error: 'unauthenticated' }); + }); + + it('returns critical error if fetching the user fails with an error', async () => { const { returnValue } = await expectSaga(getCurrentUser) .provide([[matchers.call.fn(fetchCurrentUser), throwError(new Error('fetch user error'))]]) .run(); - expect(returnValue).toEqual(false); + expect(returnValue).toEqual({ success: false, error: 'critical' }); }); function successResponses() { diff --git a/src/store/authentication/saga.ts b/src/store/authentication/saga.ts index 73903ad4d..ae634f8bc 100644 --- a/src/store/authentication/saga.ts +++ b/src/store/authentication/saga.ts @@ -66,13 +66,13 @@ export function* getCurrentUser() { try { const user = yield call(fetchCurrentUser); if (!user) { - return false; + return { success: false, error: 'unauthenticated' }; } yield completeUserLogin(user); - return true; + return { success: true }; } catch (e) { - return false; + return { success: false, error: 'critical' }; } } diff --git a/src/store/page-load/saga.test.ts b/src/store/page-load/saga.test.ts index c6e844b72..a4004f138 100644 --- a/src/store/page-load/saga.test.ts +++ b/src/store/page-load/saga.test.ts @@ -24,7 +24,7 @@ describe('page-load saga', () => { function subject(...args: Parameters) { return expectSaga(...args).provide([ [call(getHistory), history], - [call(getCurrentUser), true], + [call(getCurrentUser), { success: true }], [call(getNavigator), stubNavigator()], [spawn(redirectOnUserLogin), null], ]); @@ -62,7 +62,7 @@ describe('page-load saga', () => { storeState: { pageload }, } = await subject(saga) .withReducer(rootReducer, initialState as any) - .provide([[call(getCurrentUser), false]]) + .provide([[call(getCurrentUser), { success: false, error: 'unauthenticated' }]]) .run(); expect(pageload.isComplete).toBe(true); @@ -74,7 +74,7 @@ describe('page-load saga', () => { history = new StubHistory('/'); const { storeState } = await subject(saga) - .provide([[call(getCurrentUser), false]]) + .provide([[call(getCurrentUser), { success: false, error: 'unauthenticated' }]]) .withReducer(rootReducer, initialState as any) .run(); @@ -82,10 +82,23 @@ describe('page-load saga', () => { expect(history.replace).toHaveBeenCalledWith({ pathname: '/login' }); }); + it('redirects to error page on critical error', async () => { + const initialState = { pageload: { isComplete: false } }; + + history = new StubHistory('/'); + const { storeState } = await subject(saga) + .provide([[call(getCurrentUser), { success: false, error: 'critical' }]]) + .withReducer(rootReducer, initialState as any) + .run(); + + expect(storeState.pageload.isComplete).toBe(true); + expect(history.replace).toHaveBeenCalledWith({ pathname: '/error' }); + }); + it('saves the entry path when redirecting to the login page', async () => { history = new StubHistory('/some/path'); const { storeState } = await subject(saga) - .provide([[call(getCurrentUser), false]]) + .provide([[call(getCurrentUser), { success: false, error: 'unauthenticated' }]]) .withReducer(rootReducer) .run(); @@ -110,7 +123,7 @@ describe('page-load saga', () => { history = new StubHistory('/reset-password'); const { storeState: resetPasswordStoreState } = await subject(saga) .withReducer(rootReducer, initialState as any) - .provide([[call(getCurrentUser), false]]) + .provide([[call(getCurrentUser), { success: false, error: 'unauthenticated' }]]) .run(); expect(resetPasswordStoreState.pageload.isComplete).toBe(true); diff --git a/src/store/page-load/saga.ts b/src/store/page-load/saga.ts index 37f615780..f4d7e4483 100644 --- a/src/store/page-load/saga.ts +++ b/src/store/page-load/saga.ts @@ -21,12 +21,14 @@ export function* saga() { return; } - const success = yield call(getCurrentUser); - if (success) { + const result = yield call(getCurrentUser); + if (result.success) { yield handleAuthenticatedUser(history); - } else { + } else if (result.error === 'unauthenticated') { yield handleUnauthenticatedUser(history); yield call(setMobileAppDownloadVisibility, history); + } else { + yield handleCriticalError(history); } yield put(setIsComplete(true)); @@ -52,6 +54,10 @@ function* handleUnauthenticatedUser(history) { yield history.replace({ pathname: '/login' }); } +function* handleCriticalError(history) { + yield history.replace({ pathname: '/error' }); +} + const MOBILE_APP_DOWNLOAD_PATHS = [ '/get-access', '/login', From a5f6bb4f686c878e6712938fa18df73f022c3dec Mon Sep 17 00:00:00 2001 From: RickyRoller Date: Mon, 10 Mar 2025 09:25:02 -0600 Subject: [PATCH 2/3] Clean up error component --- src/pages/error/error-component.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/error/error-component.tsx b/src/pages/error/error-component.tsx index 1fbd590d9..7c9591b79 100644 --- a/src/pages/error/error-component.tsx +++ b/src/pages/error/error-component.tsx @@ -10,10 +10,11 @@ import './error.scss'; const cn = bemClassName('error-page'); interface ErrorComponentProperties { + error: string; onRetry: () => void; } -export const ErrorComponent = ({ onRetry }: ErrorComponentProperties) => { +export const ErrorComponent = ({ error, onRetry }: ErrorComponentProperties) => { return ( <> @@ -23,7 +24,7 @@ export const ErrorComponent = ({ onRetry }: ErrorComponentProperties) => {
-

There was an error loading ZERO, please try again.

+

{error}

From 99ecd2321685d294adc9a323f934e1fb6ac96f90 Mon Sep 17 00:00:00 2001 From: RickyRoller Date: Mon, 10 Mar 2025 09:32:07 -0600 Subject: [PATCH 3/3] Add sentry error tracking --- src/store/authentication/api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/store/authentication/api.ts b/src/store/authentication/api.ts index 658564100..7db9a256d 100644 --- a/src/store/authentication/api.ts +++ b/src/store/authentication/api.ts @@ -1,5 +1,6 @@ import { AuthorizationResponse, User } from './types'; import { del, get, post } from '../../lib/api/rest'; +import * as Sentry from '@sentry/react'; export async function nonceOrAuthorize(signedWeb3Token: string): Promise { const response = await post('/authentication/nonceOrAuthorize').set('Authorization', `Web3 ${signedWeb3Token}`); @@ -27,6 +28,7 @@ export async function fetchCurrentUser(): Promise { if (error?.response?.status === 401) { return null; } + Sentry.captureException(error); throw error; } }