VAN-408 - Redesign login/register SSO-SAML (#270)

* VAN-408 - Redesign login/register SSO-SAML

* VAN-408 - Moved back button to Logistration component

* update sign-in messages (#272)

* VAN-408 - Added back the third party provider check

* VAN-408 - Updated DISABLE_ENTERPRISE_LOGIN value to empty string by default

* VAN-408 - Updated event tracking for institution login on register and login pages

* VAN-408 - Updated event tracking for institution login on register and login pages

* VAN-408 - Fixed Logistration event name test

* VAN-408 - Merged redesign-master and resolved tpa conflicts

Co-authored-by: Uzair Rasheed <uzairr@yahoo.com>
This commit is contained in:
Shafqat Farhan
2021-05-21 18:04:33 +05:00
committed by Waheed Ahmed
parent a29360fb66
commit b9f97a1dff
15 changed files with 338 additions and 197 deletions

View File

@@ -547,4 +547,17 @@ select.form-control {
justify-content: center;
margin-top: 4rem;
}
}
}
.institute-heading {
color: $primary-700;
}
.logistration-button {
color: $gray-700;
}
.logistration-button:hover{
color: $gray-700;
text-decoration: none;
}

View File

@@ -2,68 +2,64 @@ import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Button, Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import { Button, Hyperlink, Icon } from '@edx/paragon';
import { Institution } from '@edx/paragon/icons';
import messages from './messages';
export const RenderInstitutionButton = props => {
const { onSubmitHandler, secondaryProviders, buttonTitle } = props;
if (secondaryProviders !== undefined && secondaryProviders.length > 0) {
return (
<Button
className="w-auto mb-3"
block
variant="outline-primary"
onClick={onSubmitHandler}
>
{buttonTitle}
</Button>
);
}
return <></>;
const { onSubmitHandler, buttonTitle } = props;
return (
<Button
className="btn btn-link btn-sm text-body p-0 mb-4"
variant="link"
data-event-name="institution_login"
onClick={onSubmitHandler}
>
<Icon src={Institution} className="institute-icon" />
{buttonTitle}
</Button>
);
};
const InstitutionLogistration = props => {
const lmsBaseUrl = getConfig().LMS_BASE_URL;
const {
intl,
onSubmitHandler,
secondaryProviders,
headingTitle,
buttonTitle,
} = props;
return (
<>
<div className="d-flex justify-content-center m-4">
<div className="d-flex justify-content-left mb-4 mt-4">
<div className="flex-column">
<div className="mt-3">
<FontAwesomeIcon className="mr-2" icon={faChevronLeft} />
<Hyperlink
destination=""
onClick={onSubmitHandler}
>
{buttonTitle}
</Hyperlink>
</div>
<h1 className="mt-3 mb-4 font-weight-normal h3">
<h4 className="mb-2 font-weight-bold institute-heading">
{headingTitle}
</h1>
</h4>
<p className="mb-2">
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
</p>
<div className="mb-2 ml-2">
<ul>
{secondaryProviders.map(provider => (
<li key={provider}>
<Hyperlink destination={lmsBaseUrl + provider.loginUrl}>{provider.name}</Hyperlink>
</li>
))}
</ul>
</div>
</div>
</div>
<div className="mb-5">
<table className="pgn__data-table table-striped table-borderless">
<tbody>
{secondaryProviders.map(provider => (
<tr key={provider} className="pgn__data-table-row">
<td>
<Hyperlink
className="btn nav-item p-0 mb-1"
destination={lmsBaseUrl + provider.loginUrl}
>
{provider.name}
</Hyperlink>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
};
@@ -73,16 +69,17 @@ const LogistrationDefaultProps = {
buttonTitle: '',
};
const LogistrationProps = {
onSubmitHandler: PropTypes.func.isRequired,
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequried,
loginUrl: PropTypes.string.isRequired,
})),
buttonTitle: PropTypes.string,
};
RenderInstitutionButton.propTypes = {
...LogistrationProps,
buttonTitle: PropTypes.string,
onSubmitHandler: PropTypes.func.isRequired,
};
RenderInstitutionButton.defaultProps = {
...LogistrationDefaultProps,

View File

@@ -1,8 +1,12 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
@@ -13,22 +17,51 @@ import { RegistrationPage } from '../register';
const Logistration = (props) => {
const { intl, selectedPage } = props;
const tpa = getTpaHint();
const [institutionLogin, setInstitutionLogin] = useState(false);
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
sendPageEvent('login_and_registration', e.target.dataset.eventName);
setInstitutionLogin(!institutionLogin);
};
return (
<div>
{!tpa
&& (
<span className="nav nav-tabs">
<Link className={`nav-item nav-link ${selectedPage === REGISTER_PAGE ? 'active' : ''}`} to={updatePathWithQueryParams(REGISTER_PAGE)}>
{intl.formatMessage(messages['logistration.register'])}
</Link>
<Link className={`nav-item nav-link ${selectedPage === LOGIN_PAGE ? 'active' : ''}`} to={updatePathWithQueryParams(LOGIN_PAGE)}>
{intl.formatMessage(messages['logistration.sign.in'])}
</Link>
</span>
)}
{institutionLogin
? (
<span className="nav nav-tabs mt-2 pb-2">
<FontAwesomeIcon className="mr-2 mt-1 ml-3" icon={faChevronLeft} />
<Button
variant="link"
className="nav-item p-0 mb-1 logistration-button"
data-event-name={selectedPage === LOGIN_PAGE ? 'login' : 'register'}
onClick={handleInstitutionLogin}
>
{selectedPage === LOGIN_PAGE
? intl.formatMessage(messages['logistration.sign.in'])
: intl.formatMessage(messages['logistration.register'])}
</Button>
</span>
)
: (
<>
{!tpa && (
<span className="nav nav-tabs">
<Link className={`nav-item nav-link ${selectedPage === REGISTER_PAGE ? 'active' : ''}`} to={updatePathWithQueryParams(REGISTER_PAGE)}>
{intl.formatMessage(messages['logistration.register'])}
</Link>
<Link className={`nav-item nav-link ${selectedPage === LOGIN_PAGE ? 'active' : ''}`} to={updatePathWithQueryParams(LOGIN_PAGE)}>
{intl.formatMessage(messages['logistration.sign.in'])}
</Link>
</span>
)}
</>
)}
<div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE ? <LoginPage /> : <RegistrationPage />}
{selectedPage === LOGIN_PAGE
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
</div>
</div>
);

View File

@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'institution.login.page.sub.heading': {
id: 'institution.login.page.sub.heading',
defaultMessage: 'Choose your institution from the list below:',
defaultMessage: 'Choose your institution from the list below',
description: 'Heading of the institutions list',
},
// Confirmation Alert Message

View File

@@ -4,11 +4,13 @@ import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import Logistration from '../Logistration';
import { LOGIN_PAGE } from '../../data/constants';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
analytics.sendPageEvent = jest.fn();
@@ -19,6 +21,13 @@ const IntlLogistration = injectIntl(Logistration);
describe('Logistration', () => {
let store = {};
const secondaryProviders = {
id: 'saml-test',
name: 'Test University',
loginUrl: '/dummy-auth',
registerUrl: '/dummy_auth',
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
@@ -66,4 +75,102 @@ describe('Logistration', () => {
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
});
it('should display institution login option when secondary providers are present', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
expect(logistration.text().includes('Institution/campus credentials')).toBe(true);
// on clicking "Institution/campus credentials" button, it should display institution login page
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(logistration.text().includes('Test University')).toBe(true);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
it('send tracking and page events when institutional login button is clicked', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
it('should not display institution register button', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
const root = mount(reduxWrapper(<IntlLogistration />));
root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(root.text().includes('Test University')).toBe(true);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
});

View File

@@ -68,6 +68,7 @@ initialize({
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
WELCOME_PAGE_SUPPORT_LINK: process.env.WELCOME_PAGE_SUPPORT_LINK || null,
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
});
},
},

View File

@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { sendPageEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Form, Hyperlink, Icon, StatefulButton,
@@ -55,7 +55,6 @@ class LoginPage extends React.Component {
emailOrUsername: '',
password: '',
},
institutionLogin: false,
isSubmitted: false,
};
this.queryParams = getAllPossibleQueryParam();
@@ -76,12 +75,6 @@ class LoginPage extends React.Component {
return getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
}
handleInstitutionLogin = () => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
sendPageEvent('login_and_registration', 'institution_login');
this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin }));
}
handleSubmit = (e) => {
e.preventDefault();
this.setState({ isSubmitted: true });
@@ -129,23 +122,45 @@ class LoginPage extends React.Component {
}
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
let thirdPartyComponent = null;
if ((providers.length || secondaryProviders.length) && !currentProvider) {
thirdPartyComponent = (
<>
<RenderInstitutionButton
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={secondaryProviders}
buttonTitle={intl.formatMessage(messages['institution.login.button'])}
/>
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} />
</div>
</>
);
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
thirdPartyComponent = <Skeleton className="tpa-skeleton mb-3" height={30} count={2} />;
} return thirdPartyComponent;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
return (
<>
{(!isEnterpriseLoginDisabled || ((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive))
&& (
<div className="mt-4 mb-3 h4">
{intl.formatMessage(messages['login.other.options.heading'])}
</div>
)}
{!isEnterpriseLoginDisabled && (
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={this.getEnterPriseLoginURL()}>
<Icon src={Institution} className="institute-icon" />
{intl.formatMessage(messages['enterprise.login.btn.text'])}
</Hyperlink>
)}
{thirdPartyAuthApiStatus === PENDING_STATE ? (
<Skeleton className="tpa-skeleton mb-3" height={30} count={2} />
) : (
<>
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
<RenderInstitutionButton
onSubmitHandler={this.props.handleInstitutionLogin}
buttonTitle={intl.formatMessage(messages['institution.login.button'])}
/>
)}
{isSocialAuthActive && (
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} />
</div>
)}
</>
)}
</>
);
}
renderForm(
@@ -158,13 +173,11 @@ class LoginPage extends React.Component {
intl,
) {
const activationMsgType = getActivationStatus();
if (this.state.institutionLogin) {
if (this.props.institutionLogin) {
return (
<InstitutionLogistration
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={thirdPartyAuthContext.secondaryProviders}
headingTitle={intl.formatMessage(messages['institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['institution.login.page.back.button'])}
/>
);
}
@@ -230,13 +243,6 @@ class LoginPage extends React.Component {
<Link id="forgot-password" className="btn btn-link font-weight-500 text-body" to={RESET_PAGE}>
{intl.formatMessage(messages['forgot.password'])}
</Link>
<div className="mt-4 mb-3 h4">
{intl.formatMessage(messages['login.other.options.heading'])}
</div>
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={this.getEnterPriseLoginURL()}>
<Icon src={Institution} className="institute-icon" />
{intl.formatMessage(messages['enterprise.login.btn.text'])}
</Hyperlink>
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
</Form>
</div>
@@ -319,6 +325,8 @@ LoginPage.propTypes = {
secondaryProviders: PropTypes.array,
finishAuthUrl: PropTypes.string,
}),
institutionLogin: PropTypes.bool.isRequired,
handleInstitutionLogin: PropTypes.func.isRequired,
};
const mapStateToProps = state => {

View File

@@ -54,7 +54,7 @@ const messages = defineMessages({
},
'institution.login.button': {
id: 'institution.login.button',
defaultMessage: 'Use my university info',
defaultMessage: 'Institution/campus credentials',
description: 'shows institutions list',
},
'institution.login.page.title': {
@@ -64,7 +64,7 @@ const messages = defineMessages({
},
'institution.login.page.sub.heading': {
id: 'institution.login.page.sub.heading',
defaultMessage: 'Choose your institution from the list below:',
defaultMessage: 'Choose your institution from the list below',
description: 'Heading of the institutions list',
},
'institution.login.page.back.button': {

View File

@@ -15,7 +15,6 @@ import { loginRequest, loginRequestFailure, loginRequestReset } from '../data/ac
import LoginFailureMessage from '../LoginFailure';
import LoginPage from '../LoginPage';
import { RenderInstitutionButton } from '../../common-components';
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
@@ -77,6 +76,8 @@ describe('LoginPage', () => {
store = mockStore(initialState);
props = {
loginRequest: jest.fn(),
handleInstitutionLogin: jest.fn(),
institutionLogin: false,
};
});
@@ -171,6 +172,10 @@ describe('LoginPage', () => {
});
it('should show single sign on provider button', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
...initialState,
commonComponents: {
@@ -184,6 +189,10 @@ describe('LoginPage', () => {
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find(`button#${ssoProvider.id}`).length).toEqual(1);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
it('should not display institution login option when no secondary providers are present', () => {
@@ -191,26 +200,6 @@ describe('LoginPage', () => {
expect(root.text().includes('Use my university info')).toBe(false);
});
it('should display institution login option when secondary providers are present', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
});
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.text().includes('Use my university info')).toBe(true);
// on clicking "Use my university info" button, it should display institution login page
loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(loginPage.text().includes('Test University')).toBe(true);
});
// ******** test alert messages ********
it('should match login error message', () => {
@@ -322,6 +311,10 @@ describe('LoginPage', () => {
});
it('should redirect to social auth provider url on SSO button click', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard';
store = mockStore({
...initialState,
@@ -344,6 +337,10 @@ describe('LoginPage', () => {
loginPage.find('button#oa2-apple-id').simulate('click');
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + loginUrl);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
// ******** test hinted third party auth ********
@@ -392,6 +389,10 @@ describe('LoginPage', () => {
});
it('should render regular tpa button for invalid tpa_hint value', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
...initialState,
commonComponents: {
@@ -410,6 +411,10 @@ describe('LoginPage', () => {
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
expect(loginPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(`${ssoProvider.name}`);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
// ******** miscellaneous tests ********
@@ -424,24 +429,6 @@ describe('LoginPage', () => {
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
});
it('send tracking and page events when institutional button is clicked', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
});
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
loginPage.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
});
it('tests that form is only scrollable on form submission', () => {
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));

View File

@@ -70,7 +70,6 @@ class RegistrationPage extends React.Component {
},
errorCode: null,
failureCount: 0,
institutionLogin: false,
optionalFields,
optionalFieldsState: {},
showOptionalField: false,
@@ -154,10 +153,6 @@ class RegistrationPage extends React.Component {
);
}
handleInstitutionLogin = () => {
this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin }));
}
handleSubmit = (e) => {
e.preventDefault();
const totalRegistrationTime = (Date.now() - this.state.startTime) / 1000;
@@ -343,24 +338,37 @@ class RegistrationPage extends React.Component {
}
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
let thirdPartyComponent = null;
if ((providers.length || secondaryProviders.length) && !currentProvider) {
thirdPartyComponent = (
<>
<RenderInstitutionButton
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
/>
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
return (
<>
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
<div className="mt-4 mb-3 h4">
{intl.formatMessage(messages['registration.other.options.heading'])}
</div>
</>
);
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
thirdPartyComponent = <Skeleton className="tpa-skeleton" height={36} count={2} />;
}
return thirdPartyComponent;
)}
{thirdPartyAuthApiStatus === PENDING_STATE ? (
<Skeleton className="tpa-skeleton" height={36} count={2} />
) : (
<>
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
<RenderInstitutionButton
onSubmitHandler={this.props.handleInstitutionLogin}
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
/>
)}
{isSocialAuthActive && (
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
</div>
)}
</>
)}
</>
);
}
renderForm(currentProvider,
@@ -370,13 +378,11 @@ class RegistrationPage extends React.Component {
finishAuthUrl,
submitState,
intl) {
if (this.state.institutionLogin) {
if (this.props.institutionLogin) {
return (
<InstitutionLogistration
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['create.an.account'])}
/>
);
}
@@ -542,12 +548,6 @@ class RegistrationPage extends React.Component {
onClick={this.handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
&& !currentProvider ? (
<div className="mb-3 h4">
{intl.formatMessage(messages['registration.other.options.heading'])}
</div>
) : null}
{this.renderThirdPartyAuth(providers,
secondaryProviders,
currentProvider,
@@ -654,6 +654,8 @@ RegistrationPage.propTypes = {
clearUsernameSuggestions: PropTypes.func.isRequired,
statusCode: PropTypes.number,
usernameSuggestions: PropTypes.arrayOf(string),
institutionLogin: PropTypes.bool.isRequired,
handleInstitutionLogin: PropTypes.func.isRequired,
};
const mapStateToProps = state => {

View File

@@ -67,7 +67,7 @@ const messages = defineMessages({
// Institution login
'register.institution.login.button': {
id: 'register.institution.login.button',
defaultMessage: 'Use my institution/campus credentials',
defaultMessage: 'Institution/campus credentials',
description: 'shows institutions list',
},
'register.institution.login.page.title': {

View File

@@ -15,7 +15,6 @@ import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED } from '.
import RegistrationFailureMessage from '../RegistrationFailure';
import RegistrationPage from '../RegistrationPage';
import { RenderInstitutionButton } from '../../common-components';
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
@@ -77,6 +76,8 @@ describe('RegistrationPage', () => {
});
props = {
registrationResult: jest.fn(),
handleInstitutionLogin: jest.fn(),
institutionLogin: false,
};
});
@@ -133,7 +134,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const registerPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
populateRequiredFields(registerPage, payload);
registerPage.find('button.btn-brand').simulate('click');
@@ -164,7 +165,7 @@ describe('RegistrationPage', () => {
},
});
store.dispatch = jest.fn(store.dispatch);
const registerPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
populateRequiredFields(registerPage, formPayload, true);
registerPage.find('button.btn-brand').simulate('click');
@@ -174,7 +175,7 @@ describe('RegistrationPage', () => {
it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).not.toHaveBeenCalledWith(registerNewUser({}));
});
@@ -224,7 +225,7 @@ describe('RegistrationPage', () => {
it('should call validation api on blur event, if frontend validations have passed', () => {
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
// enter a valid username so that frontend validations are passed
registrationPage.find('input#username').simulate('change', { target: { value: 'test', name: 'username' } });
@@ -267,7 +268,7 @@ describe('RegistrationPage', () => {
},
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage />)).find('RegistrationPage');
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
expect(registrationPage.prop('validationDecisions')).toEqual({
country: '',
email: 'It looks like this email address is already registered',
@@ -390,6 +391,10 @@ describe('RegistrationPage', () => {
});
it('should display institution register button', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
...initialState,
commonComponents: {
@@ -404,26 +409,11 @@ describe('RegistrationPage', () => {
window.location = { href: getConfig().BASE_URL };
const root = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(root.text().includes('Use my institution/campus credentials')).toBe(true);
});
expect(root.text().includes('Institution/campus credentials')).toBe(true);
it('should not display institution register button', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
const root = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(root.text().includes('Test University')).toBe(true);
});
it('should display no password field when current provider is present', () => {
@@ -488,7 +478,7 @@ describe('RegistrationPage', () => {
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
renderer.create(reduxWrapper(<IntlRegistrationPage />));
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(window.location.href).toBe(dasboardUrl);
});
@@ -691,7 +681,7 @@ describe('RegistrationPage', () => {
describe('TestOptionalFields', () => {
it('should toggle optional fields state', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('click', { target: { name: 'optionalFields', checked: true } });
expect(registrationPage.find('RegistrationPage').state('showOptionalField')).toEqual(true);
@@ -702,7 +692,7 @@ describe('RegistrationPage', () => {
});
it('should show optional fields section on optional check enabled', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
registrationPage.update();
@@ -716,19 +706,19 @@ describe('RegistrationPage', () => {
mergeConfig({
REGISTRATION_OPTIONAL_FIELDS: '',
});
let registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
let registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('input#optional-field-checkbox').length).toEqual(0);
mergeConfig({
REGISTRATION_OPTIONAL_FIELDS: 'gender,goals,levelOfEducation,yearOfBirth',
});
registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('input#optional-field-checkbox').length).toEqual(1);
});
it('send tracking event on optional checkbox enabled', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#optional-field-checkbox').simulate('change', { target: { name: 'optionalFields', checked: true } });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.register.optional_fields_selected', {});
@@ -756,7 +746,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
const registerPage = mount(reduxWrapper(<IntlRegistrationPage />));
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
populateRequiredFields(registerPage, payload);
// submit optional fields