From b7049c1567f974b9a35d2da531f76acb40dfcf2d Mon Sep 17 00:00:00 2001 From: Adam Butterworth Date: Tue, 30 Apr 2019 11:10:29 -0400 Subject: [PATCH] feat: add linked accounts section (#10) * feat: add linked accounts section fix: change badge to small text fix: some i18n updates * fix: return manipulated provider data * fix: add i18n note --- src/account-settings/AccountSettingsPage.jsx | 7 +- src/account-settings/actions.js | 17 ++ .../components/ThirdPartyAuth.jsx | 151 ++++++++++++++++++ src/account-settings/reducers.js | 31 ++++ src/account-settings/sagas.js | 17 ++ src/account-settings/selectors.js | 10 ++ src/account-settings/service.js | 12 ++ 7 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/account-settings/components/ThirdPartyAuth.jsx diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index e69c08c..fe2d42d 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -11,12 +11,13 @@ import { import messages from './AccountSettingsPage.messages'; -import { fetchAccount } from './actions'; +import { fetchAccount, fetchThirdPartyAuthProviders } from './actions'; import { accountSettingsSelector } from './selectors'; import { PageLoading } from '../common'; import EditableField from './components/EditableField'; import PasswordReset from './components/PasswordReset'; +import ThirdPartyAuth from './components/ThirdPartyAuth'; import { YEAR_OF_BIRTH_OPTIONS, EDUCATION_LEVELS, @@ -43,6 +44,7 @@ class AccountSettingsPage extends React.Component { componentDidMount() { this.props.fetchAccount(); + this.props.fetchThirdPartyAuthProviders(); } renderContent() { @@ -103,6 +105,7 @@ class AccountSettingsPage extends React.Component { reverseTransform={v => ([{ code: v }])} label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])} /> + @@ -151,6 +154,7 @@ AccountSettingsPage.propTypes = { loaded: PropTypes.bool, loadingError: PropTypes.string, fetchAccount: PropTypes.func.isRequired, + fetchThirdPartyAuthProviders: PropTypes.func.isRequired, }; AccountSettingsPage.defaultProps = { @@ -162,4 +166,5 @@ AccountSettingsPage.defaultProps = { export default connect(accountSettingsSelector, { fetchAccount, + fetchThirdPartyAuthProviders, })(injectIntl(AccountSettingsPage)); diff --git a/src/account-settings/actions.js b/src/account-settings/actions.js index ad36e45..e674b22 100644 --- a/src/account-settings/actions.js +++ b/src/account-settings/actions.js @@ -4,6 +4,7 @@ const { AsyncActionType } = utils; export const FETCH_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_ACCOUNT'); export const SAVE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_ACCOUNT'); +export const FETCH_THIRD_PARTY_AUTH_PROVIDERS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_THIRD_PARTY_AUTH_PROVIDERS'); export const RESET_PASSWORD = new AsyncActionType('ACCOUNT_SETTINGS', 'RESET_PASSWORD'); export const OPEN_FORM = 'OPEN_FORM'; export const CLOSE_FORM = 'CLOSE_FORM'; @@ -103,3 +104,19 @@ export const resetPasswordReset = () => ({ type: RESET_PASSWORD.RESET, }); + +// fetch third party auth providers + +export const fetchThirdPartyAuthProviders = () => ({ + type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.BASE, +}); +export const fetchThirdPartyAuthProvidersBegin = () => ({ + type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.BEGIN, +}); +export const fetchThirdPartyAuthProvidersSuccess = providers => ({ + type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.SUCCESS, payload: { providers }, +}); +export const fetchThirdPartyAuthProvidersFailure = error => ({ + type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.FAILURE, payload: { error }, +}); + diff --git a/src/account-settings/components/ThirdPartyAuth.jsx b/src/account-settings/components/ThirdPartyAuth.jsx new file mode 100644 index 0000000..50da282 --- /dev/null +++ b/src/account-settings/components/ThirdPartyAuth.jsx @@ -0,0 +1,151 @@ +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 { fetchThirdPartyAuthProviders } from '../actions'; +import { thirdPartyAuthSelector } from '../selectors'; + +class ThirdPartyAuth extends React.Component { + componentDidMount() { + this.props.fetchThirdPartyAuthProviders(); + } + + renderConnectedProvider(url, name) { + return ( + +
{name}
+ + + +
+ ); + } + + renderUnconnectedProvider(url, name) { + return ( + +
+ {name} + + + +
+ + + +
+ ); + } + + renderProvider({ + name, disconnectUrl, connectUrl, connected, id, + }) { + return ( +
+ { + connected ? + this.renderUnconnectedProvider(disconnectUrl, name) : + this.renderConnectedProvider(connectUrl, name) + } +
+ ); + } + + renderNoProviders() { + return ( + + ); + } + + renderLoading() { + return ( + + ); + } + + renderLoadingError() { + return ( + + ); + } + + render() { + return ( +
+

+ +

+

+ +

+ {this.props.providers.map(this.renderProvider, this)} + {this.props.loaded && this.props.providers.length === 0 ? this.renderNoProviders() : null} + {this.props.loading ? this.renderLoading() : null} + {this.props.loadingError ? this.renderLoadingError() : null} +
+ ); + } +} + + +ThirdPartyAuth.propTypes = { + fetchThirdPartyAuthProviders: PropTypes.func.isRequired, + providers: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + disconnectUrl: PropTypes.string, + connectUrl: PropTypes.string, + connected: PropTypes.bool, + id: PropTypes.string, + })), + loading: PropTypes.bool, + loaded: PropTypes.bool, + loadingError: PropTypes.string, +}; + +ThirdPartyAuth.defaultProps = { + providers: [], + loading: false, + loaded: false, + loadingError: undefined, +}; + + +export default connect(thirdPartyAuthSelector, { + fetchThirdPartyAuthProviders, +})(ThirdPartyAuth); diff --git a/src/account-settings/reducers.js b/src/account-settings/reducers.js index cebd7d1..93c19e6 100644 --- a/src/account-settings/reducers.js +++ b/src/account-settings/reducers.js @@ -6,6 +6,7 @@ import { RESET_PASSWORD, UPDATE_DRAFT, RESET_DRAFTS, + FETCH_THIRD_PARTY_AUTH_PROVIDERS, } from './actions'; export const defaultState = { @@ -130,6 +131,36 @@ const accountSettingsReducer = (state = defaultState, action) => { resetPasswordState: 'complete', }; + case FETCH_THIRD_PARTY_AUTH_PROVIDERS.BEGIN: + return { + ...state, + thirdPartyAuthLoading: true, + thirdPartyAuthLoaded: false, + thirdPartyAuthLoadingError: null, + }; + case FETCH_THIRD_PARTY_AUTH_PROVIDERS.SUCCESS: + return { + ...state, + authProviders: action.payload.providers, + thirdPartyAuthLoading: false, + thirdPartyAuthLoaded: true, + thirdPartyAuthLoadingError: null, + }; + case FETCH_THIRD_PARTY_AUTH_PROVIDERS.FAILURE: + return { + ...state, + thirdPartyAuthLoading: false, + thirdPartyAuthLoaded: false, + thirdPartyAuthLoadingError: action.payload.error, + }; + case FETCH_THIRD_PARTY_AUTH_PROVIDERS.RESET: + return { + ...state, + thirdPartyAuthLoading: false, + thirdPartyAuthLoaded: false, + thirdPartyAuthLoadingError: null, + }; + default: return state; } diff --git a/src/account-settings/sagas.js b/src/account-settings/sagas.js index 2143a8b..d03c964 100644 --- a/src/account-settings/sagas.js +++ b/src/account-settings/sagas.js @@ -17,6 +17,10 @@ import { RESET_PASSWORD, resetPasswordBegin, resetPasswordSuccess, + FETCH_THIRD_PARTY_AUTH_PROVIDERS, + fetchThirdPartyAuthProvidersBegin, + fetchThirdPartyAuthProvidersSuccess, + fetchThirdPartyAuthProvidersFailure, } from './actions'; import { usernameSelector } from './selectors'; @@ -68,8 +72,21 @@ export function* handleResetPassword() { } } +export function* handleFetchThirdPartyAuthProviders() { + try { + yield put(fetchThirdPartyAuthProvidersBegin()); + const authProviders = yield call(ApiService.getThirdPartyAuthProviders); + yield put(fetchThirdPartyAuthProvidersSuccess(authProviders)); + } catch (e) { + LoggingService.logAPIErrorResponse(e); + yield put(fetchThirdPartyAuthProvidersFailure(e.message)); + yield put(push('/error')); + } +} + export default function* saga() { yield takeEvery(FETCH_ACCOUNT.BASE, handleFetchAccount); yield takeEvery(SAVE_ACCOUNT.BASE, handleSaveAccount); yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword); + yield takeEvery(FETCH_THIRD_PARTY_AUTH_PROVIDERS.BASE, handleFetchThirdPartyAuthProviders); } diff --git a/src/account-settings/selectors.js b/src/account-settings/selectors.js index fe66c64..0c4e6a0 100644 --- a/src/account-settings/selectors.js +++ b/src/account-settings/selectors.js @@ -63,3 +63,13 @@ export const resetPasswordSelector = createSelector( email: accountSettings.values.email, }), ); + +export const thirdPartyAuthSelector = createSelector( + accountSettingsSelector, + accountSettings => ({ + providers: accountSettings.authProviders, + loading: accountSettings.thirdPartyAuthLoading, + loaded: accountSettings.thirdPartyAuthLoaded, + loadingError: accountSettings.thirdPartyAuthLoadingError, + }), +); diff --git a/src/account-settings/service.js b/src/account-settings/service.js index 9c70299..2b5a8f7 100644 --- a/src/account-settings/service.js +++ b/src/account-settings/service.js @@ -64,3 +64,15 @@ export async function postResetPassword() { return data; } + + +export async function getThirdPartyAuthProviders() { + const { data } = await apiClient.get(`${config.LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`) + .catch(handleRequestError); + + return data.map(({ connect_url: connectUrl, disconnect_url: disconnectUrl, ...provider }) => ({ + ...provider, + connectUrl: `${config.LMS_BASE_URL}${connectUrl}`, + disconnectUrl: `${config.LMS_BASE_URL}${disconnectUrl}`, + })); +}