From 9837b85dce1dfd296c277ccf78dde84f8f94a8ac Mon Sep 17 00:00:00 2001 From: Adam Butterworth Date: Mon, 13 May 2019 09:49:11 -0600 Subject: [PATCH] Fix auth disconnection add error states (#30) * fix: properly disconnect auth * fix: add error handling and states --- src/account-settings/actions.js | 19 ++++++ .../components/ThirdPartyAuth.jsx | 67 +++++++++++++++---- src/account-settings/reducers.js | 29 ++++++++ src/account-settings/sagas.js | 19 ++++++ src/account-settings/selectors.js | 5 +- src/account-settings/service.js | 5 ++ 6 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/account-settings/actions.js b/src/account-settings/actions.js index 6f866eb..3a984e5 100644 --- a/src/account-settings/actions.js +++ b/src/account-settings/actions.js @@ -11,6 +11,7 @@ export const OPEN_FORM = 'OPEN_FORM'; export const CLOSE_FORM = 'CLOSE_FORM'; export const UPDATE_DRAFT = 'UPDATE_DRAFT'; export const RESET_DRAFTS = 'RESET_DRAFTS'; +export const DISCONNECT_AUTH = new AsyncActionType('ACCOUNT_SETTINGS', 'DISCONNECT_AUTH'); // FETCH SETTINGS ACTIONS @@ -135,3 +136,21 @@ export const fetchTimeZonesSuccess = timeZones => ({ payload: { timeZones }, }); + +// DISCONNECT AUTH ACTIONS + +export const disconnectAuth = (url, providerId) => ({ + type: DISCONNECT_AUTH.BASE, payload: { url, providerId }, +}); +export const disconnectAuthBegin = () => ({ + type: DISCONNECT_AUTH.BEGIN, +}); +export const disconnectAuthSuccess = () => ({ + type: DISCONNECT_AUTH.SUCCESS, +}); +export const disconnectAuthFailure = providerId => ({ + type: DISCONNECT_AUTH.FAILURE, payload: { providerId }, +}); +export const disconnectAuthReset = () => ({ + type: DISCONNECT_AUTH.RESET, +}); diff --git a/src/account-settings/components/ThirdPartyAuth.jsx b/src/account-settings/components/ThirdPartyAuth.jsx index 42ea7b6..761a421 100644 --- a/src/account-settings/components/ThirdPartyAuth.jsx +++ b/src/account-settings/components/ThirdPartyAuth.jsx @@ -2,12 +2,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import { Hyperlink } from '@edx/paragon'; +import { Hyperlink, StatefulButton } from '@edx/paragon'; +import Alert from './Alert'; +import { disconnectAuth } from '../actions'; import { thirdPartyAuthSelector } from '../selectors'; class ThirdPartyAuth extends React.Component { - renderConnectedProvider(url, name) { + onClickDisconnect = (e) => { + e.preventDefault(); + if (this.props.disconnectingState === 'pending') return; + const providerId = e.currentTarget.getAttribute('data-provider-id'); + const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url'); + this.props.disconnectAuth(disconnectUrl, providerId); + } + + renderUnconnectedProvider(url, name) { return (
{name}
@@ -23,7 +33,9 @@ class ThirdPartyAuth extends React.Component { ); } - renderUnconnectedProvider(url, name) { + renderConnectedProvider(url, name, id) { + const hasError = this.props.disconnectErrors[id]; + return (
@@ -36,14 +48,34 @@ class ThirdPartyAuth extends React.Component { />
- - - + {hasError ? ( + + + + ) : null} + + + ), + }} + onClick={this.onClickDisconnect} + disabledStates={[]} + data-disconnect-url={url} + data-provider-id={id} + />
); } @@ -55,8 +87,8 @@ class ThirdPartyAuth extends React.Component {
{ connected ? - this.renderUnconnectedProvider(disconnectUrl, name) : - this.renderConnectedProvider(connectUrl, name) + this.renderConnectedProvider(disconnectUrl, name, id) : + this.renderUnconnectedProvider(connectUrl, name) }
); @@ -92,11 +124,18 @@ ThirdPartyAuth.propTypes = { connected: PropTypes.bool, id: PropTypes.string, })), + disconnectingState: PropTypes.oneOf([null, 'pending', 'complete', 'error']), + disconnectErrors: PropTypes.objectOf(PropTypes.bool), + disconnectAuth: PropTypes.func.isRequired, }; ThirdPartyAuth.defaultProps = { providers: undefined, + disconnectingState: null, + disconnectErrors: {}, }; -export default connect(thirdPartyAuthSelector)(ThirdPartyAuth); +export default connect(thirdPartyAuthSelector, { + disconnectAuth, +})(ThirdPartyAuth); diff --git a/src/account-settings/reducers.js b/src/account-settings/reducers.js index 7a545ea..d8dd514 100644 --- a/src/account-settings/reducers.js +++ b/src/account-settings/reducers.js @@ -6,6 +6,7 @@ import { FETCH_TIME_ZONES, SAVE_PREVIOUS_SITE_LANGUAGE, RESET_PASSWORD, + DISCONNECT_AUTH, UPDATE_DRAFT, RESET_DRAFTS, } from './actions'; @@ -23,6 +24,8 @@ export const defaultState = { resetPasswordState: null, timeZones: [], countryTimeZones: [], + disconnectingState: null, + disconnectErrors: {}, previousSiteLanguage: null, }; @@ -151,6 +154,32 @@ const accountSettingsReducer = (state = defaultState, action) => { countryTimeZones: action.payload.timeZones, }; + + case DISCONNECT_AUTH.BEGIN: + return { + ...state, + disconnectingState: 'pending', + }; + case DISCONNECT_AUTH.SUCCESS: + return { + ...state, + disconnectingState: 'complete', + }; + case DISCONNECT_AUTH.FAILURE: + return { + ...state, + disconnectingState: 'error', + disconnectErrors: { + [action.payload.providerId]: true, + }, + }; + case DISCONNECT_AUTH.RESET: + return { + ...state, + disconnectingState: null, + disconnectErrors: {}, + }; + default: return state; } diff --git a/src/account-settings/sagas.js b/src/account-settings/sagas.js index fea499a..55dd074 100644 --- a/src/account-settings/sagas.js +++ b/src/account-settings/sagas.js @@ -21,6 +21,11 @@ import { FETCH_TIME_ZONES, fetchTimeZones, fetchTimeZonesSuccess, + DISCONNECT_AUTH, + disconnectAuthBegin, + disconnectAuthSuccess, + disconnectAuthFailure, + disconnectAuthReset, } from './actions'; import { usernameSelector, userRolesSelector, siteLanguageSelector } from './selectors'; @@ -114,9 +119,23 @@ export function* handleFetchTimeZones(action) { } } +export function* handleDisconnectAuth(action) { + try { + yield put(disconnectAuthReset()); + yield put(disconnectAuthBegin()); + const response = yield call(ApiService.postDisconnectAuth, action.payload.url); + yield put(disconnectAuthSuccess(response)); + } catch (e) { + logAPIErrorResponse(e); + yield put(disconnectAuthFailure(action.payload.providerId)); + } +} + + export default function* saga() { yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings); yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings); yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword); yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones); + yield takeEvery(DISCONNECT_AUTH.BASE, handleDisconnectAuth); } diff --git a/src/account-settings/selectors.js b/src/account-settings/selectors.js index 3a6a61a..f79510b 100644 --- a/src/account-settings/selectors.js +++ b/src/account-settings/selectors.js @@ -75,9 +75,8 @@ export const thirdPartyAuthSelector = createSelector( accountSettingsSelector, accountSettings => ({ providers: accountSettings.authProviders, - loading: accountSettings.thirdPartyAuthLoading, - loaded: accountSettings.thirdPartyAuthLoaded, - loadingError: accountSettings.thirdPartyAuthLoadingError, + disconnectErrors: accountSettings.disconnectErrors, + disconnectingState: accountSettings.disconnectingState, }), ); diff --git a/src/account-settings/service.js b/src/account-settings/service.js index 3db8bb8..93761fb 100644 --- a/src/account-settings/service.js +++ b/src/account-settings/service.js @@ -219,3 +219,8 @@ export async function postResetPassword(email) { return data; } + +export async function postDisconnectAuth(url) { + const { data } = await apiClient.post(url).catch(handleRequestError); + return data; +}