diff --git a/client/src/components/layouts/default.tsx b/client/src/components/layouts/default.tsx index a135a4f5bb9..b9da69a81fc 100644 --- a/client/src/components/layouts/default.tsx +++ b/client/src/components/layouts/default.tsx @@ -181,7 +181,7 @@ function DefaultLayout({ const isJapanese = clientLocale === 'japanese'; - if (fetchState.pending) { + if (!fetchState.complete) { return ; } else { return ( diff --git a/client/src/redux/action-types.js b/client/src/redux/action-types.js index 0eb33759224..d242e461e26 100644 --- a/client/src/redux/action-types.js +++ b/client/src/redux/action-types.js @@ -46,7 +46,8 @@ export const actionTypes = createTypes( ...createAsyncTypes('showCert'), ...createAsyncTypes('reportUser'), ...createAsyncTypes('deleteUserToken'), - ...createAsyncTypes('saveChallenge') + ...createAsyncTypes('saveChallenge'), + 'fetchUserTimeout' ], ns ); diff --git a/client/src/redux/actions.ts b/client/src/redux/actions.ts index ebc23b054e9..a6d2ed2f9ef 100644 --- a/client/src/redux/actions.ts +++ b/client/src/redux/actions.ts @@ -50,6 +50,7 @@ export const acceptTermsError = createAction(actionTypes.acceptTermsError); export const fetchUser = createAction(actionTypes.fetchUser); export const fetchUserComplete = createAction(actionTypes.fetchUserComplete); +export const fetchUserTimeout = createAction(actionTypes.fetchUserTimeout); export const fetchUserError = createAction(actionTypes.fetchUserError); export const toggleTheme = createAction(actionTypes.toggleTheme); diff --git a/client/src/redux/fetch-user-saga.js b/client/src/redux/fetch-user-saga.js index 6a2d8835be6..d093dda4d66 100644 --- a/client/src/redux/fetch-user-saga.js +++ b/client/src/redux/fetch-user-saga.js @@ -1,20 +1,41 @@ -import { call, put, takeEvery } from 'redux-saga/effects'; +import { call, cancel, delay, fork, put, takeEvery } from 'redux-saga/effects'; import { getSessionUser, getUserProfile } from '../utils/ajax'; import { fetchProfileForUserComplete, fetchProfileForUserError, fetchUserComplete, - fetchUserError + fetchUserError, + fetchUserTimeout } from './actions'; function* fetchSessionUser() { - try { - const { data: user } = yield call(getSessionUser); + const timeoutTask = yield fork(function* () { + // The server should not take anywhere near 2 seconds to respond. If it + // does, we assume the request has failed and dispatch a timeout action to + // dismiss the loading state. + yield delay(2000); + yield put(fetchUserTimeout()); + }); + try { + // This is on a longer timeout to make sure that users with slow connections + // do, eventually, get signed in. + const res = yield call(getSessionUser, AbortSignal.timeout(10000)); + + const isSignedOut = res.response.status === 401; + if (!res.response.ok && !isSignedOut) { + throw new Error( + `HTTP Error: ${res.response.status} ${res.response.statusText}` + ); + } + + const { data: user } = res; yield put(fetchUserComplete({ user })); } catch (e) { console.log('failed to fetch user', e); yield put(fetchUserError(e)); + } finally { + yield cancel(timeoutTask); } } diff --git a/client/src/redux/index.js b/client/src/redux/index.js index fc8b939f6d8..2335cd15dfe 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -218,11 +218,22 @@ export const reducer = handleActions( error: null } }), + [actionTypes.fetchUserTimeout]: state => ({ + ...state, + userFetchState: { + // Pending because the fetch may still complete. This allows the UI to + // render what it can while waiting for the fetch to complete. + pending: true, + complete: true, + errored: false, + error: null + } + }), [actionTypes.fetchUserError]: (state, { payload }) => ({ ...state, userFetchState: { pending: false, - complete: false, + complete: true, errored: true, error: payload } diff --git a/client/src/utils/ajax.ts b/client/src/utils/ajax.ts index 06023558227..bb045e62059 100644 --- a/client/src/utils/ajax.ts +++ b/client/src/utils/ajax.ts @@ -37,10 +37,14 @@ export interface ResponseWithData { // TODO: Might want to handle flash messages as close to the request as possible // to make use of the Response object (message, status, etc) -async function get(path: string): Promise> { +async function get( + path: string, + signal?: AbortSignal +): Promise> { const response = await fetch(`${base}${path}`, { ...defaultOptions, - headers: { 'CSRF-Token': getCSRFToken() } + headers: { 'CSRF-Token': getCSRFToken() }, + signal }); return combineDataWithResponse(response); @@ -144,9 +148,12 @@ function mapKeyToFileKey( return files.map(({ key, ...rest }) => ({ ...rest, fileKey: key })); } -export function getSessionUser(): Promise> { +export function getSessionUser( + signal?: AbortSignal +): Promise> { const responseWithData: Promise> = get( - '/user/get-session-user' + '/user/get-session-user', + signal ); // TODO: Once DB is migrated, no longer need to parse `files` -> `challengeFiles` etc. return responseWithData.then(({ response, data }) => {