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

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