Fix auth disconnection add error states (#30)

* fix: properly disconnect auth

* fix: add error handling and states
This commit is contained in:
Adam Butterworth
2019-05-13 09:49:11 -06:00
committed by GitHub
parent 2e83d33de2
commit 9837b85dce
6 changed files with 127 additions and 17 deletions

View File

@@ -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,
});

View File

@@ -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 (
<React.Fragment>
<h6>{name}</h6>
@@ -23,7 +33,9 @@ class ThirdPartyAuth extends React.Component {
);
}
renderUnconnectedProvider(url, name) {
renderConnectedProvider(url, name, id) {
const hasError = this.props.disconnectErrors[id];
return (
<React.Fragment>
<h6>
@@ -36,14 +48,34 @@ class ThirdPartyAuth extends React.Component {
/>
</span>
</h6>
<Hyperlink destination={url}>
<FormattedMessage
id="account.settings.sso.unlink.account"
defaultMessage="Unlink {name} account"
description="An action link to unlink a connected third party account"
values={{ name }}
/>
</Hyperlink>
{hasError ? (
<Alert className="alert-danger">
<FormattedMessage
id="account.settings.sso.account.disconnect.error"
defaultMessage="There was a problem disconnecting this account. Contact support if the problem persists."
description="A message displayed when an error occurred while disconnecting a third party account"
/>
</Alert>
) : null}
<StatefulButton
className="btn-link"
state={this.props.disconnectingState}
labels={{
default: (
<FormattedMessage
id="account.settings.sso.unlink.account"
defaultMessage="Unlink {name} account"
description="An action link to unlink a connected third party account"
values={{ name }}
/>
),
}}
onClick={this.onClickDisconnect}
disabledStates={[]}
data-disconnect-url={url}
data-provider-id={id}
/>
</React.Fragment>
);
}
@@ -55,8 +87,8 @@ class ThirdPartyAuth extends React.Component {
<div className="form-group" key={id}>
{
connected ?
this.renderUnconnectedProvider(disconnectUrl, name) :
this.renderConnectedProvider(connectUrl, name)
this.renderConnectedProvider(disconnectUrl, name, id) :
this.renderUnconnectedProvider(connectUrl, name)
}
</div>
);
@@ -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);

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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,
}),
);

View File

@@ -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;
}