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:
committed by
Waheed Ahmed
parent
a29360fb66
commit
b9f97a1dff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 || '',
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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} />));
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user