Merge pull request #3597 from virtuacoplenny/lenny/handle-calendar-signed-out

fix(calendar): show error message if authorization fails on event fetch
This commit is contained in:
virtuacoplenny 2018-12-10 17:51:08 -08:00 committed by GitHub
commit f89f3f144f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 145 additions and 8 deletions

View File

@ -29,13 +29,19 @@
background: #0074E0;
border-radius: 4px;
color: #FFFFFF;
display: flex;
display: inline-block;
justify-content: center;
align-items: center;
padding: 5px 10px;
cursor: pointer;
}
.calendar-action-buttons {
.button {
margin: 0px 10px;
}
}
.item {
background: rgba(255,255,255,0.50);
box-sizing: border-box;

View File

@ -662,6 +662,11 @@
"addMeetingURL": "Add a meeting link",
"confirmAddLink": "Do you want to add a Jitsi link to this event?",
"confirmAddLinkTitle": "Calendar",
"error": {
"appConfiguration": "Calendar integration is not properly configured.",
"generic": "An error has occurred. Please check your calendar settings or try refreshing the calendar.",
"notSignedIn": "An error occurred while authenticating to see calendar events. Please check your calendar settings and try logging in again."
},
"join": "Join",
"joinTooltip": "Join the meeting",
"nextMeeting": "next meeting",

View File

@ -33,6 +33,17 @@ export const REFRESH_CALENDAR = Symbol('REFRESH_CALENDAR');
*/
export const SET_CALENDAR_AUTHORIZATION = Symbol('SET_CALENDAR_AUTHORIZATION');
/**
* Action to update the last error that occurred while trying to authenticate
* with or fetch data from the calendar integration.
*
* {
* type: SET_CALENDAR_ERROR,
* error: ?Object
* }
*/
export const SET_CALENDAR_ERROR = Symbol('SET_CALENDAR_ERROR');
/**
* Action to update the current calendar entry list in the store.
*

View File

@ -8,6 +8,7 @@ import { createCalendarConnectedEvent, sendAnalytics } from '../analytics';
import {
CLEAR_CALENDAR_INTEGRATION,
SET_CALENDAR_AUTH_STATE,
SET_CALENDAR_ERROR,
SET_CALENDAR_INTEGRATION,
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
@ -119,6 +120,22 @@ export function setCalendarAPIAuthState(newState: ?Object) {
};
}
/**
* Sends an action to update the calendar error state in redux.
*
* @param {Object} error - An object with error details.
* @returns {{
* type: SET_CALENDAR_ERROR,
* error: Object
* }}
*/
export function setCalendarError(error: ?Object) {
return {
type: SET_CALENDAR_ERROR,
error
};
}
/**
* Sends an action to update the current calendar profile email state in redux.
*

View File

@ -13,6 +13,7 @@ import {
} from '../../analytics';
import { refreshCalendar } from '../actions';
import { ERRORS } from '../constants';
import { isCalendarEnabled } from '../functions';
import CalendarListContent from './CalendarListContent';
@ -24,6 +25,12 @@ declare var interfaceConfig: Object;
*/
type Props = {
/**
* The error object containing details about any error that has occurred
* while interacting with calendar integration.
*/
_calendarError: ?Object,
/**
* Whether or not a calendar may be connected for fetching calendar events.
*/
@ -87,6 +94,54 @@ class CalendarList extends AbstractPage<Props> {
);
}
/**
* Returns a component for showing the error message related to calendar
* sync.
*
* @private
* @returns {React$Component}
*/
_getErrorMessage() {
const { _calendarError = {}, t } = this.props;
let errorMessageKey = 'calendarSync.error.generic';
let showRefreshButton = true;
let showSettingsButton = true;
if (_calendarError.error === ERRORS.GOOGLE_APP_MISCONFIGURED) {
errorMessageKey = 'calendarSync.error.appConfiguration';
showRefreshButton = false;
showSettingsButton = false;
} else if (_calendarError.error === ERRORS.AUTH_FAILED) {
errorMessageKey = 'calendarSync.error.notSignedIn';
showRefreshButton = false;
}
return (
<div className = 'meetings-list-empty'>
<p className = 'description'>
{ t(errorMessageKey) }
</p>
<div className = 'calendar-action-buttons'>
{ showSettingsButton
&& <div
className = 'button'
onClick = { this._onOpenSettings }>
{ t('calendarSync.permissionButton') }
</div>
}
{ showRefreshButton
&& <div
className = 'button'
onClick = { this._onRefreshEvents }>
{ t('calendarSync.refresh') }
</div>
}
</div>
</div>
);
}
_getRenderListEmptyComponent: () => Object;
/**
@ -97,12 +152,21 @@ class CalendarList extends AbstractPage<Props> {
* @returns {React$Component}
*/
_getRenderListEmptyComponent() {
const { _hasIntegrationSelected, _hasLoadedEvents, t } = this.props;
const {
_calendarError,
_hasIntegrationSelected,
_hasLoadedEvents,
t
} = this.props;
if (_hasIntegrationSelected && _hasLoadedEvents) {
if (_calendarError) {
return this._getErrorMessage();
} else if (_hasIntegrationSelected && _hasLoadedEvents) {
return (
<div className = 'meetings-list-empty'>
<div>{ t('calendarSync.noEvents') }</div>
<p className = 'description'>
{ t('calendarSync.noEvents') }
</p>
<div
className = 'button'
onClick = { this._onRefreshEvents }>
@ -172,18 +236,21 @@ class CalendarList extends AbstractPage<Props> {
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _calendarError: Object,
* _hasIntegrationSelected: boolean,
* _hasLoadedEvents: boolean
* }}
*/
function _mapStateToProps(state) {
const {
error,
events,
integrationType,
isLoadingEvents
} = state['features/calendar-sync'];
return {
_calendarError: error,
_hasIntegrationSelected: Boolean(integrationType),
_hasLoadedEvents: Boolean(events) || !isLoadingEvents
};

View File

@ -10,6 +10,17 @@ export const CALENDAR_TYPE = {
MICROSOFT: 'microsoft'
};
/**
* An enumeration of known errors that can occur while interacting with the
* calendar integration.
*
* @enum {string}
*/
export const ERRORS = {
AUTH_FAILED: 'sign_in_failed',
GOOGLE_APP_MISCONFIGURED: 'idpiframe_initialization_failed'
};
/**
* The number of days to fetch.
*/

View File

@ -1,10 +1,15 @@
// @flow
import { setLoadingCalendarEvents } from './actions';
import {
clearCalendarIntegration,
setCalendarError,
setLoadingCalendarEvents
} from './actions';
export * from './functions.any';
import {
CALENDAR_TYPE,
ERRORS,
FETCH_END_DAYS,
FETCH_START_DAYS
} from './constants';
@ -66,7 +71,9 @@ export function _fetchCalendarEntries(
return Promise.resolve();
}
return Promise.reject('Not authorized, please sign in!');
return Promise.reject({
error: ERRORS.AUTH_FAILED
});
})
.then(() => dispatch(integration.getCalendarEntries(
FETCH_START_DAYS, FETCH_END_DAYS)))
@ -74,8 +81,17 @@ export function _fetchCalendarEntries(
dispatch,
getState
}, events))
.catch(error =>
logger.error('Error fetching calendar.', error))
.then(() => {
dispatch(setCalendarError());
}, error => {
logger.error('Error fetching calendar.', error);
if (error.error === ERRORS.AUTH_FAILED) {
dispatch(clearCalendarIntegration());
}
dispatch(setCalendarError(error));
})
.then(() => dispatch(setLoadingCalendarEvents(false)));
}

View File

@ -8,6 +8,7 @@ import {
CLEAR_CALENDAR_INTEGRATION,
SET_CALENDAR_AUTH_STATE,
SET_CALENDAR_AUTHORIZATION,
SET_CALENDAR_ERROR,
SET_CALENDAR_EVENTS,
SET_CALENDAR_INTEGRATION,
SET_CALENDAR_PROFILE_EMAIL,
@ -86,6 +87,9 @@ isCalendarEnabled()
case SET_CALENDAR_AUTHORIZATION:
return set(state, 'authorization', action.authorization);
case SET_CALENDAR_ERROR:
return set(state, 'error', action.error);
case SET_CALENDAR_EVENTS:
return set(state, 'events', action.events);