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..7c9591b79
--- /dev/null
+++ b/src/pages/error/error-component.tsx
@@ -0,0 +1,34 @@
+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 {
+ error: string;
+ onRetry: () => void;
+}
+
+export const ErrorComponent = ({ error, onRetry }: ErrorComponentProperties) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
{error}
+
+
+
+
+ >
+ );
+};
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..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}`);
@@ -23,11 +24,13 @@ 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;
+ }
+ Sentry.captureException(error);
+ 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',