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
This commit is contained in:
@@ -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'])}
|
||||
/>
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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));
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
|
||||
151
src/account-settings/components/ThirdPartyAuth.jsx
Normal file
151
src/account-settings/components/ThirdPartyAuth.jsx
Normal file
@@ -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 (
|
||||
<React.Fragment>
|
||||
<h6>{name}</h6>
|
||||
<Hyperlink destination={url} className="btn btn-outline-primary">
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.link.account"
|
||||
defaultMessage="Sign in with {name}"
|
||||
description="An action link to link a connected third party account.m {name} will be Google, Facebook, etc."
|
||||
values={{ name }}
|
||||
/>
|
||||
</Hyperlink>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderUnconnectedProvider(url, name) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h6>
|
||||
{name}
|
||||
<span className="small font-weight-normal text-muted ml-2">
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.account.connected"
|
||||
defaultMessage="Linked"
|
||||
description="A badge to show that a third party account is linked"
|
||||
/>
|
||||
</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>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderProvider({
|
||||
name, disconnectUrl, connectUrl, connected, id,
|
||||
}) {
|
||||
return (
|
||||
<div className="form-group" key={id}>
|
||||
{
|
||||
connected ?
|
||||
this.renderUnconnectedProvider(disconnectUrl, name) :
|
||||
this.renderConnectedProvider(connectUrl, name)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderNoProviders() {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.no.providers"
|
||||
defaultMessage="No accounts can be linked at this time."
|
||||
description="Displayed when no third party accounts are available to link an edX account to"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.loading"
|
||||
defaultMessage="Loading..."
|
||||
description="Waiting for data to load in the third party auth provider list"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingError() {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.loading.error"
|
||||
defaultMessage="There was a problem loading linked accounts."
|
||||
description="Error message for failing to load the third party auth provider list"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.section.header"
|
||||
defaultMessage="Linked Accounts"
|
||||
description="Section header for the third party auth settings"
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.section.header"
|
||||
defaultMessage="You can link your identity accounts to simplify signing in to edX."
|
||||
description="Section subheader for the third party auth settings"
|
||||
/>
|
||||
</p>
|
||||
{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}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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}`,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user