Skip to content

Commit

Permalink
Add error page and and re-direct user fetch errors
Browse files Browse the repository at this point in the history
  • Loading branch information
RickyRoller committed Mar 10, 2025
1 parent 5032af6 commit 3c88479
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 20 deletions.
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,6 +50,7 @@ root.render(
<Route path='/get-access' exact component={Invite} />
<Route path='/login' exact component={LoginPage} />
<Route path='/reset-password' exact component={ResetPassword} />
<Route path='/error' exact component={ErrorPage} />
<Route component={App} />
</Switch>
</RainbowKitConnect>
Expand Down
33 changes: 33 additions & 0 deletions src/pages/error/error-component.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<ThemeEngine theme={Themes.Dark} />
<div {...cn('')}>
<main {...cn('content')}>
<div {...cn('logo-container')}>
<ZeroLogo />
</div>
<div {...cn('message-container')}>
<h3 {...cn('message')}>There was an error loading ZERO, please try again.</h3>
<Button onPress={onRetry}>Try Again</Button>
</div>
</main>
</div>
</>
);
};
11 changes: 11 additions & 0 deletions src/pages/error/error-container.tsx
Original file line number Diff line number Diff line change
@@ -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 <ErrorComponent onRetry={handleRetry} error={error} />;
};
54 changes: 54 additions & 0 deletions src/pages/error/error.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/pages/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ErrorPage } from './error-container';
1 change: 1 addition & 0 deletions src/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './login';
export * from './error';
9 changes: 5 additions & 4 deletions src/store/authentication/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export async function fetchCurrentUser(): Promise<User> {
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<AuthorizationResponse> {
Expand Down
17 changes: 13 additions & 4 deletions src/store/authentication/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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() {
Expand Down
6 changes: 3 additions & 3 deletions src/store/authentication/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
}
}

Expand Down
23 changes: 18 additions & 5 deletions src/store/page-load/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('page-load saga', () => {
function subject(...args: Parameters<typeof expectSaga>) {
return expectSaga(...args).provide([
[call(getHistory), history],
[call(getCurrentUser), true],
[call(getCurrentUser), { success: true }],
[call(getNavigator), stubNavigator()],
[spawn(redirectOnUserLogin), null],
]);
Expand Down Expand Up @@ -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);
Expand All @@ -74,18 +74,31 @@ 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();

expect(storeState.pageload.isComplete).toBe(true);
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();

Expand All @@ -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);
Expand Down
12 changes: 9 additions & 3 deletions src/store/page-load/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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',
Expand Down

0 comments on commit 3c88479

Please sign in to comment.