Merge pull request #7823 from Tainan404/join-HOC

Implement HOC to handle errors
This commit is contained in:
Anton Georgiev 2019-08-16 16:06:30 -04:00 committed by GitHub
commit b5c4209c50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 182 additions and 83 deletions

View File

@ -7,6 +7,7 @@ import Base from '/imports/startup/client/base';
import JoinHandler from '/imports/ui/components/join-handler/component'; import JoinHandler from '/imports/ui/components/join-handler/component';
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component'; import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';
import Subscriptions from '/imports/ui/components/subscriptions/component'; import Subscriptions from '/imports/ui/components/subscriptions/component';
import JoinLoading from '/imports/ui/components/join-loading/component';
Meteor.startup(() => { Meteor.startup(() => {
// Logs all uncaught exceptions to the client logger // Logs all uncaught exceptions to the client logger
@ -33,13 +34,15 @@ Meteor.startup(() => {
// TODO make this a Promise // TODO make this a Promise
render( render(
<JoinHandler> <JoinLoading>
<AuthenticatedHandler> <JoinHandler>
<Subscriptions> <AuthenticatedHandler>
<Base /> <Subscriptions>
</Subscriptions> <Base />
</AuthenticatedHandler> </Subscriptions>
</JoinHandler>, </AuthenticatedHandler>
</JoinHandler>
</JoinLoading>,
document.getElementById('app'), document.getElementById('app'),
); );
}); });

View File

@ -3,9 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import AppContainer from '/imports/ui/components/app/container'; import AppContainer from '/imports/ui/components/app/container';
import ErrorScreen from '/imports/ui/components/error-screen/component';
import MeetingEnded from '/imports/ui/components/meeting-ended/component'; import MeetingEnded from '/imports/ui/components/meeting-ended/component';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import AudioManager from '/imports/ui/services/audio-manager'; import AudioManager from '/imports/ui/services/audio-manager';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
@ -18,6 +16,7 @@ import Breakouts from '/imports/api/breakouts';
import AudioService from '/imports/ui/components/audio/service'; import AudioService from '/imports/ui/components/audio/service';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { notify } from '/imports/ui/services/notification'; import { notify } from '/imports/ui/services/notification';
import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context';
const HTML = document.getElementsByTagName('html')[0]; const HTML = document.getElementsByTagName('html')[0];
@ -87,12 +86,29 @@ class Base extends Component {
ejected, ejected,
isMeteorConnected, isMeteorConnected,
subscriptionsReady, subscriptionsReady,
codeError,
meetingHasEnded,
dispatch,
} = this.props; } = this.props;
const { const {
loading, loading,
meetingExisted, meetingExisted,
} = this.state; } = this.state;
if (codeError && !meetingHasEnded) {
// 680 is set for the codeError when the user requests a logout
if (codeError !== '680') {
logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${codeError}`);
}
Session.set('codeError', codeError);
return dispatch('hasError');
}
if (!(loading || !subscriptionsReady)
&& !meetingHasEnded && meetingExist) {
dispatch('hideLoading');
}
if (!prevProps.subscriptionsReady && subscriptionsReady) { if (!prevProps.subscriptionsReady && subscriptionsReady) {
logger.info({ logCode: 'startup_client_subscriptions_ready' }, 'Subscriptions are ready'); logger.info({ logCode: 'startup_client_subscriptions_ready' }, 'Subscriptions are ready');
} }
@ -152,21 +168,14 @@ class Base extends Component {
renderByState() { renderByState() {
const { updateLoadingState } = this; const { updateLoadingState } = this;
const stateControls = { updateLoadingState }; const stateControls = { updateLoadingState };
const { loading } = this.state;
const codeError = Session.get('codeError'); const codeError = Session.get('codeError');
const { const {
ejected, ejected,
meetingExist,
meetingHasEnded, meetingHasEnded,
meetingIsBreakout, meetingIsBreakout,
subscriptionsReady,
User, User,
} = this.props; } = this.props;
if ((loading || !subscriptionsReady) && !meetingHasEnded && meetingExist) {
return (<LoadingScreen>{loading}</LoadingScreen>);
}
if (ejected && ejected.ejectedReason) { if (ejected && ejected.ejectedReason) {
const { ejectedReason } = ejected; const { ejectedReason } = ejected;
AudioManager.exitAudio(); AudioManager.exitAudio();
@ -179,14 +188,6 @@ class Base extends Component {
AudioManager.exitAudio(); AudioManager.exitAudio();
return (<MeetingEnded code={codeError} />); return (<MeetingEnded code={codeError} />);
} }
if (codeError && !meetingHasEnded) {
// 680 is set for the codeError when the user requests a logout
if (codeError !== '680') {
logger.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${codeError}`);
}
return (<ErrorScreen code={codeError} />);
}
// this.props.annotationsHandler.stop(); // this.props.annotationsHandler.stop();
return (<AppContainer {...this.props} baseControls={stateControls} />); return (<AppContainer {...this.props} baseControls={stateControls} />);
} }
@ -196,15 +197,11 @@ class Base extends Component {
const { locale, meetingExist } = this.props; const { locale, meetingExist } = this.props;
const stateControls = { updateLoadingState }; const stateControls = { updateLoadingState };
const { meetingExisted } = this.state; const { meetingExisted } = this.state;
if ((!meetingExisted && !meetingExist && Auth.loggedIn)) return null;
return ( return (
(!meetingExisted && !meetingExist && Auth.loggedIn) <IntlStartup locale={locale} baseControls={stateControls}>
? <LoadingScreen /> {this.renderByState()}
: ( </IntlStartup>
<IntlStartup locale={locale} baseControls={stateControls}>
{this.renderByState()}
</IntlStartup>
)
); );
} }
} }
@ -316,4 +313,4 @@ const BaseContainer = withTracker(() => {
}; };
})(Base); })(Base);
export default BaseContainer; export default withJoinLoadingConsumer(BaseContainer);

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import LoadingScreen from '/imports/ui/components/loading-screen/component'; import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context';
// currently supported locales. // currently supported locales.
import ar from 'react-intl/locale-data/ar'; import ar from 'react-intl/locale-data/ar';
@ -106,7 +106,7 @@ class IntlStartup extends Component {
fetchLocalizedMessages(locale) { fetchLocalizedMessages(locale) {
const url = `/html5client/locale?locale=${locale}`; const url = `/html5client/locale?locale=${locale}`;
const { dispatch } = this.props;
this.setState({ fetching: true }, () => { this.setState({ fetching: true }, () => {
fetch(url) fetch(url)
.then((response) => { .then((response) => {
@ -120,6 +120,7 @@ class IntlStartup extends Component {
const dasherizedLocale = normalizedLocale.replace('_', '-'); const dasherizedLocale = normalizedLocale.replace('_', '-');
this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => { this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
this.saveLocale(dasherizedLocale); this.saveLocale(dasherizedLocale);
dispatch('hideLoading');
}); });
}) })
.catch(() => { .catch(() => {
@ -146,7 +147,7 @@ class IntlStartup extends Component {
const { fetching, normalizedLocale, messages } = this.state; const { fetching, normalizedLocale, messages } = this.state;
const { children } = this.props; const { children } = this.props;
return fetching ? <LoadingScreen /> : ( return !fetching && (
<IntlProvider locale={normalizedLocale} messages={messages}> <IntlProvider locale={normalizedLocale} messages={messages}>
{children} {children}
</IntlProvider> </IntlProvider>
@ -154,7 +155,7 @@ class IntlStartup extends Component {
} }
} }
export default IntlStartup; export default withJoinLoadingConsumer(IntlStartup);
IntlStartup.propTypes = propTypes; IntlStartup.propTypes = propTypes;
IntlStartup.defaultProps = defaultProps; IntlStartup.defaultProps = defaultProps;

View File

@ -2,18 +2,13 @@ import React, { Component } from 'react';
import { Session } from 'meteor/session'; import { Session } from 'meteor/session';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import LoadingScreen from '/imports/ui/components/loading-screen/component'; import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context';
const STATUS_CONNECTING = 'connecting'; const STATUS_CONNECTING = 'connecting';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
class AuthenticatedHandler extends Component { class AuthenticatedHandler extends Component {
static setError(codeError) {
Session.set('hasError', true);
if (codeError) Session.set('codeError', codeError);
}
static shouldAuthenticate(status, lastStatus) { static shouldAuthenticate(status, lastStatus) {
return lastStatus != null && lastStatus === STATUS_CONNECTING && status.connected; return lastStatus != null && lastStatus === STATUS_CONNECTING && status.connected;
} }
@ -35,7 +30,32 @@ class AuthenticatedHandler extends Component {
}); });
} }
static async authenticatedRouteHandler(callback) { constructor(props) {
super(props);
this.setError = this.setError.bind(this);
this.authenticatedRouteHandler = this.authenticatedRouteHandler.bind(this);
this.state = {
authenticated: false,
};
}
componentDidMount() {
if (Session.get('codeError')) {
return this.setError(Session.get('codeError'));
}
return this.authenticatedRouteHandler((value, error) => {
if (error) return this.setError(error);
return this.setState({ authenticated: true });
});
}
setError(codeError) {
const { dispatch } = this.props;
dispatch('hasError');
if (codeError) Session.set('codeError', codeError);
}
async authenticatedRouteHandler(callback) {
if (Auth.loggedIn) { if (Auth.loggedIn) {
callback(); callback();
} }
@ -43,6 +63,7 @@ class AuthenticatedHandler extends Component {
AuthenticatedHandler.addReconnectObservable(); AuthenticatedHandler.addReconnectObservable();
const setReason = (reason) => { const setReason = (reason) => {
this.setError(reason.error);
logger.error({ logger.error({
logCode: 'authenticatedhandlercomponent_setreason', logCode: 'authenticatedhandlercomponent_setreason',
extraInfo: { reason }, extraInfo: { reason },
@ -60,21 +81,6 @@ class AuthenticatedHandler extends Component {
} }
} }
constructor(props) {
super(props);
this.state = {
authenticated: false,
};
}
componentDidMount() {
if (Session.get('codeError')) return this.changeState(true);
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {
if (error) AuthenticatedHandler.setError(error);
this.setState({ authenticated: true });
});
}
render() { render() {
const { const {
children, children,
@ -89,11 +95,9 @@ class AuthenticatedHandler extends Component {
Session.set('isPollOpen', false); Session.set('isPollOpen', false);
Session.set('breakoutRoomIsOpen', false); Session.set('breakoutRoomIsOpen', false);
return authenticated return authenticated && children;
? children
: (<LoadingScreen />);
} }
} }
export default AuthenticatedHandler; export default withJoinLoadingConsumer(AuthenticatedHandler);

View File

@ -57,16 +57,16 @@ class ErrorScreen extends React.PureComponent {
children, children,
} = this.props; } = this.props;
const codeError = Session.get('codeError') || code;
let formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]); let formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]);
if (code in intlMessages) { if (codeError in intlMessages) {
formatedMessage = intl.formatMessage(intlMessages[code]); formatedMessage = intl.formatMessage(intlMessages[codeError]);
} }
return ( return (
<div className={styles.background}> <div className={styles.background}>
<h1 className={styles.codeError}> <h1 className={styles.codeError}>
{code} {codeError}
</h1> </h1>
<h1 className={styles.message}> <h1 className={styles.message}>
{formatedMessage} {formatedMessage}

View File

@ -6,7 +6,7 @@ import { setCustomLogoUrl } from '/imports/ui/components/user-list/service';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import deviceInfo from '/imports/utils/deviceInfo'; import deviceInfo from '/imports/utils/deviceInfo';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import LoadingScreen from '/imports/ui/components/loading-screen/component'; import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context';
const propTypes = { const propTypes = {
children: PropTypes.element.isRequired, children: PropTypes.element.isRequired,
@ -17,14 +17,10 @@ const { showParticipantsOnLogin } = APP_CONFIG;
const CHAT_ENABLED = Meteor.settings.public.chat.enabled; const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
class JoinHandler extends Component { class JoinHandler extends Component {
static setError(codeError) {
Session.set('hasError', true);
if (codeError) Session.set('codeError', codeError);
}
constructor(props) { constructor(props) {
super(props); super(props);
this.fetchToken = this.fetchToken.bind(this); this.fetchToken = this.fetchToken.bind(this);
this.setError = this.setError.bind(this);
this.numFetchTokenRetries = 0; this.numFetchTokenRetries = 0;
this.state = { this.state = {
@ -41,6 +37,12 @@ class JoinHandler extends Component {
this._isMounted = false; this._isMounted = false;
} }
setError(codeError) {
const { dispatch } = this.props;
if (codeError) Session.set('codeError', codeError);
dispatch('hasError');
}
async fetchToken() { async fetchToken() {
if (!this._isMounted) return; if (!this._isMounted) return;
@ -63,7 +65,7 @@ class JoinHandler extends Component {
const sessionToken = urlParams.get('sessionToken'); const sessionToken = urlParams.get('sessionToken');
if (!sessionToken) { if (!sessionToken) {
JoinHandler.setError('400'); this.setError('400');
Session.set('errorMessageDescription', 'Session token was not provided'); Session.set('errorMessageDescription', 'Session token was not provided');
} }
@ -141,7 +143,6 @@ class JoinHandler extends Component {
const { response } = parseToJson; const { response } = parseToJson;
setLogoutURL(response); setLogoutURL(response);
if (response.returncode !== 'FAILED') { if (response.returncode !== 'FAILED') {
await setAuth(response); await setAuth(response);
@ -167,9 +168,11 @@ class JoinHandler extends Component {
response, response,
}, },
}, 'User successfully went through main.joinRouteHandler'); }, 'User successfully went through main.joinRouteHandler');
this.setState({ joined: true });
} else { } else {
const e = new Error(response.message); const e = new Error(response.message);
if (!Session.get('codeError')) Session.set('errorMessageDescription', response.message); if (!Session.get('codeError')) Session.set('errorMessageDescription', response.message);
this.setError(401);
logger.error({ logger.error({
logCode: 'joinhandler_component_joinroutehandler_error', logCode: 'joinhandler_component_joinroutehandler_error',
extraInfo: { extraInfo: {
@ -178,18 +181,15 @@ class JoinHandler extends Component {
}, },
}, 'User faced an error on main.joinRouteHandler.'); }, 'User faced an error on main.joinRouteHandler.');
} }
this.setState({ joined: true });
} }
render() { render() {
const { children } = this.props; const { children } = this.props;
const { joined } = this.state; const { joined } = this.state;
return joined return joined && children;
? children
: (<LoadingScreen />);
} }
} }
export default JoinHandler; export default withJoinLoadingConsumer(JoinHandler);
JoinHandler.propTypes = propTypes; JoinHandler.propTypes = propTypes;

View File

@ -0,0 +1,23 @@
import React, { Fragment } from 'react';
import LoadingScreen from '/imports/ui/components/loading-screen/component';
import ErrorScreen from '/imports/ui/components/error-screen/component';
import { withJoinLoadingContext } from './context/context';
import IntlProvider from '/imports/startup/client/intl';
import Settings from '/imports/ui/services/settings';
const JoinLoadComponent = (props) => {
const { children, showLoading, hasError } = props;
const { locale } = Settings.application;
return (
<Fragment>
{(showLoading && !hasError) && (<LoadingScreen />)}
{hasError ? (
<IntlProvider locale={locale}>
<ErrorScreen />
</IntlProvider>
) : children}
</Fragment>
);
};
export default withJoinLoadingContext(JoinLoadComponent);

View File

@ -0,0 +1,64 @@
import React, { useReducer, createContext } from 'react';
function reducer(state, action) {
switch (action) {
case 'showLoading':
return {
...state,
showLoading: true,
};
case 'hideLoading':
return {
...state,
showLoading: false,
};
case 'hasError':
return {
...state,
hasError: true,
};
default:
throw new Error();
}
}
const JoinLoadingContext = createContext({});
const initialState = {
showLoading: true,
hasError: false,
};
const JoinLoading = Component => (props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const contextValue = {
...props,
...state,
dispatch,
};
return (
<JoinLoadingContext.Provider value={contextValue}>
<Component />
</JoinLoadingContext.Provider>
);
};
const joinLoadingContextConsumer = Component => props => (
<JoinLoadingContext.Consumer>
{ contexts => <Component {...contexts} {...props} />}
</JoinLoadingContext.Consumer>
);
const withJoinLoadingConsumer = Component => joinLoadingContextConsumer(Component);
const withJoinLoadingContext = Component => JoinLoading(withJoinLoadingConsumer(Component));
export {
withJoinLoadingContext,
withJoinLoadingConsumer,
joinLoadingContextConsumer,
JoinLoading,
reducer,
initialState,
JoinLoadingContext,
};

View File

@ -11,13 +11,18 @@
} }
.background { .background {
position: fixed;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
justify-content: center; justify-content: center;
position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--loader-bg); background-color: var(--loader-bg);
z-index: 99999;
} }
.message { .message {

View File

@ -8,6 +8,7 @@ import Annotations from '/imports/api/annotations';
import AnnotationsTextService from '/imports/ui/components/whiteboard/annotations/text/service'; import AnnotationsTextService from '/imports/ui/components/whiteboard/annotations/text/service';
import AnnotationsLocal from '/imports/ui/components/whiteboard/service'; import AnnotationsLocal from '/imports/ui/components/whiteboard/service';
import mapUser from '/imports/ui/services/user/mapUser'; import mapUser from '/imports/ui/services/user/mapUser';
import { withJoinLoadingConsumer } from '/imports/ui/components/join-loading/context/context';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_ENABLED = CHAT_CONFIG.enabled; const CHAT_ENABLED = CHAT_CONFIG.enabled;
@ -34,7 +35,7 @@ class Subscriptions extends Component {
} }
} }
export default withTracker(() => { export default withJoinLoadingConsumer(withTracker(({ dispatch }) => {
const { credentials } = Auth; const { credentials } = Auth;
const { meetingId, requesterUserId } = credentials; const { meetingId, requesterUserId } = credentials;
if (Session.get('codeError')) { if (Session.get('codeError')) {
@ -50,6 +51,7 @@ export default withTracker(() => {
extraInfo: { error }, extraInfo: { error },
}, 'Error while subscribing to collections'); }, 'Error while subscribing to collections');
Session.set('codeError', error.error); Session.set('codeError', error.error);
dispatch('hasError');
}, },
}; };
@ -115,4 +117,4 @@ export default withTracker(() => {
subscriptionsReady: ready, subscriptionsReady: ready,
subscriptionsHandlers, subscriptionsHandlers,
}; };
})(Subscriptions); })(Subscriptions));