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:
Adam Butterworth
2019-04-30 11:10:29 -04:00
committed by GitHub
parent cbcaf3d3a6
commit b7049c1567
7 changed files with 244 additions and 1 deletions

View File

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

View File

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

View 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);

View File

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

View File

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

View File

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

View File

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