diff --git a/src/_style.scss b/src/_style.scss
index 65463d81..af3b448b 100644
--- a/src/_style.scss
+++ b/src/_style.scss
@@ -63,6 +63,20 @@ $apple-focus-black: $apple-black;
}
}
+.btn-tpa {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ padding-left: 20px;
+
+ .icon-image {
+ background-color: transparent;
+ max-height: 24px;
+ max-width: 24px;
+ }
+}
+
.tpa-container {
display: flex;
flex-direction: row;
@@ -249,6 +263,21 @@ $apple-focus-black: $apple-black;
padding-top: 10px;
}
+.section-title {
+ width: 100%;
+ text-align: center;
+ border-bottom: 1px solid;
+ line-height: 0.1em;
+ margin-bottom: 2rem;
+ margin-top: 2rem;
+}
+.section-title span {
+ background:#fff;
+ padding:0 20px;
+ color:#646464;
+ font-size: 20px;
+}
+
@media (min-width: 576px) {
.reset-password-container {
width: 420px;
diff --git a/src/common-components/EnterpriseSSO.jsx b/src/common-components/EnterpriseSSO.jsx
new file mode 100644
index 00000000..b23f5c38
--- /dev/null
+++ b/src/common-components/EnterpriseSSO.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
+import { getConfig } from '@edx/frontend-platform';
+import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
+
+import {
+ Form, Button,
+} from '@edx/paragon';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
+import messages from './messages';
+
+const EnterpriseSSO = (props) => {
+ const { intl } = props;
+ const tpaProvider = props.provider;
+
+ const handleSubmit = (e, url) => {
+ e.preventDefault();
+ window.location.href = getConfig().LMS_BASE_URL + url;
+ };
+
+ const handleClick = (e) => {
+ e.preventDefault();
+ window.location.href = LOGIN_PAGE;
+ };
+
+ if (tpaProvider) {
+ return (
+
+ );
+ }
+ return ;
+};
+
+EnterpriseSSO.defaultProps = {
+ provider: {
+ id: '',
+ name: '',
+ iconClass: '',
+ iconImage: '',
+ loginUrl: '',
+ registerUrl: '',
+ },
+};
+
+EnterpriseSSO.propTypes = {
+ provider: PropTypes.shape({
+ id: PropTypes.string,
+ name: PropTypes.string,
+ iconClass: PropTypes.string,
+ iconImage: PropTypes.string,
+ loginUrl: PropTypes.string,
+ registerUrl: PropTypes.string,
+ }),
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(EnterpriseSSO);
diff --git a/src/common-components/messages.jsx b/src/common-components/messages.jsx
index 28c89415..b8970c8f 100644
--- a/src/common-components/messages.jsx
+++ b/src/common-components/messages.jsx
@@ -28,6 +28,17 @@ const messages = defineMessages({
defaultMessage: 'An error has occurred. Try refreshing the page, or check your Internet connection.',
description: 'Error message that appears when server responds with 500 error code',
},
+ // enterprise sso strings
+ 'enterprisetpa.title.heading': {
+ id: 'enterprisetpa.title.heading',
+ defaultMessage: 'Would you like to sign in using {providerName} credentials?',
+ description: 'Header text used in enterprise third party authentication',
+ },
+ 'enterprisetpa.login.button.text': {
+ id: 'enterprisetpa.login.button.text',
+ defaultMessage: 'Show me other ways to sign in or register',
+ description: 'Button text for login',
+ },
});
export default messages;
diff --git a/src/data/utils/dataUtils.js b/src/data/utils/dataUtils.js
index e94be8e9..7d0908f3 100644
--- a/src/data/utils/dataUtils.js
+++ b/src/data/utils/dataUtils.js
@@ -7,3 +7,21 @@ export default function processLink(link) {
});
return matches;
}
+
+export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
+ let tpaProvider = null;
+ primaryProviders.forEach((provider) => {
+ if (provider.id === tpaHintProvider) {
+ tpaProvider = provider;
+ }
+ });
+
+ if (!tpaProvider) {
+ secondaryProviders.forEach((provider) => {
+ if (provider.id === tpaHintProvider) {
+ tpaProvider = provider;
+ }
+ });
+ }
+ return tpaProvider;
+};
diff --git a/src/data/utils/index.js b/src/data/utils/index.js
index 7bca6fc4..ca817c73 100644
--- a/src/data/utils/index.js
+++ b/src/data/utils/index.js
@@ -1,2 +1,2 @@
-export { default } from './dataUtils';
+export { default, getTpaProvider } from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx
index 11b84ebb..83b7f9fe 100644
--- a/src/login/LoginPage.jsx
+++ b/src/login/LoginPage.jsx
@@ -21,6 +21,7 @@ import { loginErrorSelector, loginRequestSelector } from './data/selectors';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import LoginHelpLinks from './LoginHelpLinks';
import LoginFailureMessage from './LoginFailure';
+import EnterpriseSSO from '../common-components/EnterpriseSSO';
import messages from './messages';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
@@ -30,6 +31,7 @@ import {
DEFAULT_REDIRECT_URL, DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE, ENTERPRISE_LOGIN_URL, PENDING_STATE,
} from '../data/constants';
import { forgotPasswordResultSelector } from '../forgot-password';
+import { getTpaProvider } from '../data/utils';
class LoginPage extends React.Component {
constructor(props, context) {
@@ -50,9 +52,14 @@ class LoginPage extends React.Component {
componentDidMount() {
const params = (new URL(document.location)).searchParams;
+ const tpaHint = params.get('tpa_hint');
const payload = {
redirect_to: params.get('next') || DEFAULT_REDIRECT_URL,
};
+
+ if (tpaHint) {
+ payload.tpa_hint = tpaHint;
+ }
this.props.getThirdPartyAuthContext(payload);
}
@@ -144,16 +151,16 @@ class LoginPage extends React.Component {
} return thirdPartyComponent;
}
- render() {
+ renderForm(params,
+ currentProvider,
+ providers,
+ secondaryProviders,
+ thirdPartyAuthContext,
+ thirdPartyAuthApiStatus,
+ submitState,
+ intl) {
const { email, errors, password } = this.state;
- const {
- intl, submitState, thirdPartyAuthContext, thirdPartyAuthApiStatus,
- } = this.props;
- const { currentProvider, providers, secondaryProviders } = this.props.thirdPartyAuthContext;
-
- const params = (new URL(window.location.href)).searchParams;
const activationMsgType = params.get('account_activation_status');
-
if (this.state.institutionLogin) {
return (
);
}
+
+ render() {
+ const {
+ intl, submitState, thirdPartyAuthContext, thirdPartyAuthApiStatus,
+ } = this.props;
+ const { currentProvider, providers, secondaryProviders } = this.props.thirdPartyAuthContext;
+
+ const params = (new URL(window.location.href)).searchParams;
+
+ const tpaHint = params.get('tpa_hint');
+ if (tpaHint) {
+ if (thirdPartyAuthApiStatus === PENDING_STATE) {
+ return ;
+ }
+ const provider = getTpaProvider(tpaHint, providers, secondaryProviders);
+ return provider ? () : this.renderForm(params,
+ currentProvider,
+ providers,
+ secondaryProviders,
+ thirdPartyAuthContext,
+ thirdPartyAuthApiStatus,
+ submitState,
+ intl);
+ }
+ return this.renderForm(params,
+ currentProvider,
+ providers,
+ secondaryProviders,
+ thirdPartyAuthContext,
+ thirdPartyAuthApiStatus,
+ submitState,
+ intl);
+ }
}
LoginPage.defaultProps = {
diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx
index fffa6120..69b68b63 100644
--- a/src/login/tests/LoginPage.test.jsx
+++ b/src/login/tests/LoginPage.test.jsx
@@ -13,7 +13,7 @@ import LoginFailureMessage from '../LoginFailure';
import LoginPage from '../LoginPage';
import { loginRequest, loginRequestFailure } from '../data/actions';
import { RenderInstitutionButton } from '../../common-components';
-import { PENDING_STATE } from '../../data/constants';
+import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
@@ -413,4 +413,70 @@ describe('LoginPage', () => {
expect(loginPage.find()).toBeTruthy();
expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true);
});
+
+ it('should render tpa button for tpa_hint id in primary provider', () => {
+ const expectedMessage = `Sign in using ${appleProvider.name}`;
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ thirdPartyAuthContext: {
+ ...initialState.commonComponents.thirdPartyAuthContext,
+ providers: [appleProvider],
+ },
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
+ },
+ });
+
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL.concat(`/login?next=/dashboard&tpa_hint=${appleProvider.id}`) };
+ appleProvider.iconImage = null;
+
+ const loginPage = mount(reduxWrapper());
+ expect(loginPage.find(`button#${appleProvider.id}`).find('span').text()).toEqual(expectedMessage);
+ });
+
+ it('should render regular tpa button for invalid tpa_hint value', () => {
+ const expectedMessage = `${appleProvider.name}`;
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ thirdPartyAuthContext: {
+ ...initialState.commonComponents.thirdPartyAuthContext,
+ providers: [appleProvider],
+ },
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
+ },
+ });
+
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL.concat('/login?next=/dashboard&tpa_hint=invalid') };
+ appleProvider.iconImage = null;
+
+ const loginPage = mount(reduxWrapper());
+ expect(loginPage.find(`button#${appleProvider.id}`).find('span').text()).toEqual(expectedMessage);
+ });
+
+ it('should render tpa button for tpa_hint id in secondary provider', () => {
+ const expectedMessage = `Sign in using ${secondaryProviders.name}`;
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ thirdPartyAuthContext: {
+ ...initialState.commonComponents.thirdPartyAuthContext,
+ secondaryProviders: [secondaryProviders],
+ },
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
+ },
+ });
+
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL.concat(`/login?next=/dashboard&tpa_hint=${secondaryProviders.id}`) };
+ secondaryProviders.iconImage = null;
+
+ const loginPage = mount(reduxWrapper());
+ expect(loginPage.find(`button#${secondaryProviders.id}`).find('span').text()).toEqual(expectedMessage);
+ });
});
diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx
index 0aa1c041..628a8fe5 100644
--- a/src/register/RegistrationPage.jsx
+++ b/src/register/RegistrationPage.jsx
@@ -31,6 +31,7 @@ import {
InstitutionLogistration, AuthnValidationFormGroup,
} from '../common-components';
import RegistrationFailure from './RegistrationFailure';
+import EnterpriseSSO from '../common-components/EnterpriseSSO';
import {
DEFAULT_REDIRECT_URL,
DEFAULT_STATE,
@@ -42,7 +43,7 @@ import {
REGISTRATION_EXTRA_FIELDS,
} from '../data/constants';
import messages from './messages';
-import processLink from '../data/utils';
+import processLink, { getTpaProvider } from '../data/utils';
class RegistrationPage extends React.Component {
constructor(props, context) {
@@ -101,9 +102,14 @@ class RegistrationPage extends React.Component {
componentDidMount() {
const params = (new URL(document.location)).searchParams;
+ const tpaHint = params.get('tpa_hint');
const payload = {
redirect_to: params.get('next') || DEFAULT_REDIRECT_URL,
};
+
+ if (tpaHint) {
+ payload.tpa_hint = tpaHint;
+ }
this.props.getThirdPartyAuthContext(payload);
this.props.fetchRegistrationForm();
}
@@ -642,16 +648,13 @@ class RegistrationPage extends React.Component {
return thirdPartyComponent;
}
- render() {
- const { intl, submitState, thirdPartyAuthApiStatus } = this.props;
- const {
- currentProvider, finishAuthUrl, providers, secondaryProviders,
- } = this.props.thirdPartyAuthContext;
-
- if (!this.props.formData) {
- return ;
- }
-
+ renderForm(currentProvider,
+ providers,
+ secondaryProviders,
+ thirdPartyAuthApiStatus,
+ finishAuthUrl,
+ submitState,
+ intl) {
if (this.state.institutionLogin) {
return (
);
}
+
+ render() {
+ const { intl, submitState, thirdPartyAuthApiStatus } = this.props;
+ const {
+ currentProvider, finishAuthUrl, providers, secondaryProviders,
+ } = this.props.thirdPartyAuthContext;
+
+ if (!this.props.formData) {
+ return ;
+ }
+
+ const params = (new URL(window.location.href)).searchParams;
+ const tpaHint = params.get('tpa_hint');
+
+ if (tpaHint) {
+ if (thirdPartyAuthApiStatus === PENDING_STATE) {
+ return ;
+ }
+ const provider = getTpaProvider(tpaHint, providers, secondaryProviders);
+ return provider ? ()
+ : this.renderForm(currentProvider,
+ providers,
+ secondaryProviders,
+ thirdPartyAuthApiStatus,
+ finishAuthUrl,
+ submitState,
+ intl);
+ }
+ return this.renderForm(currentProvider,
+ providers,
+ secondaryProviders,
+ thirdPartyAuthApiStatus,
+ finishAuthUrl,
+ submitState,
+ intl);
+ }
}
RegistrationPage.defaultProps = {
diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx
index cbfce000..d5d26e46 100644
--- a/src/register/tests/RegistrationPage.test.jsx
+++ b/src/register/tests/RegistrationPage.test.jsx
@@ -11,7 +11,7 @@ import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import RegistrationPage from '../RegistrationPage';
import { RenderInstitutionButton } from '../../common-components';
import RegistrationFailureMessage from '../RegistrationFailure';
-import { PENDING_STATE } from '../../data/constants';
+import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
import { INTERNAL_SERVER_ERROR } from '../../login/data/constants';
import { fetchRegistrationForm, fetchRealtimeValidations, registerNewUser } from '../data/actions';
@@ -476,7 +476,7 @@ describe('./RegistrationPage.js', () => {
},
});
delete window.location;
- window.location = { href: '' };
+ window.location = { href: getConfig().BASE_URL };
renderer.create(reduxWrapper());
expect(window.location.href).toBe(dasboardUrl);
});
@@ -492,6 +492,8 @@ describe('./RegistrationPage.js', () => {
},
},
});
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL };
const root = mount(reduxWrapper());
expect(root.text().includes('Use my institution/campus credentials')).toBe(true);
});
@@ -507,6 +509,8 @@ describe('./RegistrationPage.js', () => {
},
},
});
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL };
const root = mount(reduxWrapper());
root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(root.text().includes('Test University')).toBe(true);
@@ -533,7 +537,7 @@ describe('./RegistrationPage.js', () => {
});
delete window.location;
- window.location = { href: '' };
+ window.location = { href: getConfig().BASE_URL };
renderer.create(reduxWrapper());
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
@@ -569,7 +573,7 @@ describe('./RegistrationPage.js', () => {
});
delete window.location;
- window.location = { href: '' };
+ window.location = { href: getConfig().BASE_URL };
const loginPage = mount(reduxWrapper());
@@ -589,6 +593,8 @@ describe('./RegistrationPage.js', () => {
},
});
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL };
const expectedMessage = 'You\'ve successfully signed into Apple. We just need a little more information before '
+ 'you start learning with openedX.';
@@ -597,39 +603,75 @@ describe('./RegistrationPage.js', () => {
});
it('check cookie rendered', () => {
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL };
const registerPage = mount(reduxWrapper());
expect(registerPage.find()).toBeTruthy();
});
- it('should show error message on 409 on alert and below the fields', () => {
- const windowSpy = jest.spyOn(global, 'window', 'get');
- windowSpy.mockImplementation(() => ({
- scrollTo: jest.fn(),
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- }));
+ it('should render tpa button for tpa_hint id in primary provider', () => {
+ const expectedMessage = `Sign in using ${appleProvider.name}`;
store = mockStore({
...initialState,
- register: {
- ...initialState.register,
- isSubmitted: true,
- registrationError: {
- email: [
- {
- user_message: 'It looks like test@gmail.com belongs to an existing account. Try again with a different email address.',
- },
- ],
- username: [
- {
- user_message: 'It looks like test belongs to an existing account. Try again with a different username.',
- },
- ],
+ commonComponents: {
+ ...initialState.commonComponents,
+ thirdPartyAuthContext: {
+ ...initialState.commonComponents.thirdPartyAuthContext,
+ providers: [appleProvider],
},
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
- const tree = renderer.create(reduxWrapper());
- expect(tree.toJSON()).toMatchSnapshot();
- windowSpy.mockClear();
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL.concat(`/login?next=/dashboard&tpa_hint=${appleProvider.id}`) };
+ appleProvider.iconImage = null;
+
+ const registerPage = mount(reduxWrapper());
+ expect(registerPage.find(`button#${appleProvider.id}`).find('span').text()).toEqual(expectedMessage);
+ });
+
+ it('should render regular tpa button for invalid tpa_hint value', () => {
+ const expectedMessage = `${appleProvider.name}`;
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ thirdPartyAuthContext: {
+ ...initialState.commonComponents.thirdPartyAuthContext,
+ providers: [appleProvider],
+ },
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
+ },
+ });
+
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL.concat('/login?next=/dashboard&tpa_hint=invalid') };
+ appleProvider.iconImage = null;
+
+ const registerPage = mount(reduxWrapper());
+ expect(registerPage.find(`button#${appleProvider.id}`).find('span').text()).toEqual(expectedMessage);
+ });
+
+ it('should render tpa button for tpa_hint id in secondary provider', () => {
+ const expectedMessage = `Sign in using ${secondaryProviders.name}`;
+ store = mockStore({
+ ...initialState,
+ commonComponents: {
+ ...initialState.commonComponents,
+ thirdPartyAuthContext: {
+ ...initialState.commonComponents.thirdPartyAuthContext,
+ secondaryProviders: [secondaryProviders],
+ },
+ thirdPartyAuthApiStatus: COMPLETE_STATE,
+ },
+ });
+
+ delete window.location;
+ window.location = { href: getConfig().BASE_URL.concat(`/login?next=/dashboard&tpa_hint=${secondaryProviders.id}`) };
+ secondaryProviders.iconImage = null;
+
+ const registerPage = mount(reduxWrapper());
+ expect(registerPage.find(`button#${secondaryProviders.id}`).find('span').text()).toEqual(expectedMessage);
});
});
diff --git a/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap b/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap
index 31beaff2..ddb495c2 100644
--- a/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap
+++ b/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap
@@ -812,228 +812,3 @@ exports[`./RegistrationPage.js should match pending button state snapshot 1`] =
`;
-
-exports[`./RegistrationPage.js should show error message on 409 on alert and below the fields 1`] = `
-
-
-
-
-
- We couldn't create your account.
-
-
- -
- It looks like test@gmail.com belongs to an existing account. Try again with a different email address.
-
- -
- It looks like test belongs to an existing account. Try again with a different username.
-
-
-
-
- Already have an edX account?
-
- Sign in.
-
-
-
-
- Create a new account
-
-
-
-
-
-`;