feat: added support for both legacy and new design (#349)
This commit is contained in:
@@ -23,9 +23,10 @@ AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||
REGISTRATION_OPTIONAL_FIELDS=''
|
||||
REGISTRATION_OPTIONAL_FIELDS='gender,goals,level_of_education,year_of_birth'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
COOKIE_DOMAIN='localhost'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
INFO_EMAIL='info@edx.org'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
DEFAULT_DESIGN='legacy'
|
||||
|
||||
2
Makefile
2
Makefile
@@ -2,7 +2,7 @@ transifex_resource = frontend-app-authn
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
i18n = ./src/legacy/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
@@ -6,7 +6,10 @@ module.exports = createConfig('jest', {
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/setupTest.js',
|
||||
'src/i18n',
|
||||
'src/legacy/i18n',
|
||||
'src/redesign/i18n',
|
||||
'src/index.jsx',
|
||||
'src/legacy/index.jsx',
|
||||
'src/redesign/index.jsx',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src/legacy --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
|
||||
@@ -4,49 +4,34 @@ import 'regenerator-runtime/runtime';
|
||||
import {
|
||||
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
|
||||
import {
|
||||
BaseComponent, UnAuthOnlyRoute, registerIcons, NotFoundPage, Logistration,
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, WELCOME_PAGE,
|
||||
} from './data/constants';
|
||||
import appMessages from './i18n';
|
||||
import './index.scss';
|
||||
const RedesignApp = React.lazy(() => import('./redesign/index.jsx'));
|
||||
const LegacyApp = React.lazy(() => import('./legacy/index.jsx'));
|
||||
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import WelcomePage from './welcome';
|
||||
const redesignAppMessages = React.lazy(() => import('./redesign/i18n'));
|
||||
const legacyAppMessages = React.lazy(() => import('./legacy/i18n'));
|
||||
|
||||
registerIcons();
|
||||
const OLD_DESIGN = 'legacy';
|
||||
const NEW_DESIGN = 'redesign';
|
||||
const DEFAULT_DESIGN = process.env.DEFAULT_DESIGN || OLD_DESIGN;
|
||||
const CHOSEN_DESIGN = localStorage.getItem('DESIGN_NAME') || DEFAULT_DESIGN;
|
||||
const REGISTRATION_OPTIONAL_FIELDS = CHOSEN_DESIGN === DEFAULT_DESIGN ? process.env.REGISTRATION_OPTIONAL_FIELDS : '';
|
||||
|
||||
const AppSelector = () => (
|
||||
<React.Suspense fallback={<></>}>
|
||||
{(CHOSEN_DESIGN === OLD_DESIGN) && <LegacyApp />}
|
||||
{(CHOSEN_DESIGN === NEW_DESIGN) && <RedesignApp />}
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<BaseComponent>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={REGISTER_PAGE} />
|
||||
</Route>
|
||||
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} />
|
||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Route exact path={WELCOME_PAGE} component={WelcomePage} />
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</BaseComponent>
|
||||
</AppProvider>,
|
||||
<AppSelector />,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
});
|
||||
@@ -64,7 +49,7 @@ initialize({
|
||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
|
||||
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
|
||||
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
|
||||
REGISTRATION_OPTIONAL_FIELDS: process.env.REGISTRATION_OPTIONAL_FIELDS || '',
|
||||
REGISTRATION_OPTIONAL_FIELDS,
|
||||
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,
|
||||
@@ -74,7 +59,7 @@ initialize({
|
||||
},
|
||||
},
|
||||
messages: [
|
||||
appMessages,
|
||||
CHOSEN_DESIGN === DEFAULT_DESIGN ? legacyAppMessages : redesignAppMessages,
|
||||
headerMessages,
|
||||
],
|
||||
});
|
||||
|
||||
340
src/legacy/_style.scss
Normal file
340
src/legacy/_style.scss
Normal file
@@ -0,0 +1,340 @@
|
||||
// ----------------------------
|
||||
// #COLORS
|
||||
// ----------------------------
|
||||
$font-blue: #126f9a;
|
||||
$white: #FFFFFF;
|
||||
|
||||
// social platforms
|
||||
$facebook-blue: #1877F2;
|
||||
$facebook-focus-blue: #29487d;
|
||||
$google-blue: #4285f4;
|
||||
$google-focus-blue: #287ae6;
|
||||
$microsoft-black: #2f2f2f;
|
||||
$microsoft-focus-black: #000;
|
||||
$apple-black: #000000;
|
||||
$apple-focus-black: $apple-black;
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.focus-out {
|
||||
position: absolute;
|
||||
padding-left: 17px;
|
||||
opacity: 0.75;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
color: #0075b4 !important;
|
||||
|
||||
&:hover {
|
||||
color: #065683 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.authn-header {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
height: 3.75rem;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.authn-header img {
|
||||
height: 1.75rem;
|
||||
margin-left: 2rem;
|
||||
padding: 1rem 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
|
||||
background-color: $white;
|
||||
border: 1px solid $font-blue;
|
||||
width: 242px;
|
||||
height: 36px;
|
||||
color: $font-blue;
|
||||
|
||||
.icon-image {
|
||||
background-color: transparent;
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: flex-start;
|
||||
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.font-container {
|
||||
background-color: $font-blue;
|
||||
color: $white;
|
||||
font-size: 11px;
|
||||
|
||||
margin-left: -6px;
|
||||
padding-top: 10px;
|
||||
min-width: 30px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.btn-oa2-facebook {
|
||||
color: $white;
|
||||
border-color: $facebook-blue;
|
||||
background-color: $facebook-blue;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $facebook-focus-blue;
|
||||
border: 1px solid $facebook-focus-blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-google-oauth2 {
|
||||
color: $white;
|
||||
border-color: $google-blue;
|
||||
background-color: $google-blue;
|
||||
|
||||
.icon-image {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $google-focus-blue;
|
||||
border: 1px solid $google-focus-blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-apple-id {
|
||||
color: $white;
|
||||
border-color: $apple-black;
|
||||
background-color: $apple-black;
|
||||
font-size: 16px;
|
||||
|
||||
.icon-image {
|
||||
max-height: 1.8em;
|
||||
max-width: 2.0em;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $apple-focus-black;
|
||||
border: 1px solid $apple-focus-black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-azuread-oauth2 {
|
||||
color: $white;
|
||||
border-color: $microsoft-black;
|
||||
background-color: $microsoft-black;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $microsoft-focus-black;
|
||||
border: 1px solid $microsoft-focus-black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
display: inherit;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-heading-line {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 20%;
|
||||
background-color: gray;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
width: 20%;
|
||||
background-color: gray;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.field-link {
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
color: $primary;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
text-transform: initial;
|
||||
letter-spacing: normal;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.login-help {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.opt-inline-field {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.opt-year-field {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.full-vertical-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.help-links {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
#honor-code p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#honor-code a span {
|
||||
@extend .sr-only;
|
||||
}
|
||||
|
||||
.mw-420 {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.mw-500 {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.mw-32em {
|
||||
max-width: 32em;
|
||||
}
|
||||
|
||||
.h-90 {
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pt-10 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.reset-password-container {
|
||||
width: 420px;
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.mw-500 {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.btn-social {
|
||||
width: 47%;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tpa-container {
|
||||
justify-content: center;
|
||||
}
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
.section-heading-line {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-size {
|
||||
background-size: 8px 10px;
|
||||
}
|
||||
|
||||
.x-small-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
166
src/legacy/common-components/AuthnValidationFormGroup.jsx
Normal file
166
src/legacy/common-components/AuthnValidationFormGroup.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
|
||||
const AuthnCustomValidationFormGroup = (props) => {
|
||||
const {
|
||||
onBlur, onChange, onClick, onFocus,
|
||||
} = props;
|
||||
const [showHelpText, setShowHelpText] = useState(false);
|
||||
const [showLabelText, setShowLabelText] = useState(false);
|
||||
|
||||
// handler code that need to be invoked via input
|
||||
const onClickHandler = (e, clickCb) => {
|
||||
setShowHelpText(true);
|
||||
setShowLabelText(true);
|
||||
if (clickCb) {
|
||||
clickCb(e);
|
||||
}
|
||||
};
|
||||
const onBlurHandler = (e, blurCb) => {
|
||||
setShowHelpText(false);
|
||||
setShowLabelText(false);
|
||||
if (blurCb) {
|
||||
blurCb(e);
|
||||
}
|
||||
};
|
||||
const onChangeHandler = (e, changeCb) => {
|
||||
if (changeCb) {
|
||||
changeCb(e);
|
||||
}
|
||||
};
|
||||
const onFocusHandler = (e, focusCb) => {
|
||||
if (focusCb) {
|
||||
focusCb(e);
|
||||
}
|
||||
};
|
||||
const onOptionalHandler = (e, clickCb) => { clickCb(e); };
|
||||
|
||||
const showLabel = () => {
|
||||
let className;
|
||||
if (props.optionalFieldCheckbox || (!showLabelText && (props.value !== '' || props.type === 'select'))) {
|
||||
className = 'sr-only';
|
||||
} else if (showLabelText) {
|
||||
className = 'pt-10 x-small-label';
|
||||
} else {
|
||||
className = 'pt-10 focus-out';
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Label htmlFor={props.for} className={className}>{props.label}</Form.Label>
|
||||
);
|
||||
};
|
||||
const showOptional = () => {
|
||||
const additionalField = props.optionalFieldCheckbox ? (
|
||||
<p role="presentation" id="additionalFields" className="mb-1 small" onClick={(e) => onOptionalHandler(e, onClick)}>
|
||||
{props.checkboxMessage}
|
||||
</p>
|
||||
) : <span />;
|
||||
return additionalField;
|
||||
};
|
||||
|
||||
const inputProps = {
|
||||
name: props.name,
|
||||
id: props.for,
|
||||
type: props.type,
|
||||
value: props.value,
|
||||
className: props.inputFieldStyle,
|
||||
'aria-invalid': props.ariaInvalid,
|
||||
autoComplete: 'on',
|
||||
};
|
||||
inputProps.onChange = (e) => onChangeHandler(e, onChange);
|
||||
inputProps.onClick = (e) => onClickHandler(e, onClick);
|
||||
inputProps.onBlur = (e) => onBlurHandler(e, onBlur);
|
||||
inputProps.onFocus = (e) => onFocusHandler(e, onFocus);
|
||||
|
||||
if (props.type === 'select') {
|
||||
inputProps.options = props.selectOptions;
|
||||
inputProps.className = props.value === '' ? `${props.inputFieldStyle} text-muted` : props.inputFieldStyle;
|
||||
}
|
||||
if (props.type === 'checkbox') {
|
||||
inputProps.checked = props.isChecked;
|
||||
}
|
||||
|
||||
const validationGroupProps = {
|
||||
for: props.for,
|
||||
};
|
||||
if (!props.optionalFieldCheckbox) {
|
||||
validationGroupProps.invalid = props.invalid;
|
||||
validationGroupProps.invalidMessage = props.invalidMessage;
|
||||
validationGroupProps.helpText = showHelpText ? props.helpText : '';
|
||||
} else {
|
||||
validationGroupProps.className = props.optionalFieldCheckbox ? 'custom-control pt-10 mb-0' : '';
|
||||
}
|
||||
if (props.className) {
|
||||
validationGroupProps.className = props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<ValidationFormGroup
|
||||
{...validationGroupProps}
|
||||
>
|
||||
{showLabel()}
|
||||
<Input
|
||||
{...inputProps}
|
||||
required
|
||||
/>
|
||||
{showOptional()}
|
||||
</ValidationFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
AuthnCustomValidationFormGroup.defaultProps = {
|
||||
name: '',
|
||||
for: '',
|
||||
label: '',
|
||||
optionalFieldCheckbox: false,
|
||||
type: '',
|
||||
value: '',
|
||||
invalid: false,
|
||||
ariaInvalid: false,
|
||||
invalidMessage: '',
|
||||
inputFieldStyle: '',
|
||||
helpText: '',
|
||||
className: '',
|
||||
onClick: null,
|
||||
onBlur: null,
|
||||
onChange: null,
|
||||
onFocus: null,
|
||||
isChecked: false,
|
||||
checkboxMessage: '',
|
||||
selectOptions: null,
|
||||
};
|
||||
|
||||
AuthnCustomValidationFormGroup.propTypes = {
|
||||
name: PropTypes.string,
|
||||
for: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
invalid: PropTypes.bool,
|
||||
ariaInvalid: PropTypes.bool,
|
||||
invalidMessage: PropTypes.string,
|
||||
helpText: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
inputFieldStyle: PropTypes.string,
|
||||
isChecked: PropTypes.bool,
|
||||
optionalFieldCheckbox: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
checkboxMessage: PropTypes.string,
|
||||
selectOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
export default AuthnCustomValidationFormGroup;
|
||||
49
src/legacy/common-components/ConfirmationAlert.jsx
Normal file
49
src/legacy/common-components/ConfirmationAlert.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ConfirmationAlert = (props) => {
|
||||
const { email, intl } = props;
|
||||
|
||||
return (
|
||||
<Alert id="confirmation-alert" variant="success">
|
||||
<Alert.Heading>{intl.formatMessage(messages['forgot.password.confirmation.title'])}</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="forgot.password.confirmation.message"
|
||||
defaultMessage="You entered {strongEmail}. If this email address is associated with your
|
||||
edX account, we will send a message with password recovery instructions to this email address."
|
||||
description="Forgot password confirmation message"
|
||||
values={{ strongEmail: <strong className="data-hj-suppress">{email}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages['forgot.password.confirmation.info'])}</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="forgot.password.technical.support.help.message"
|
||||
defaultMessage="If you need further assistance, {technicalSupportLink}."
|
||||
description="Message to help user contact technical support"
|
||||
values={{
|
||||
technicalSupportLink: (
|
||||
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['forgot.password.confirmation.support.link'])}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmationAlert.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ConfirmationAlert);
|
||||
105
src/legacy/common-components/EnterpriseSSO.jsx
Normal file
105
src/legacy/common-components/EnterpriseSSO.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
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 (
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-450">
|
||||
<h3>Sign in</h3>
|
||||
<Form className="m-0">
|
||||
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<Button
|
||||
id={tpaProvider.id}
|
||||
key={tpaProvider.id}
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="btn-tpa"
|
||||
onClick={(e) => handleSubmit(e, tpaProvider.loginUrl)}
|
||||
>
|
||||
{tpaProvider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||
<span className="pl-2" aria-hidden="true">{intl.formatMessage(messages['enterprisetpa.sso.button.title'], { providerName: tpaProvider.name })}</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</div>
|
||||
<span className="pl-2" aria-hidden="true">{intl.formatMessage(messages['enterprisetpa.sso.button.title'], { providerName: tpaProvider.name })}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="mb-4" />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline-primary"
|
||||
state="Complete"
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div />;
|
||||
};
|
||||
|
||||
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);
|
||||
22
src/legacy/common-components/HeaderLayout.jsx
Normal file
22
src/legacy/common-components/HeaderLayout.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
const HeaderLayout = ({ children }) => (
|
||||
<div className="d-flex flex-column">
|
||||
<CookiePolicyBanner languageCode={getLocale()} />
|
||||
<Header />
|
||||
<main className="flex-grow-1" id="main">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
HeaderLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default HeaderLayout;
|
||||
101
src/legacy/common-components/InstitutionLogistration.jsx
Normal file
101
src/legacy/common-components/InstitutionLogistration.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
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 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 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="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">
|
||||
{headingTitle}
|
||||
</h1>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LogistrationDefaultProps = {
|
||||
secondaryProviders: [],
|
||||
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,
|
||||
};
|
||||
RenderInstitutionButton.defaultProps = {
|
||||
...LogistrationDefaultProps,
|
||||
};
|
||||
|
||||
InstitutionLogistration.propTypes = {
|
||||
...LogistrationProps,
|
||||
intl: intlShape.isRequired,
|
||||
headingTitle: PropTypes.string,
|
||||
};
|
||||
InstitutionLogistration.defaultProps = {
|
||||
...LogistrationDefaultProps,
|
||||
headingTitle: '',
|
||||
};
|
||||
|
||||
export default injectIntl(InstitutionLogistration);
|
||||
75
src/legacy/common-components/SocialAuthProviders.jsx
Normal file
75
src/legacy/common-components/SocialAuthProviders.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
|
||||
function SocialAuthProviders(props) {
|
||||
const { intl, referrer, socialAuthProviders } = props;
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = e.currentTarget.dataset.providerUrl;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
}
|
||||
|
||||
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||
<button
|
||||
id={provider.id}
|
||||
key={provider.id}
|
||||
type="button"
|
||||
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
|
||||
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{provider.iconImage ? (
|
||||
<div className="ml-auto" aria-hidden="true">
|
||||
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<span id="provider-name" className="mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
||||
<span className="sr-only">
|
||||
{referrer === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
</span>
|
||||
</button>
|
||||
));
|
||||
|
||||
return <>{socialAuth}</>;
|
||||
}
|
||||
|
||||
SocialAuthProviders.defaultProps = {
|
||||
referrer: LOGIN_PAGE,
|
||||
socialAuthProviders: [],
|
||||
};
|
||||
|
||||
SocialAuthProviders.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
export default injectIntl(SocialAuthProviders);
|
||||
45
src/legacy/common-components/ThirdPartyAuthAlert.jsx
Normal file
45
src/legacy/common-components/ThirdPartyAuthAlert.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { currentProvider, referrer, platformName } = props;
|
||||
let message;
|
||||
|
||||
if (referrer === LOGIN_PAGE) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="login.third.party.auth.account.not.linked.message"
|
||||
defaultMessage="You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password."
|
||||
description="Message that appears on login page if user has successfully authenticated with TPA but no associated platform account exists"
|
||||
values={{ currentProvider, platformName }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="register.third.party.auth.account.not.linked.message"
|
||||
defaultMessage="You've successfully signed into {currentProvider}. We just need a little more information before you start learning with {platformName}."
|
||||
description="Message that appears on register page if user has successfully authenticated with TPA but no associated platform account exists"
|
||||
values={{ currentProvider, platformName }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>{ message }</Alert>;
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.defaultProps = {
|
||||
referrer: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string.isRequired,
|
||||
platformName: PropTypes.string.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ThirdPartyAuthAlert;
|
||||
23
src/legacy/common-components/data/service.js
Normal file
23
src/legacy/common-components/data/service.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function getThirdPartyAuthContext(urlParams) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
params: urlParams,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(
|
||||
`${getConfig().LMS_BASE_URL}/api/third_party_auth_context`,
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
return {
|
||||
thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { runSaga } from 'redux-saga';
|
||||
import * as actions from '../actions';
|
||||
import { fetchThirdPartyAuthContext } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
import initializeMockLogging from '../../../../setupTest';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
14
src/legacy/common-components/index.jsx
Normal file
14
src/legacy/common-components/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export { default as HeaderLayout } from './HeaderLayout';
|
||||
export { default as RedirectLogistration } from './RedirectLogistration';
|
||||
export { default as registerIcons } from './RegisterFaIcons';
|
||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||
export { default as InstitutionLogistration } from './InstitutionLogistration';
|
||||
export { RenderInstitutionButton } from './InstitutionLogistration';
|
||||
export { default as AuthnValidationFormGroup } from './AuthnValidationFormGroup';
|
||||
export { default as APIFailureMessage } from './APIFailureMessage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
65
src/legacy/common-components/messages.jsx
Normal file
65
src/legacy/common-components/messages.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
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:',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
// Confirmation Alert Message
|
||||
'forgot.password.confirmation.title': {
|
||||
id: 'forgot.password.confirmation.title',
|
||||
defaultMessage: 'Check your email',
|
||||
description: 'Forgot password confirmation message title',
|
||||
},
|
||||
'forgot.password.confirmation.support.link': {
|
||||
id: 'forgot.password.confirmation.support.link',
|
||||
defaultMessage: 'contact technical support',
|
||||
description: 'Technical support link text',
|
||||
},
|
||||
'forgot.password.confirmation.info': {
|
||||
id: 'forgot.password.confirmation.info',
|
||||
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
|
||||
+ 'email address, or check your spam folder.',
|
||||
description: 'Part of message that appears after user requests password change',
|
||||
},
|
||||
'internal.server.error.message': {
|
||||
id: 'internal.server.error.message',
|
||||
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',
|
||||
},
|
||||
'server.ratelimit.error.message': {
|
||||
id: 'server.ratelimit.error.message',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
// enterprise sso strings
|
||||
'enterprisetpa.title.heading': {
|
||||
id: 'enterprisetpa.title.heading',
|
||||
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
||||
description: 'Header text used in enterprise third party authentication',
|
||||
},
|
||||
'enterprisetpa.sso.button.title': {
|
||||
id: 'enterprisetpa.sso.button.title',
|
||||
defaultMessage: 'Sign in using {providerName}',
|
||||
description: 'Text for third party auth provider buttons',
|
||||
},
|
||||
'enterprisetpa.login.button.text': {
|
||||
id: 'enterprisetpa.login.button.text',
|
||||
defaultMessage: 'Show me other ways to sign in or register',
|
||||
description: 'Button text for login',
|
||||
},
|
||||
// social auth providers
|
||||
'sso.sign.in.with': {
|
||||
id: 'sso.sign.in.with',
|
||||
defaultMessage: 'Sign in with {providerName}',
|
||||
description: 'Screen reader text that appears before social auth provider name',
|
||||
},
|
||||
'sso.create.account.using': {
|
||||
id: 'sso.create.account.using',
|
||||
defaultMessage: 'Create account using {providerName}',
|
||||
description: 'Screen reader text that appears before social auth provider name',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import AuthnCustomValidationFormGroup from '../AuthnValidationFormGroup';
|
||||
|
||||
describe('AuthnCustomValidationFormGroup', () => {
|
||||
let props = {
|
||||
label: 'Email Label',
|
||||
for: 'email',
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
value: '',
|
||||
helpText: 'Email field help text',
|
||||
};
|
||||
|
||||
it('should show label in place of placeholder when field is empty', () => {
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label pt-10 focus-out');
|
||||
});
|
||||
|
||||
it('should show label on top of field when field is focused in', () => {
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
|
||||
validationFormGroup.find('input').simulate('focus');
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label pt-10 focus-out');
|
||||
});
|
||||
|
||||
it('should keep label hidden for checkbox field', () => {
|
||||
props = {
|
||||
...props,
|
||||
type: 'checkbox',
|
||||
optionalFieldCheckbox: true,
|
||||
};
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label sr-only');
|
||||
});
|
||||
|
||||
it('should keep label hidden when input field is not empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
value: 'test',
|
||||
};
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label sr-only');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ConfirmationAlert from '../ConfirmationAlert';
|
||||
|
||||
const IntlConfirmationAlertMessage = injectIntl(ConfirmationAlert);
|
||||
|
||||
describe('ConfirmationAlert', () => {
|
||||
const supportLink = 'https://support.test.com/What-if-I-did-not-receive-a-password-reset-message';
|
||||
mergeConfig({
|
||||
PASSWORD_RESET_SUPPORT_LINK: supportLink,
|
||||
});
|
||||
|
||||
it('should match default confirmation message', () => {
|
||||
const confirmationAlertMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlConfirmationAlertMessage email="test@example.com" />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'Check your email'
|
||||
+ 'You entered test@example.com. If this email address is associated with your edX account, '
|
||||
+ 'we will send a message with password recovery instructions to this email address.'
|
||||
+ 'If you do not receive a password reset message after 1 minute, verify that you entered '
|
||||
+ 'the correct email address, or check your spam folder.'
|
||||
+ 'If you need further assistance, contact technical support.';
|
||||
|
||||
expect(confirmationAlertMessage.find('#confirmation-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(confirmationAlertMessage.find('#confirmation-alert').find('a').props().href).toEqual(supportLink);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
||||
|
||||
describe('ThirdPartyAuthAlert', () => {
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
currentProvider: 'Google',
|
||||
platformName: 'edX',
|
||||
};
|
||||
});
|
||||
|
||||
it('should match login page third party auth alert message snapshot', () => {
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<ThirdPartyAuthAlert {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match register page third party auth alert message snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
referrer: 'register',
|
||||
};
|
||||
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<ThirdPartyAuthAlert {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SocialAuthProviders should match social auth provider with default icon snapshot 1`] = `
|
||||
<button
|
||||
className="btn-social btn-oa2-apple-id mr-3"
|
||||
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
||||
id="oa2-apple-id"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="font-container"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-sign-in-alt fa-w-16 "
|
||||
data-icon="sign-in-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M416 448h-84c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h84c17.7 0 32-14.3 32-32V160c0-17.7-14.3-32-32-32h-84c-6.6 0-12-5.4-12-12V76c0-6.6 5.4-12 12-12h84c53 0 96 43 96 96v192c0 53-43 96-96 96zm-47-201L201 79c-15-15-41-4.5-41 17v96H24c-13.3 0-24 10.7-24 24v96c0 13.3 10.7 24 24 24h136v96c0 21.5 26 32 41 17l168-168c9.3-9.4 9.3-24.6 0-34z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mr-auto pl-2"
|
||||
id="provider-name"
|
||||
>
|
||||
Apple
|
||||
</span>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Sign in with Apple
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`SocialAuthProviders should match social auth provider with iconClass snapshot 1`] = `
|
||||
<button
|
||||
className="btn-social btn-oa2-apple-id mr-3"
|
||||
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
||||
id="oa2-apple-id"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="font-container"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-google fa-w-16 "
|
||||
data-icon="google"
|
||||
data-prefix="fab"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 488 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mr-auto pl-2"
|
||||
id="provider-name"
|
||||
>
|
||||
Apple
|
||||
</span>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Sign in with Apple
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
|
||||
Array [
|
||||
<button
|
||||
className="btn-social btn-oa2-apple-id mr-3"
|
||||
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
||||
id="oa2-apple-id"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="ml-auto"
|
||||
>
|
||||
<img
|
||||
alt="icon Apple"
|
||||
className="icon-image"
|
||||
src="https://edx.devstack.lms/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mr-auto pl-2"
|
||||
id="provider-name"
|
||||
>
|
||||
Apple
|
||||
</span>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Sign in with Apple
|
||||
</span>
|
||||
</button>,
|
||||
<button
|
||||
className="btn-social btn-oa2-facebook "
|
||||
data-provider-url="/auth/login/facebook/?auth_entry=login&next=/dashboard"
|
||||
id="oa2-facebook"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="ml-auto"
|
||||
>
|
||||
<img
|
||||
alt="icon Facebook"
|
||||
className="icon-image"
|
||||
src="https://edx.devstack.lms/facebook-logo.png"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mr-auto pl-2"
|
||||
id="provider-name"
|
||||
>
|
||||
Facebook
|
||||
</span>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Sign in with Facebook
|
||||
</span>
|
||||
</button>,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert-content alert-warning mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
You have successfully signed into Google, but your Google account does not have a linked edX account. To link your accounts, sign in now using your edX password.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert-content alert-warning mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
You've successfully signed into Google. We just need a little more information before you start learning with edX.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
31
src/legacy/data/constants.js
Normal file
31
src/legacy/data/constants.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// URL Paths
|
||||
export const LOGIN_PAGE = '/login';
|
||||
export const REGISTER_PAGE = '/register';
|
||||
export const RESET_PAGE = '/reset';
|
||||
export const WELCOME_PAGE = '/welcome';
|
||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||
export const PAGE_NOT_FOUND = '/notfound';
|
||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||
|
||||
// Constants
|
||||
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
|
||||
|
||||
// Error Codes
|
||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
|
||||
|
||||
// States
|
||||
export const DEFAULT_STATE = 'default';
|
||||
export const PENDING_STATE = 'pending';
|
||||
export const COMPLETE_STATE = 'complete';
|
||||
|
||||
// Regex
|
||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||
|
||||
// Query string parameters that can be passed to LMS to manage
|
||||
// things like auto-enrollment upon login and registration.
|
||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next'];
|
||||
158
src/legacy/forgot-password/ForgotPasswordPage.jsx
Normal file
158
src/legacy/forgot-password/ForgotPasswordPage.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Formik } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Form,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { forgotPassword } from './data/actions';
|
||||
import { forgotPasswordResultSelector } from './data/selectors';
|
||||
import RequestInProgressAlert from './RequestInProgressAlert';
|
||||
|
||||
import messages from './messages';
|
||||
import {
|
||||
AuthnValidationFormGroup,
|
||||
} from '../common-components';
|
||||
import APIFailureMessage from '../common-components/APIFailureMessage';
|
||||
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||
import LoginHelpLinks from '../login/LoginHelpLinks';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
|
||||
const ForgotPasswordPage = (props) => {
|
||||
const { intl, status } = props;
|
||||
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
const getErrorMessage = (errors) => {
|
||||
const header = intl.formatMessage(messages['forgot.password.request.server.error']);
|
||||
if (errors.email) {
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
<Alert.Heading>{header}</Alert.Heading>
|
||||
<ul><li>{errors.email}</li></ul>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (status === INTERNAL_SERVER_ERROR) {
|
||||
return <APIFailureMessage header={header} errorCode={INTERNAL_SERVER_ERROR} />;
|
||||
}
|
||||
return status === 'forbidden' ? <RequestInProgressAlert /> : null;
|
||||
};
|
||||
|
||||
const getValidationMessage = (email) => {
|
||||
let error = '';
|
||||
|
||||
if (email === '') {
|
||||
error = intl.formatMessage(messages['forgot.password.empty.email.field.error']);
|
||||
} else if (!regex.test(email)) {
|
||||
error = intl.formatMessage(messages['forgot.password.page.invalid.email.message']);
|
||||
}
|
||||
|
||||
setValidationError(error);
|
||||
return error;
|
||||
};
|
||||
|
||||
sendPageEvent('login_and_registration', 'reset');
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{ email: '' }}
|
||||
validateOnChange={false}
|
||||
validate={(values) => {
|
||||
const validationMessage = getValidationMessage(values.email);
|
||||
|
||||
if (validationMessage !== '') {
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
return { email: validationMessage };
|
||||
}
|
||||
|
||||
return {};
|
||||
}}
|
||||
onSubmit={(values) => { props.forgotPassword(values.email); }}
|
||||
>
|
||||
{({
|
||||
errors, handleSubmit, setFieldValue, values,
|
||||
}) => (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['forgot.password.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
{status === 'complete' ? <Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} /> : null}
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column">
|
||||
<Form className="mw-500">
|
||||
{ getErrorMessage(errors) }
|
||||
<h1 className="mt-3 h3">
|
||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
||||
</h1>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
||||
</p>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||
for="forgot-password-input"
|
||||
name="email"
|
||||
type="email"
|
||||
invalid={validationError !== ''}
|
||||
ariaInvalid={validationError !== ''}
|
||||
invalidMessage={validationError}
|
||||
value={values.email}
|
||||
onBlur={() => getValidationMessage(values.email)}
|
||||
onChange={e => setFieldValue('email', e.target.value)}
|
||||
helpText={intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })}
|
||||
className="mb-0 w-100"
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
<LoginHelpLinks page="forgot-password" />
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="btn-primary mt-3"
|
||||
state={status}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
|
||||
}}
|
||||
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
ForgotPasswordPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
ForgotPasswordPage.defaultProps = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
forgotPasswordResultSelector,
|
||||
{
|
||||
forgotPassword,
|
||||
},
|
||||
)(injectIntl(ForgotPasswordPage));
|
||||
25
src/legacy/forgot-password/RequestInProgressAlert.jsx
Normal file
25
src/legacy/forgot-password/RequestInProgressAlert.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const RequestInProgressAlert = (props) => {
|
||||
const { intl } = props;
|
||||
|
||||
return (
|
||||
<Alert variant="danger">
|
||||
<Alert.Heading>{intl.formatMessage(messages['forgot.password.error.message.title'])}</Alert.Heading>
|
||||
<ul>
|
||||
<li>{intl.formatMessage(messages['forgot.password.request.in.progress.message'])}</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
RequestInProgressAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RequestInProgressAlert);
|
||||
35
src/legacy/forgot-password/data/reducers.js
Normal file
35
src/legacy/forgot-password/data/reducers.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { FORGOT_PASSWORD } from './actions';
|
||||
import { INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
|
||||
export const defaultState = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
if (action !== null) {
|
||||
switch (action.type) {
|
||||
case FORGOT_PASSWORD.BEGIN:
|
||||
return {
|
||||
status: 'pending',
|
||||
};
|
||||
case FORGOT_PASSWORD.SUCCESS:
|
||||
return {
|
||||
...action.payload,
|
||||
status: 'complete',
|
||||
};
|
||||
case FORGOT_PASSWORD.FORBIDDEN:
|
||||
return {
|
||||
status: 'forbidden',
|
||||
};
|
||||
case FORGOT_PASSWORD.FAILURE:
|
||||
return {
|
||||
status: INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
@@ -3,7 +3,7 @@ import { runSaga } from 'redux-saga';
|
||||
import * as actions from '../actions';
|
||||
import { handleForgotPassword } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
import initializeMockLogging from '../../../../setupTest';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
70
src/legacy/forgot-password/messages.js
Normal file
70
src/legacy/forgot-password/messages.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'forgot.password.page.title': {
|
||||
id: 'forgot.password.page.title',
|
||||
defaultMessage: 'Forgot Password | {siteName}',
|
||||
description: 'forgot password page title',
|
||||
},
|
||||
'forgot.password.page.heading': {
|
||||
id: 'forgot.password.page.heading',
|
||||
defaultMessage: 'Password assistance',
|
||||
description: 'The page heading for the forgot password page.',
|
||||
},
|
||||
'forgot.password.page.instructions': {
|
||||
id: 'forgot.password.page.instructions',
|
||||
defaultMessage: 'Please enter your log-in or recovery email address below and we will send you an email with instructions.',
|
||||
description: 'Instructions message for forgot password page.',
|
||||
},
|
||||
'forgot.password.page.invalid.email.message': {
|
||||
id: 'forgot.password.page.invalid.email.message',
|
||||
defaultMessage: "The email address you've provided isn't formatted correctly.",
|
||||
description: 'Invalid email address message for the forgot password page.',
|
||||
},
|
||||
'forgot.password.page.email.field.label': {
|
||||
id: 'forgot.password.page.email.field.label',
|
||||
defaultMessage: 'Email',
|
||||
description: 'Email field label for the forgot password page.',
|
||||
},
|
||||
'forgot.password.page.submit.button': {
|
||||
id: 'forgot.password.page.submit.button',
|
||||
defaultMessage: 'Recover my password',
|
||||
description: 'Submit button text for the forgot password page.',
|
||||
},
|
||||
'forgot.password.request.server.error': {
|
||||
id: 'forgot.password.request.server.error',
|
||||
defaultMessage: 'We couldn’t send the password recovery email.',
|
||||
description: 'Failed to send password recovery email.',
|
||||
},
|
||||
'forgot.password.error.message.title': {
|
||||
id: 'forgot.password.error.message.title',
|
||||
defaultMessage: 'An error occurred.',
|
||||
description: 'Title for message that appears when error occurs for password assistance page',
|
||||
},
|
||||
'forgot.password.request.in.progress.message': {
|
||||
id: 'forgot.password.request.in.progress.message',
|
||||
defaultMessage: 'Your previous request is in progress, please try again in a few moments.',
|
||||
description: 'Message displayed when previous password reset request is still in progress.',
|
||||
},
|
||||
'forgot.password.empty.email.field.error': {
|
||||
id: 'forgot.password.empty.email.field.error',
|
||||
defaultMessage: 'Please enter your email.',
|
||||
description: 'Error message that appears when user tries to submit empty email field',
|
||||
},
|
||||
'forgot.password.invalid.email.heading': {
|
||||
id: 'forgot.password.invalid.email',
|
||||
defaultMessage: 'An error occurred.',
|
||||
description: 'heading for invalid email',
|
||||
},
|
||||
'forgot.password.invalid.email.message': {
|
||||
id: 'forgot.password.invalid.email.message',
|
||||
defaultMessage: "The email address you've provided isn't formatted correctly.",
|
||||
description: 'message for invalid email',
|
||||
},
|
||||
'forgot.password.email.help.text': {
|
||||
id: 'forgot.password.email.help.text',
|
||||
defaultMessage: 'The email address you used to register with {platformName}',
|
||||
description: 'text help for the email',
|
||||
},
|
||||
});
|
||||
export default messages;
|
||||
160
src/legacy/forgot-password/tests/ForgotPasswordPage.test.jsx
Normal file
160
src/legacy/forgot-password/tests/ForgotPasswordPage.test.jsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { mount } from 'enzyme';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
|
||||
import ForgotPasswordPage from '../ForgotPasswordPage';
|
||||
import { INTERNAL_SERVER_ERROR } from '../../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
analytics.sendPageEvent = jest.fn();
|
||||
|
||||
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
|
||||
const mockStore = configureStore();
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('ForgotPasswordPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
forgotPassword: jest.fn(),
|
||||
status: null,
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default section snapshot', () => {
|
||||
const tree = renderer.create(reduxWrapper(<IntlForgotPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match forbidden section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'forbidden',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlForgotPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match pending section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'pending',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<IntlForgotPasswordPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match success section snapshot', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: 'complete',
|
||||
};
|
||||
renderer.create(
|
||||
reduxWrapper(
|
||||
<Router history={history}>
|
||||
<IntlForgotPasswordPage {...props} />
|
||||
</Router>,
|
||||
),
|
||||
);
|
||||
expect(history.location.pathname).toEqual('/login');
|
||||
});
|
||||
|
||||
it('should display need other help signing in button', () => {
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('button.field-link').first().text()).toEqual('Need other help signing in?');
|
||||
});
|
||||
|
||||
it('should display email validation error message', async () => {
|
||||
const validationMessage = "We couldn’t send the password recovery email.The email address you've provided isn't formatted correctly.";
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
wrapper.find('input#forgot-password-input').simulate(
|
||||
'change', { target: { value: 'invalid-email', name: 'email' } },
|
||||
);
|
||||
await act(async () => { await wrapper.find('button.btn-primary').simulate('click'); });
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.alert-danger').text()).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('should show alert on server error', () => {
|
||||
props = {
|
||||
...props,
|
||||
status: INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
const expectedMessage = 'We couldn’t send the password recovery email.'
|
||||
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
expect(wrapper.find('#internal-server-error').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display empty email validation message', async () => {
|
||||
const validationMessage = 'We couldn’t send the password recovery email.Please enter your email.';
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
await act(async () => { await forgotPasswordPage.find('button.btn-primary').simulate('click'); });
|
||||
|
||||
forgotPasswordPage.update();
|
||||
expect(forgotPasswordPage.find('.alert-danger').text()).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('should display request in progress error message', () => {
|
||||
const rateLimitMessage = 'An error occurred.Your previous request is in progress, please try again in a few moments.';
|
||||
store = mockStore({
|
||||
forgotPassword: { status: 'forbidden' },
|
||||
});
|
||||
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(forgotPasswordPage.find('.alert-danger').text()).toEqual(rateLimitMessage);
|
||||
});
|
||||
|
||||
it('should not display any error message on change event', () => {
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const emailInput = forgotPasswordPage.find('input#forgot-password-input');
|
||||
emailInput.simulate('change', { target: { value: 'invalid-email', name: 'email' } });
|
||||
forgotPasswordPage.update();
|
||||
|
||||
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it('should display error message on blur event', async () => {
|
||||
const validationMessage = 'Please enter your email.';
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const emailInput = forgotPasswordPage.find('input#forgot-password-input');
|
||||
|
||||
await act(async () => {
|
||||
await emailInput.simulate('blur', { target: { value: '', name: 'email' } });
|
||||
});
|
||||
|
||||
forgotPasswordPage.update();
|
||||
expect(forgotPasswordPage.find('#forgot-password-input-invalid-feedback').text()).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('check cookie rendered', () => {
|
||||
const forgotPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(forgotPage.find(<CookiePolicyBanner />)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ForgotPasswordPage should match default section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<form
|
||||
className="mw-500"
|
||||
>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Password assistance
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Please enter your log-in or recovery email address below and we will send you an email with instructions.
|
||||
</p>
|
||||
<div
|
||||
className="form-group mb-0 w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="forgot-password-input"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="forgot-password-input"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need other help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-null btn-primary mt-3 btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Recover my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ForgotPasswordPage should match forbidden section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<form
|
||||
className="mw-500"
|
||||
>
|
||||
<div
|
||||
className="fade alert-content undefined alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
An error occurred.
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
Your previous request is in progress, please try again in a few moments.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Password assistance
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Please enter your log-in or recovery email address below and we will send you an email with instructions.
|
||||
</p>
|
||||
<div
|
||||
className="form-group mb-0 w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="forgot-password-input"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="forgot-password-input"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need other help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-forbidden btn-primary mt-3 btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Recover my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ForgotPasswordPage should match pending section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<form
|
||||
className="mw-500"
|
||||
>
|
||||
<h1
|
||||
className="mt-3 h3"
|
||||
>
|
||||
Password assistance
|
||||
</h1>
|
||||
<p
|
||||
className="mb-4"
|
||||
>
|
||||
Please enter your log-in or recovery email address below and we will send you an email with instructions.
|
||||
</p>
|
||||
<div
|
||||
className="form-group mb-0 w-100"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="forgot-password-input"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="forgot-password-input"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need other help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-disabled={true}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-pending btn-primary mt-3 disabled btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className="pgn__stateful-btn-icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-spinner fa-w-16 fa-spin "
|
||||
data-icon="spinner"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Recover my password
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
146
src/legacy/i18n/transifex_input.json
Normal file
146
src/legacy/i18n/transifex_input.json
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"forgot.password.confirmation.message": "You entered {strongEmail}. If this email address is associated with your\n edX account, we will send a message with password recovery instructions to this email address.",
|
||||
"forgot.password.technical.support.help.message": "If you need further assistance, {technicalSupportLink}.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below:",
|
||||
"forgot.password.confirmation.title": "Check your email",
|
||||
"forgot.password.confirmation.support.link": "contact technical support",
|
||||
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"login.third.party.auth.account.not.linked.message": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
|
||||
"register.third.party.auth.account.not.linked.message": "You've successfully signed into {currentProvider}. We just need a little more information before you start learning with {platformName}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Password assistance",
|
||||
"forgot.password.page.instructions": "Please enter your log-in or recovery email address below and we will send you an email with instructions.",
|
||||
"forgot.password.page.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.page.email.field.label": "Email",
|
||||
"forgot.password.page.submit.button": "Recover my password",
|
||||
"forgot.password.request.server.error": "We couldn’t send the password recovery email.",
|
||||
"forgot.password.error.message.title": "An error occurred.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Please enter your email.",
|
||||
"forgot.password.invalid.email": "An error occurred.",
|
||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"non.compliant.password.error": "{passwordComplaintRequirements} {lineBreak}Your current password does not meet the new security\n requirements. We just sent a password-reset message to the email address associated with this account.\n Thank you for helping us keep your data safe.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.reset.password.message.with.link": "If you've forgotten your password, click {resetLink} to reset.",
|
||||
"login.locked.reset.password.message.with.link": "To be on the safe side, you can reset your password {resetLink} before you try again.",
|
||||
"login.page.title": "Login | {siteName}",
|
||||
"sign.in.button": "Sign in",
|
||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
||||
"forgot.password.link": "Forgot my password",
|
||||
"other.sign.in.issues": "Other sign in issues",
|
||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
||||
"institution.login.button": "Use my university info",
|
||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
||||
"institution.login.page.back.button": "Back to sign in",
|
||||
"create.an.account": "Create an account",
|
||||
"or.sign.in.with": "or sign in with",
|
||||
"non.compliant.password.title": "We recently changed our password requirements",
|
||||
"first.time.here": "First time here?",
|
||||
"email.label": "Email",
|
||||
"email.help.message": "The email address you used to register with edX.",
|
||||
"enterprise.login.link.text": "Sign in with your company or school",
|
||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
||||
"email.format.validation.less.chars.message": "Email must have at least 3 characters.",
|
||||
"email.validation.message": "Please enter your email.",
|
||||
"password.validation.message": "Please enter your password.",
|
||||
"password.label": "Password (required)",
|
||||
"register.link": "Create an account",
|
||||
"sign.in.heading": "Sign in",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
||||
"account.already.activated.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "Your account could not be activated",
|
||||
"account.activation.support.link": "contact support",
|
||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
||||
"login.failure.header.title": "We couldn't sign you in.",
|
||||
"contact.support.link": "contact {platformName} support",
|
||||
"login.failed.link.text": "here",
|
||||
"login.incorrect.credentials.error": "Email or password is incorrect.",
|
||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"create.account.button": "Create account",
|
||||
"already.have.an.edx.account": "Already have an edX account?",
|
||||
"sign.in.hyperlink": "Sign in.",
|
||||
"create.an.account.using": "or create an account using",
|
||||
"create.a.new.account": "Create a new account",
|
||||
"register.institution.login.button": "Use my institution/campus credentials",
|
||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
||||
"register.page.email.label": "Email (required)",
|
||||
"register.rate.limit.reached.message": "Too many failed registration attempts. Try again later.",
|
||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
||||
"email.ratelimit.incorrect.format.validation.message": "The email address you provided isn't formatted correctly.",
|
||||
"email.ratelimit.password.validation.message": "Your password must contain at least 8 characters",
|
||||
"register.page.password.validation.message": "Please enter your password.",
|
||||
"fullname.label": "Full name (required)",
|
||||
"fullname.validation.message": "Please enter your full name.",
|
||||
"username.label": "Public username (required)",
|
||||
"username.validation.message": "Please enter your public username.",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-).",
|
||||
"username.character.validation.message": "Your password must contain at least 1 letter.",
|
||||
"username.number.validation.message": "Your password must contain at least 1 number.",
|
||||
"username.ratelimit.less.chars.message": "Public username must have atleast 2 characters.",
|
||||
"country.validation.message": "Select your country or region of residence.",
|
||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"registration.request.failure.header": "We couldn't create your account.",
|
||||
"helptext.name": "This name will be used by any certificates that you earn.",
|
||||
"helptext.username": "The name that will identify you in your courses. It cannot be changed later.",
|
||||
"helptext.password": "Your password must contain at least 8 characters, including 1 letter & 1 number.",
|
||||
"helptext.email": "This is what you will use to login.",
|
||||
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
|
||||
"privacy.policy": "Privacy Policy",
|
||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
||||
"registration.country.label": "Country or region of residence (required)",
|
||||
"registration.field.gender.options.label": "Gender (optional)",
|
||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
||||
"registration.field.gender.options.f": "Female",
|
||||
"registration.field.gender.options.m": "Male",
|
||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
||||
"registration.field.education.levels.p": "Doctorate",
|
||||
"registration.field.education.levels.m": "Master's or professional degree",
|
||||
"registration.field.education.levels.b": "Bachelor's degree",
|
||||
"registration.field.education.levels.a": "Associate's degree",
|
||||
"registration.field.education.levels.hs": "Secondary/high school",
|
||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"registration.field.education.levels.el": "Elementary/primary school",
|
||||
"registration.field.education.levels.none": "No formal education",
|
||||
"registration.field.education.levels.other": "Other education",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"reset.password.request.invalid.token.description.message": "This password reset link is invalid. It may have been used already.\n To reset your password, go to the {loginPasswordLink} page and select {forgotPassword}",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password.page.heading": "Reset your password",
|
||||
"reset.password.page.instructions": "Enter and confirm your new password.",
|
||||
"reset.password.page.invalid.match.message": "Passwords do not match.",
|
||||
"forgot.password.page.new.field.label": "New password",
|
||||
"forgot.password.page.confirm.field.label": "Confirm password",
|
||||
"reset.password.page.submit.button": "Reset my password",
|
||||
"reset.password.request.success.header.message": "Password reset complete.",
|
||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
||||
"reset.password.request.forgot.password.text": "Forgot password",
|
||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
||||
"forgot.password.empty.new.password.error.heading": "We couldn't reset your password.",
|
||||
"reset.password.request.server.error": "Failed to reset password",
|
||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
||||
"reset.server.ratelimit.error": "Too many requests.",
|
||||
"reset.password.confirmation.support.link": "Sign in to your account.",
|
||||
"reset.password.request.success.header.description.message": "Your password has been reset. {loginPasswordLink}",
|
||||
"optional.fields.page.title": "Optional Fields | {siteName}",
|
||||
"optional.fields.page.heading": "Support education research by providing additional information.",
|
||||
"welcome.to.edx": "Welcome to edX, {username}!",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now"
|
||||
}
|
||||
43
src/legacy/index.jsx
Executable file
43
src/legacy/index.jsx
Executable file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import configureStore from './data/configureStore';
|
||||
import { RegistrationPage } from './register';
|
||||
import { LoginPage } from './login';
|
||||
import {
|
||||
LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, WELCOME_PAGE,
|
||||
} from './data/constants';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import {
|
||||
HeaderLayout, UnAuthOnlyRoute, registerIcons, NotFoundPage,
|
||||
} from './common-components';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import WelcomePage from './welcome';
|
||||
import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const LegacyApp = () => (
|
||||
<AppProvider store={configureStore()}>
|
||||
<HeaderLayout>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
</Route>
|
||||
<UnAuthOnlyRoute exact path={LOGIN_PAGE} component={LoginPage} />
|
||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={RegistrationPage} />
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Route exact path={WELCOME_PAGE} component={WelcomePage} />
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</HeaderLayout>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
export default LegacyApp;
|
||||
63
src/legacy/login/AccountActivationMessage.jsx
Normal file
63
src/legacy/login/AccountActivationMessage.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ACCOUNT_ACTIVATION_MESSAGE } from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const AccountActivationMessage = (props) => {
|
||||
const { intl, messageType } = props;
|
||||
const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType;
|
||||
|
||||
let activationMessage;
|
||||
let heading;
|
||||
|
||||
switch (messageType) {
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.SUCCESS: {
|
||||
heading = intl.formatMessage(messages['account.activation.success.message.title']);
|
||||
activationMessage = intl.formatMessage(messages['account.activation.success.message']);
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.INFO: {
|
||||
activationMessage = intl.formatMessage(messages['account.already.activated.message']);
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
||||
const supportLink = (
|
||||
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['account.activation.support.link'])}
|
||||
</Alert.Link>
|
||||
);
|
||||
|
||||
heading = intl.formatMessage(messages['account.activation.error.message.title']);
|
||||
activationMessage = (
|
||||
<FormattedMessage
|
||||
id="account.activation.error.message"
|
||||
defaultMessage="Something went wrong, please {supportLink} to resolve this issue."
|
||||
description="Account activation error message"
|
||||
values={{ supportLink }}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return activationMessage ? (
|
||||
<Alert id="account-activation-message" variant={variant}>
|
||||
{heading && <Alert.Heading>{heading}</Alert.Heading>}
|
||||
{activationMessage}
|
||||
</Alert>
|
||||
) : null;
|
||||
};
|
||||
|
||||
AccountActivationMessage.propTypes = {
|
||||
messageType: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccountActivationMessage);
|
||||
195
src/legacy/login/LoginFailure.jsx
Normal file
195
src/legacy/login/LoginFailure.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import processLink from '../data/utils';
|
||||
import {
|
||||
ACCOUNT_LOCKED_OUT,
|
||||
FAILED_LOGIN_ATTEMPT,
|
||||
FORBIDDEN_REQUEST,
|
||||
INACTIVE_USER,
|
||||
INCORRECT_EMAIL_PASSWORD,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
INVALID_FORM,
|
||||
NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
} from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const LoginFailureMessage = (props) => {
|
||||
const { intl } = props;
|
||||
const { context, errorCode, value } = props.loginError;
|
||||
let errorList;
|
||||
let link;
|
||||
|
||||
switch (errorCode) {
|
||||
case NON_COMPLIANT_PASSWORD_EXCEPTION: {
|
||||
errorList = (
|
||||
<li key="password-non-compliance">
|
||||
<FormattedMessage
|
||||
id="non.compliant.password.error"
|
||||
defaultMessage="{passwordComplaintRequirements} {lineBreak}Your current password does not meet the new security
|
||||
requirements. We just sent a password-reset message to the email address associated with this account.
|
||||
Thank you for helping us keep your data safe."
|
||||
values={{
|
||||
passwordComplaintRequirements: <strong>{intl.formatMessage(messages['non.compliant.password.title'])}</strong>,
|
||||
lineBreak: <br />,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FORBIDDEN_REQUEST:
|
||||
errorList = (
|
||||
<li key={FORBIDDEN_REQUEST}>
|
||||
{intl.formatMessage(messages['login.rate.limit.reached.message'])}
|
||||
</li>
|
||||
);
|
||||
break;
|
||||
case INACTIVE_USER: {
|
||||
const contextSupportLink = typeof context.supportLink === 'string' ? context.supportLink : '';
|
||||
const supportLink = (
|
||||
<Alert.Link href={contextSupportLink}>
|
||||
{intl.formatMessage(messages['contact.support.link'], { platformName: context.platformName })}
|
||||
</Alert.Link>
|
||||
);
|
||||
errorList = (
|
||||
<li key={INACTIVE_USER}>
|
||||
<FormattedMessage
|
||||
id="login.inactive.user.error"
|
||||
defaultMessage="In order to sign in, you need to activate your account.{lineBreak}
|
||||
{lineBreak}We just sent an activation link to {email}. If you do not receive an email,
|
||||
check your spam folders or {supportLink}."
|
||||
values={{
|
||||
lineBreak: <br />,
|
||||
email: <strong className="data-hj-suppress">{props.loginError.email}</strong>,
|
||||
supportLink,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
errorList = (
|
||||
<li key={INTERNAL_SERVER_ERROR}>
|
||||
{intl.formatMessage(messages['internal.server.error.message'])}
|
||||
</li>
|
||||
);
|
||||
break;
|
||||
case INVALID_FORM:
|
||||
errorList = (
|
||||
<>
|
||||
{context.email && <li key={`${INVALID_FORM}-email`}>{context.email}</li>}
|
||||
{context.password && <li key={`${INVALID_FORM}-password`}>{context.password}</li>}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case FAILED_LOGIN_ATTEMPT: {
|
||||
const resetLink = (
|
||||
<Alert.Link href="/reset">
|
||||
{intl.formatMessage(messages['login.failed.link.text'])}
|
||||
</Alert.Link>
|
||||
);
|
||||
errorList = (
|
||||
<>
|
||||
<li key={FAILED_LOGIN_ATTEMPT + 1}>
|
||||
{intl.formatMessage(messages['login.incorrect.credentials.error'])}
|
||||
</li>
|
||||
<li key={FAILED_LOGIN_ATTEMPT + 2}>
|
||||
{intl.formatMessage(messages['login.failed.attempt.error'], { remainingAttempts: context.remainingAttempts })}
|
||||
</li>
|
||||
<li key={FAILED_LOGIN_ATTEMPT + 3}>
|
||||
<FormattedMessage
|
||||
id="login.reset.password.message.with.link"
|
||||
defaultMessage="If you've forgotten your password, click {resetLink} to reset."
|
||||
description="Password reset user message with link"
|
||||
values={{ resetLink }}
|
||||
/>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_LOCKED_OUT: {
|
||||
const resetLink = (
|
||||
<Alert.Link href="/reset">
|
||||
{intl.formatMessage(messages['login.failed.link.text'])}
|
||||
</Alert.Link>
|
||||
);
|
||||
errorList = (
|
||||
<>
|
||||
<li key={ACCOUNT_LOCKED_OUT + 1}>
|
||||
{intl.formatMessage(messages['login.locked.out.error.message'], { lockedOutPeriod: context.lockedOutPeriod })}
|
||||
</li>
|
||||
<li key={FAILED_LOGIN_ATTEMPT + 2}>
|
||||
<FormattedMessage
|
||||
id="login.locked.reset.password.message.with.link"
|
||||
defaultMessage="To be on the safe side, you can reset your password {resetLink} before you try again."
|
||||
description="Password reset user message with link"
|
||||
values={{ resetLink }}
|
||||
/>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case INCORRECT_EMAIL_PASSWORD:
|
||||
errorList = (
|
||||
<li key={INCORRECT_EMAIL_PASSWORD}>
|
||||
{intl.formatMessage(messages['login.incorrect.credentials.error'])}
|
||||
</li>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// TODO: use errorCode instead of processing error messages on frontend
|
||||
errorList = value.trim().split('\n');
|
||||
errorList = errorList.map((error) => {
|
||||
let matches;
|
||||
if (error.includes('a href')) {
|
||||
matches = processLink(error);
|
||||
const [beforeLink, href, linkText, afterLink] = matches;
|
||||
link = href;
|
||||
if (href.indexOf('/dashboard?tpa_hint') === 0) {
|
||||
link = `/login?next=${href}`;
|
||||
}
|
||||
return (
|
||||
<li key={error}>
|
||||
{beforeLink}
|
||||
<Alert.Link href={link}>{linkText}</Alert.Link>
|
||||
{afterLink}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return <li key={error}>{error}</li>;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert id="login-failure-alert" variant="danger">
|
||||
<Alert.Heading>{intl.formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
|
||||
<ul>{errorList}</ul>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
LoginFailureMessage.defaultProps = {
|
||||
loginError: {
|
||||
errorCode: null,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
LoginFailureMessage.propTypes = {
|
||||
loginError: PropTypes.shape({
|
||||
context: PropTypes.object,
|
||||
email: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LoginFailureMessage);
|
||||
94
src/legacy/login/LoginHelpLinks.jsx
Normal file
94
src/legacy/login/LoginHelpLinks.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import SwitchContent from '../common-components/SwitchContent';
|
||||
import {
|
||||
LOGIN_PAGE,
|
||||
REGISTER_PAGE,
|
||||
RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { updatePathWithQueryParams } from '../data/utils';
|
||||
|
||||
const LoginHelpLinks = (props) => {
|
||||
const { intl, page } = props;
|
||||
const [showLoginHelp, setShowLoginHelpValue] = useState(false);
|
||||
|
||||
const toggleLoginHelp = (e) => {
|
||||
e.preventDefault();
|
||||
setShowLoginHelpValue(!showLoginHelp);
|
||||
};
|
||||
|
||||
const handleForgotPasswordLinkClickEvent = () => {
|
||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
};
|
||||
|
||||
const forgotPasswordLink = () => (
|
||||
<Hyperlink
|
||||
className="field-link"
|
||||
destination={updatePathWithQueryParams(RESET_PAGE)}
|
||||
onClick={handleForgotPasswordLinkClickEvent}
|
||||
>
|
||||
{intl.formatMessage(messages['forgot.password.link'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
const signUpLink = () => (
|
||||
<Hyperlink className="field-link" destination={updatePathWithQueryParams(REGISTER_PAGE)}>
|
||||
{intl.formatMessage(messages['register.link'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
const loginIssueSupportURL = (config) => (config.LOGIN_ISSUE_SUPPORT_LINK
|
||||
? (
|
||||
<Hyperlink className="field-link" destination={config.LOGIN_ISSUE_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['other.sign.in.issues'])}
|
||||
</Hyperlink>
|
||||
)
|
||||
: null);
|
||||
|
||||
const getHelpButtonMessage = () => {
|
||||
let mid = 'need.other.help.signing.in.collapsible.menu';
|
||||
if (page === LOGIN_PAGE) {
|
||||
mid = 'need.help.signing.in.collapsible.menu';
|
||||
}
|
||||
|
||||
return intl.formatMessage(messages[mid]);
|
||||
};
|
||||
|
||||
const renderLoginHelp = () => (
|
||||
<div className="login-help small">
|
||||
{ page === LOGIN_PAGE ? forgotPasswordLink() : signUpLink() }
|
||||
{ loginIssueSupportURL(getConfig()) }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="mt-2 field-link small" onClick={toggleLoginHelp}>
|
||||
<FontAwesomeIcon className="mr-1" icon={showLoginHelp ? faCaretDown : faCaretRight} />
|
||||
{getHelpButtonMessage()}
|
||||
</button>
|
||||
<SwitchContent
|
||||
expression={showLoginHelp ? 'showHelp' : 'default'}
|
||||
cases={{
|
||||
showHelp: renderLoginHelp(),
|
||||
default: <></>,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LoginHelpLinks.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
page: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LoginHelpLinks);
|
||||
377
src/legacy/login/LoginPage.jsx
Normal file
377
src/legacy/login/LoginPage.jsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import React from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
Form, Hyperlink, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import AccountActivationMessage from './AccountActivationMessage';
|
||||
import ConfirmationAlert from '../common-components/ConfirmationAlert';
|
||||
import { loginRequest, loginRequestFailure } from './data/actions';
|
||||
import { INVALID_FORM } from './data/constants';
|
||||
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||
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,
|
||||
InstitutionLogistration, AuthnValidationFormGroup,
|
||||
} from '../common-components';
|
||||
import {
|
||||
DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE, ENTERPRISE_LOGIN_URL, PENDING_STATE, VALID_EMAIL_REGEX,
|
||||
} from '../data/constants';
|
||||
import { forgotPasswordResultSelector } from '../forgot-password';
|
||||
import {
|
||||
getTpaHint,
|
||||
getTpaProvider,
|
||||
windowScrollTo,
|
||||
setSurveyCookie,
|
||||
getActivationStatus,
|
||||
getAllPossibleQueryParam,
|
||||
updatePathWithQueryParams,
|
||||
} from '../data/utils';
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
sendPageEvent('login_and_registration', 'login');
|
||||
this.state = {
|
||||
password: '',
|
||||
email: '',
|
||||
errors: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
institutionLogin: false,
|
||||
isSubmitted: false,
|
||||
};
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
this.tpaHint = getTpaHint();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const payload = { ...this.queryParams };
|
||||
|
||||
if (this.tpaHint) {
|
||||
payload.tpa_hint = this.tpaHint;
|
||||
}
|
||||
this.props.getThirdPartyAuthContext(payload);
|
||||
}
|
||||
|
||||
getEnterPriseLoginURL() {
|
||||
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 });
|
||||
|
||||
const { email, password } = this.state;
|
||||
const emailValidationError = this.validateEmail(email);
|
||||
const passwordValidationError = this.validatePassword(password);
|
||||
|
||||
if (emailValidationError !== '' || passwordValidationError !== '') {
|
||||
this.props.loginRequestFailure({
|
||||
errorCode: INVALID_FORM,
|
||||
context: { email: emailValidationError, password: passwordValidationError },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
email, password, ...this.queryParams,
|
||||
};
|
||||
this.props.loginRequest(payload);
|
||||
}
|
||||
|
||||
validateEmail(email) {
|
||||
const { errors } = this.state;
|
||||
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
|
||||
if (email === '') {
|
||||
errors.email = this.props.intl.formatMessage(messages['email.validation.message']);
|
||||
} else if (email.length < 3) {
|
||||
errors.email = this.props.intl.formatMessage(messages['email.format.validation.less.chars.message']);
|
||||
} else if (!regex.test(email)) {
|
||||
errors.email = this.props.intl.formatMessage(messages['email.format.validation.message']);
|
||||
} else {
|
||||
errors.email = '';
|
||||
}
|
||||
this.setState({ errors });
|
||||
return errors.email;
|
||||
}
|
||||
|
||||
validatePassword(password) {
|
||||
const { errors } = this.state;
|
||||
errors.password = password.length > 0 ? '' : this.props.intl.formatMessage(messages['password.validation.message']);
|
||||
|
||||
this.setState({ errors });
|
||||
return errors.password;
|
||||
}
|
||||
|
||||
handleCreateAccountLinkClickEvent() {
|
||||
sendTrackEvent('edx.bi.register_form.toggled', { category: 'user-engagement' });
|
||||
}
|
||||
|
||||
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 tpa-container">
|
||||
<SocialAuthProviders socialAuthProviders={providers} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
thirdPartyComponent = <Skeleton height={36} />;
|
||||
} return thirdPartyComponent;
|
||||
}
|
||||
|
||||
renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus,
|
||||
submitState,
|
||||
intl,
|
||||
) {
|
||||
const { email, errors, password } = this.state;
|
||||
const activationMsgType = getActivationStatus();
|
||||
if (this.state.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'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.loginResult.success) {
|
||||
setSurveyCookie('login');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['login.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={this.props.loginResult.success}
|
||||
redirectUrl={this.props.loginResult.redirectUrl}
|
||||
finishAuthUrl={thirdPartyAuthContext.finishAuthUrl}
|
||||
/>
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-500">
|
||||
{thirdPartyAuthContext.currentProvider
|
||||
&& (
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={thirdPartyAuthContext.currentProvider}
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
)}
|
||||
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
|
||||
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
|
||||
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
|
||||
{this.props.forgotPassword.status === 'complete' && !this.props.loginError ? (
|
||||
<ConfirmationAlert email={this.props.forgotPassword.email} />
|
||||
) : null}
|
||||
<p>
|
||||
{intl.formatMessage(messages['first.time.here'])}
|
||||
<Hyperlink
|
||||
className="ml-1"
|
||||
destination={updatePathWithQueryParams(REGISTER_PAGE)}
|
||||
onClick={this.handleCreateAccountLinkClickEvent}
|
||||
>
|
||||
{intl.formatMessage(messages['create.an.account'])}.
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<hr className="mt-0 border-gray-200" />
|
||||
<h1 className="text-left mt-2 mb-3 h3">
|
||||
{intl.formatMessage(messages['sign.in.heading'])}
|
||||
</h1>
|
||||
<Form className="m-0">
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['email.label'])}
|
||||
for="email"
|
||||
name="email"
|
||||
type="email"
|
||||
invalid={errors.email !== ''}
|
||||
ariaInvalid={errors.email !== ''}
|
||||
invalidMessage={errors.email}
|
||||
value={email}
|
||||
helpText={intl.formatMessage(messages['email.help.message'])}
|
||||
onChange={(e) => this.setState({ email: e.target.value, isSubmitted: false })}
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['password.label'])}
|
||||
for="password"
|
||||
name="password"
|
||||
type="password"
|
||||
invalid={errors.password !== ''}
|
||||
ariaInvalid={errors.password !== ''}
|
||||
invalidMessage={errors.password}
|
||||
value={password}
|
||||
onChange={(e) => this.setState({ password: e.target.value, isSubmitted: false })}
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
<LoginHelpLinks page={LOGIN_PAGE} />
|
||||
<Hyperlink className="field-link mt-0 mb-3 small" destination={this.getEnterPriseLoginURL()}>
|
||||
{intl.formatMessage(messages['enterprise.login.link.text'])}
|
||||
</Hyperlink>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['sign.in.button']),
|
||||
}}
|
||||
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
|
||||
onClick={this.handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</Form>
|
||||
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
|
||||
&& !currentProvider ? (
|
||||
<div className="mb-3">
|
||||
<hr className="mt-3 mb-3 border-gray-200" />
|
||||
{intl.formatMessage(messages['or.sign.in.with'])}
|
||||
</div>
|
||||
) : null}
|
||||
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl, submitState, thirdPartyAuthContext, thirdPartyAuthApiStatus,
|
||||
} = this.props;
|
||||
const { currentProvider, providers, secondaryProviders } = this.props.thirdPartyAuthContext;
|
||||
|
||||
if (this.tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders);
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.loginUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? (<EnterpriseSSO provider={provider} intl={intl} />) : this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
return this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginPage.defaultProps = {
|
||||
forgotPassword: null,
|
||||
loginResult: null,
|
||||
loginError: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
};
|
||||
|
||||
LoginPage.propTypes = {
|
||||
forgotPassword: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
loginError: PropTypes.objectOf(PropTypes.any),
|
||||
loginRequest: PropTypes.func.isRequired,
|
||||
loginRequestFailure: PropTypes.func.isRequired,
|
||||
loginResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const forgotPassword = forgotPasswordResultSelector(state);
|
||||
const loginResult = loginRequestSelector(state);
|
||||
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
|
||||
const loginError = loginErrorSelector(state);
|
||||
return {
|
||||
submitState: state.login.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
forgotPassword,
|
||||
loginError,
|
||||
loginResult,
|
||||
thirdPartyAuthContext,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
getThirdPartyAuthContext,
|
||||
loginRequest,
|
||||
loginRequestFailure,
|
||||
},
|
||||
)(injectIntl(LoginPage));
|
||||
23
src/legacy/login/data/actions.js
Normal file
23
src/legacy/login/data/actions.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
|
||||
|
||||
// Login
|
||||
export const loginRequest = creds => ({
|
||||
type: LOGIN_REQUEST.BASE,
|
||||
payload: { creds },
|
||||
});
|
||||
|
||||
export const loginRequestBegin = () => ({
|
||||
type: LOGIN_REQUEST.BEGIN,
|
||||
});
|
||||
|
||||
export const loginRequestSuccess = (redirectUrl, success) => ({
|
||||
type: LOGIN_REQUEST.SUCCESS,
|
||||
payload: { redirectUrl, success },
|
||||
});
|
||||
|
||||
export const loginRequestFailure = (loginError) => ({
|
||||
type: LOGIN_REQUEST.FAILURE,
|
||||
payload: { loginError },
|
||||
});
|
||||
33
src/legacy/login/data/reducers.js
Normal file
33
src/legacy/login/data/reducers.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { LOGIN_REQUEST } from './actions';
|
||||
|
||||
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
export const defaultState = {
|
||||
loginError: null,
|
||||
loginResult: {},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case LOGIN_REQUEST.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
submitState: PENDING_STATE,
|
||||
};
|
||||
case LOGIN_REQUEST.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loginResult: action.payload,
|
||||
};
|
||||
case LOGIN_REQUEST.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loginError: action.payload.loginError,
|
||||
submitState: DEFAULT_STATE,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
26
src/legacy/login/data/service.js
Normal file
26
src/legacy/login/data/service.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import querystring from 'querystring';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function loginRequest(creds) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/login_session/`,
|
||||
querystring.stringify(creds),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
|
||||
return {
|
||||
redirectUrl: data.redirect_url || `${getConfig().LMS_BASE_URL}/dashboard`,
|
||||
success: data.success || false,
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from '../constants';
|
||||
import * as actions from '../actions';
|
||||
import { handleLoginRequest } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
import initializeMockLogging from '../../../../setupTest';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
192
src/legacy/login/messages.jsx
Normal file
192
src/legacy/login/messages.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'login.page.title': {
|
||||
id: 'login.page.title',
|
||||
defaultMessage: 'Login | {siteName}',
|
||||
description: 'login page title',
|
||||
},
|
||||
'sign.in.button': {
|
||||
id: 'sign.in.button',
|
||||
defaultMessage: 'Sign in',
|
||||
description: 'Button label that appears on login page',
|
||||
},
|
||||
'need.help.signing.in.collapsible.menu': {
|
||||
id: 'need.help.signing.in.collapsible.menu',
|
||||
defaultMessage: 'Need help signing in?',
|
||||
description: 'A button for collapsible need help signing in menu on login page',
|
||||
},
|
||||
'forgot.password.link': {
|
||||
id: 'forgot.password.link',
|
||||
defaultMessage: 'Forgot my password',
|
||||
description: 'Forgot password link',
|
||||
},
|
||||
'other.sign.in.issues': {
|
||||
id: 'other.sign.in.issues',
|
||||
defaultMessage: 'Other sign in issues',
|
||||
description: 'A link that redirects to sign-in issues help',
|
||||
},
|
||||
'need.other.help.signing.in.collapsible.menu': {
|
||||
id: 'need.other.help.signing.in.collapsible.menu',
|
||||
defaultMessage: 'Need other help signing in?',
|
||||
description: 'A button for collapsible need other help signing in menu on forgot password page',
|
||||
},
|
||||
'institution.login.button': {
|
||||
id: 'institution.login.button',
|
||||
defaultMessage: 'Use my university info',
|
||||
description: 'shows institutions list',
|
||||
},
|
||||
'institution.login.page.title': {
|
||||
id: 'institution.login.page.title',
|
||||
defaultMessage: 'Sign in with institution/campus credentials',
|
||||
description: 'Heading of institution page',
|
||||
},
|
||||
'institution.login.page.sub.heading': {
|
||||
id: 'institution.login.page.sub.heading',
|
||||
defaultMessage: 'Choose your institution from the list below:',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
'institution.login.page.back.button': {
|
||||
id: 'institution.login.page.back.button',
|
||||
defaultMessage: 'Back to sign in',
|
||||
description: 'return to login page',
|
||||
},
|
||||
'create.an.account': {
|
||||
id: 'create.an.account',
|
||||
defaultMessage: 'Create an account',
|
||||
description: 'Message on button to return to register page',
|
||||
},
|
||||
'or.sign.in.with': {
|
||||
id: 'or.sign.in.with',
|
||||
defaultMessage: 'or sign in with',
|
||||
description: 'gives hint about other sign in options',
|
||||
},
|
||||
'non.compliant.password.title': {
|
||||
id: 'non.compliant.password.title',
|
||||
defaultMessage: 'We recently changed our password requirements',
|
||||
description: 'A title that appears in bold before error message for non-compliant password',
|
||||
},
|
||||
'first.time.here': {
|
||||
id: 'first.time.here',
|
||||
defaultMessage: 'First time here?',
|
||||
description: 'A question that appears before sign up link',
|
||||
},
|
||||
'email.label': {
|
||||
id: 'email.label',
|
||||
defaultMessage: 'Email',
|
||||
description: 'Label that appears above email field',
|
||||
},
|
||||
'email.help.message': {
|
||||
id: 'email.help.message',
|
||||
defaultMessage: 'The email address you used to register with edX.',
|
||||
description: 'Message that appears below email field on login page',
|
||||
},
|
||||
'enterprise.login.link.text': {
|
||||
id: 'enterprise.login.link.text',
|
||||
defaultMessage: 'Sign in with your company or school',
|
||||
description: 'Company or school login link text.',
|
||||
},
|
||||
'email.format.validation.message': {
|
||||
id: 'email.format.validation.message',
|
||||
defaultMessage: 'The email address you\'ve provided isn\'t formatted correctly.',
|
||||
description: 'Validation message that appears when email address format is incorrect',
|
||||
},
|
||||
'email.format.validation.less.chars.message': {
|
||||
id: 'email.format.validation.less.chars.message',
|
||||
defaultMessage: 'Email must have at least 3 characters.',
|
||||
description: 'Validation message that appears when email address is less than 3 characters',
|
||||
},
|
||||
'email.validation.message': {
|
||||
id: 'email.validation.message',
|
||||
defaultMessage: 'Please enter your email.',
|
||||
description: 'Validation message that appears when email is empty',
|
||||
},
|
||||
'password.validation.message': {
|
||||
id: 'password.validation.message',
|
||||
defaultMessage: 'Please enter your password.',
|
||||
description: 'Validation message that appears when password is empty',
|
||||
},
|
||||
'password.label': {
|
||||
id: 'password.label',
|
||||
defaultMessage: 'Password',
|
||||
description: 'Text that appears above password field or as a placeholder',
|
||||
},
|
||||
'register.link': {
|
||||
id: 'register.link',
|
||||
defaultMessage: 'Create an account',
|
||||
description: 'Register page link',
|
||||
},
|
||||
'sign.in.heading': {
|
||||
id: 'sign.in.heading',
|
||||
defaultMessage: 'Sign in',
|
||||
description: 'Sign in text',
|
||||
},
|
||||
// Account Activation Strings
|
||||
'account.activation.success.message.title': {
|
||||
id: 'account.activation.success.message.title',
|
||||
defaultMessage: 'Success! You have activated your account.',
|
||||
description: 'Account Activation success message title',
|
||||
},
|
||||
'account.activation.success.message': {
|
||||
id: 'account.activation.success.message',
|
||||
defaultMessage: 'You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.',
|
||||
description: 'Message show to learners when their account has been activated successfully',
|
||||
},
|
||||
'account.already.activated.message': {
|
||||
id: 'account.already.activated.message',
|
||||
defaultMessage: 'This account has already been activated.',
|
||||
description: 'Message shown when learner account has already been activated',
|
||||
},
|
||||
'account.activation.error.message.title': {
|
||||
id: 'account.activation.error.message.title',
|
||||
defaultMessage: 'Your account could not be activated',
|
||||
description: 'Account Activation error message title',
|
||||
},
|
||||
'account.activation.support.link': {
|
||||
id: 'account.activation.support.link',
|
||||
defaultMessage: 'contact support',
|
||||
description: 'Link text used in account activation error message to go to learner help center',
|
||||
},
|
||||
'internal.server.error.message': {
|
||||
id: 'internal.server.error.message',
|
||||
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',
|
||||
},
|
||||
'login.rate.limit.reached.message': {
|
||||
id: 'login.rate.limit.reached.message',
|
||||
defaultMessage: 'Too many failed login attempts. Try again later.',
|
||||
description: 'Error message that appears when an anonymous user has made too many failed login attempts',
|
||||
},
|
||||
'login.failure.header.title': {
|
||||
id: 'login.failure.header.title',
|
||||
defaultMessage: 'We couldn\'t sign you in.',
|
||||
description: 'Login failure header message.',
|
||||
},
|
||||
'contact.support.link': {
|
||||
id: 'contact.support.link',
|
||||
defaultMessage: 'contact {platformName} support',
|
||||
description: 'Link text used in inactive user error message to go to learner help center',
|
||||
},
|
||||
'login.failed.link.text': {
|
||||
id: 'login.failed.link.text',
|
||||
defaultMessage: 'here',
|
||||
description: 'Link text used in failed login attempt user error message to reset password',
|
||||
},
|
||||
'login.incorrect.credentials.error': {
|
||||
id: 'login.incorrect.credentials.error',
|
||||
defaultMessage: 'Email or password is incorrect.',
|
||||
description: 'Error message for incorrect email or password',
|
||||
},
|
||||
'login.failed.attempt.error': {
|
||||
id: 'login.failed.attempt.error',
|
||||
defaultMessage: 'You have {remainingAttempts} more sign in attempts before your account is temporarily locked.',
|
||||
description: 'Failed login attempts error message',
|
||||
},
|
||||
'login.locked.out.error.message': {
|
||||
id: 'login.locked.out.error.message',
|
||||
defaultMessage: 'To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.',
|
||||
description: 'Account locked out user message',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
155
src/legacy/login/tests/LoginFailure.test.jsx
Normal file
155
src/legacy/login/tests/LoginFailure.test.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LoginFailureMessage from '../LoginFailure';
|
||||
import {
|
||||
FORBIDDEN_REQUEST,
|
||||
INACTIVE_USER,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
INVALID_FORM,
|
||||
NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
} from '../data/constants';
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
|
||||
describe('LoginFailureMessage', () => {
|
||||
let props = {};
|
||||
|
||||
it('should match non compliant password error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.We recently changed our password requirements '
|
||||
+ 'Your current password does not meet the new security requirements. We just sent a '
|
||||
+ 'password-reset message to the email address associated with this account. '
|
||||
+ 'Thank you for helping us keep your data safe.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match inactive user error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
email: 'text@example.com',
|
||||
errorCode: INACTIVE_USER,
|
||||
context: {
|
||||
platformName: 'openedX',
|
||||
supportLink: 'https://support.edx.org/',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.In order to sign in, you need to activate your account. '
|
||||
+ 'We just sent an activation link to text@example.com. If you do not receive an email, '
|
||||
+ 'check your spam folders or contact openedX support.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual('https://support.edx.org/');
|
||||
});
|
||||
|
||||
it('should match rate limit error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: FORBIDDEN_REQUEST,
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.Too many failed login attempts. Try again later.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match internal server error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match invalid form error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: INVALID_FORM,
|
||||
context: { email: 'Please enter your email.', password: 'Please enter your password.' },
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.Please enter your email.Please enter your password.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match direct render of error message', () => {
|
||||
const errorMessage = 'Email or password is incorrect.';
|
||||
props = {
|
||||
loginError: {
|
||||
value: errorMessage,
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.'.concat(errorMessage);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match error message containing link snapshot', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
value: 'To be on the safe side, you can reset your password <a href="/reset">here</a> before you try again.\n',
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.To be on the safe side, you can reset your password here before you try again.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual('/reset');
|
||||
});
|
||||
});
|
||||
68
src/legacy/login/tests/LoginHelpLinks.test.jsx
Normal file
68
src/legacy/login/tests/LoginHelpLinks.test.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import LoginHelpLinks from '../LoginHelpLinks';
|
||||
import { LOGIN_PAGE } from '../../data/constants';
|
||||
|
||||
const otherSignInIssues = 'https://login-issue-support-url.com';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn().mockReturnValue({ LOGIN_ISSUE_SUPPORT_LINK: otherSignInIssues }),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
|
||||
describe('LoginHelpLinks', () => {
|
||||
let props = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
it('renders help links on button click', () => {
|
||||
props = {
|
||||
...props,
|
||||
page: LOGIN_PAGE,
|
||||
};
|
||||
const loginHelpLinks = mount(reduxWrapper(<LoginHelpLinks {...props} />));
|
||||
|
||||
expect(loginHelpLinks.find('.login-help').length).toBe(0);
|
||||
loginHelpLinks.find('button').first().simulate('click');
|
||||
expect(loginHelpLinks.find('.login-help').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should display login page help links', () => {
|
||||
props = {
|
||||
...props,
|
||||
page: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
const wrapper = mount(reduxWrapper(<LoginHelpLinks {...props} />));
|
||||
wrapper.find('button').first().simulate('click');
|
||||
|
||||
const loginHelpLinks = wrapper.find('a');
|
||||
|
||||
expect(loginHelpLinks.at(0).prop('href')).toEqual('/reset');
|
||||
expect(loginHelpLinks.at(1).prop('href')).toEqual(otherSignInIssues);
|
||||
});
|
||||
|
||||
it('should display forget password page help links', () => {
|
||||
props = {
|
||||
...props,
|
||||
page: 'forget-password',
|
||||
};
|
||||
|
||||
const wrapper = mount(reduxWrapper(<LoginHelpLinks {...props} />));
|
||||
wrapper.find('button').first().simulate('click');
|
||||
|
||||
const loginHelpLinks = wrapper.find('a');
|
||||
|
||||
expect(loginHelpLinks.at(0).prop('href')).toEqual('/register');
|
||||
expect(loginHelpLinks.at(1).prop('href')).toEqual(otherSignInIssues);
|
||||
});
|
||||
});
|
||||
523
src/legacy/login/tests/LoginPage.test.jsx
Normal file
523
src/legacy/login/tests/LoginPage.test.jsx
Normal file
@@ -0,0 +1,523 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { mount } from 'enzyme';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LoginFailureMessage from '../LoginFailure';
|
||||
import LoginPage from '../LoginPage';
|
||||
import { loginRequest, loginRequestFailure } from '../data/actions';
|
||||
import { RenderInstitutionButton } from '../../common-components';
|
||||
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
analytics.sendPageEvent = jest.fn();
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
const IntlLoginPage = injectIntl(LoginPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('LoginPage', () => {
|
||||
mergeConfig({
|
||||
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
forgotPassword: { status: null },
|
||||
login: {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const secondaryProviders = {
|
||||
id: 'saml-test',
|
||||
name: 'Test University',
|
||||
loginUrl: '/dummy-auth',
|
||||
registerUrl: '/dummy_auth',
|
||||
skipHintedLogin: false,
|
||||
};
|
||||
|
||||
const appleProvider = {
|
||||
id: 'oa2-apple-id',
|
||||
name: 'Apple',
|
||||
iconClass: null,
|
||||
iconImage: 'https://edx.devstack.lms/logo.png',
|
||||
loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
|
||||
};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
props = {
|
||||
loginRequest: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default section snapshot', () => {
|
||||
const tree = renderer.create(reduxWrapper(<IntlLoginPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match pending button state snapshot', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
submitState: PENDING_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const tree = renderer.create(reduxWrapper(<IntlLoginPage {...props} />))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match forget password alert message snapshot', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
forgotPassword: { status: 'complete', email: 'test@example.com' },
|
||||
});
|
||||
|
||||
const tree = renderer.create(reduxWrapper(<IntlLoginPage {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match TPA provider snapshot', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [appleProvider],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tree = renderer.create(reduxWrapper(<IntlLoginPage {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show error message', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginError: { value: 'Email or password is incorrect.' },
|
||||
},
|
||||
});
|
||||
|
||||
const tree = renderer.create(reduxWrapper(<IntlLoginPage {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show account activation message', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat('/login'), search: '?account_activation_status=info' };
|
||||
|
||||
const expectedMessage = 'This account has already been activated.';
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#account-activation-message').find('div').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display login help button', () => {
|
||||
const root = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(root.find('button.field-link').first().text()).toEqual('Need help signing in?');
|
||||
});
|
||||
|
||||
it('updates the error state for empty email input on form submission', () => {
|
||||
const errorState = { email: 'Please enter your email.', password: '' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
loginRequestFailure({ errorCode: 'invalid-form', context: errorState }),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the error state for invalid email; less than 3 characters on form submission', () => {
|
||||
const errorState = { email: 'Email must have at least 3 characters.', password: '' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
|
||||
loginPage.find('input#email').simulate('change', { target: { value: 'te', name: 'email' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
loginRequestFailure({ errorCode: 'invalid-form', context: errorState }),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the error state for invalid email format validation on form submission', () => {
|
||||
const errorState = { email: 'The email address you\'ve provided isn\'t formatted correctly.', password: '' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
|
||||
loginPage.find('input#email').simulate('change', { target: { value: 'test@', name: 'email' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
});
|
||||
|
||||
it('updates the error state for invalid password', () => {
|
||||
const errorState = { email: '', password: 'Please enter your password.' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
|
||||
loginPage.find('input#email').simulate('change', { target: { value: 'test@example.com', name: 'email' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
loginRequestFailure({ errorCode: 'invalid-form', context: errorState }),
|
||||
);
|
||||
});
|
||||
|
||||
it('submits login request for valid email and password values', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
loginPage.find('input#email').simulate('change', { target: { value: 'test@example.com' } });
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'password' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
loginRequest({ email: 'test@example.com', password: 'password' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should match url after redirection', () => {
|
||||
const dasboardUrl = 'http://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginResult: {
|
||||
success: true,
|
||||
redirectUrl: dasboardUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(dasboardUrl);
|
||||
});
|
||||
|
||||
it('should match url after TPA redirection', () => {
|
||||
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginResult: {
|
||||
success: true,
|
||||
redirectUrl: '',
|
||||
},
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
finishAuthUrl: authCompleteUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
});
|
||||
|
||||
it('should redirect to enterprise selection page', () => {
|
||||
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||
const enterpriseSelectionPage = 'http://localhost:18000/enterprise/select/active/?success_url='.concat(authCompleteUrl);
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginResult: {
|
||||
success: true,
|
||||
redirectUrl: enterpriseSelectionPage,
|
||||
},
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
finishAuthUrl: authCompleteUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(enterpriseSelectionPage);
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url', () => {
|
||||
const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...appleProvider,
|
||||
loginUrl,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('button#oa2-apple-id').simulate('click');
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + loginUrl);
|
||||
});
|
||||
|
||||
it('should match third party auth alert', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: 'Apple',
|
||||
platformName: 'edX',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const expectedMessage = 'You have successfully signed into Apple, but your Apple account does not have a '
|
||||
+ 'linked edX account. To link your accounts, sign in now using your edX password.';
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#tpa-alert').find('span').text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display institution login button', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
},
|
||||
});
|
||||
const root = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(root.text().includes('Use my university info')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not display institution login button', () => {
|
||||
const root = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(root.text().includes('Use my university info')).toBe(false);
|
||||
});
|
||||
|
||||
it('should display institution login page', () => {
|
||||
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(loginPage.text().includes('Test University')).toBe(true);
|
||||
});
|
||||
|
||||
it('send tracking event when create account link is clicked', () => {
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('a[href*="/register"]').simulate('click');
|
||||
loginPage.update();
|
||||
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' });
|
||||
});
|
||||
|
||||
it('send page event when login page is rendered', () => {
|
||||
mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
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('check cookie rendered', () => {
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find(<CookiePolicyBanner />)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('form only be scrollable on submission', () => {
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test@example.com', name: 'password' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.find(<IntlLoginFailureMessage />)).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'), search: `?next=/dashboard&tpa_hint=${appleProvider.id}` };
|
||||
appleProvider.iconImage = null;
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
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'), search: '?next=/dashboard&tpa_hint=invalid' };
|
||||
appleProvider.iconImage = null;
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find(`button#${appleProvider.id}`).find('span#provider-name').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'), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
|
||||
secondaryProviders.iconImage = null;
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find(`button#${secondaryProviders.id}`).find('span').text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should redirect to idp page if skipHinetedLogin is true', () => {
|
||||
secondaryProviders.skipHintedLogin = true;
|
||||
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'), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
|
||||
secondaryProviders.iconImage = null;
|
||||
|
||||
mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
|
||||
});
|
||||
|
||||
it('should set login survey cookie', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginResult: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderer.create(reduxWrapper(<IntlLoginPage />));
|
||||
expect(document.cookie).toMatch(`${getConfig().USER_SURVEY_COOKIE_NAME}=login`);
|
||||
});
|
||||
});
|
||||
914
src/legacy/login/tests/__snapshots__/LoginPage.test.jsx.snap
Normal file
914
src/legacy/login/tests/__snapshots__/LoginPage.test.jsx.snap
Normal file
@@ -0,0 +1,914 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoginPage should match TPA provider snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="mw-500"
|
||||
>
|
||||
<p>
|
||||
First time here?
|
||||
<a
|
||||
className="ml-1"
|
||||
href="/register"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Create an account
|
||||
.
|
||||
</a>
|
||||
</p>
|
||||
<hr
|
||||
className="mt-0 border-gray-200"
|
||||
/>
|
||||
<h1
|
||||
className="text-left mt-2 mb-3 h3"
|
||||
>
|
||||
Sign in
|
||||
</h1>
|
||||
<form
|
||||
className="m-0"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="email"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="password"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
className="field-link mt-0 mb-3 small"
|
||||
href="http://localhost:18000/enterprise/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Sign in with your company or school
|
||||
</a>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Sign in
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
className="mb-3"
|
||||
>
|
||||
<hr
|
||||
className="mt-3 mb-3 border-gray-200"
|
||||
/>
|
||||
or sign in with
|
||||
</div>
|
||||
<div
|
||||
className="row tpa-container"
|
||||
>
|
||||
<button
|
||||
className="btn-social btn-oa2-apple-id mr-3"
|
||||
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
||||
id="oa2-apple-id"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="ml-auto"
|
||||
>
|
||||
<img
|
||||
alt="icon Apple"
|
||||
className="icon-image"
|
||||
src="https://edx.devstack.lms/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mr-auto pl-2"
|
||||
id="provider-name"
|
||||
>
|
||||
Apple
|
||||
</span>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Sign in with Apple
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage should match default section snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="mw-500"
|
||||
>
|
||||
<p>
|
||||
First time here?
|
||||
<a
|
||||
className="ml-1"
|
||||
href="/register"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Create an account
|
||||
.
|
||||
</a>
|
||||
</p>
|
||||
<hr
|
||||
className="mt-0 border-gray-200"
|
||||
/>
|
||||
<h1
|
||||
className="text-left mt-2 mb-3 h3"
|
||||
>
|
||||
Sign in
|
||||
</h1>
|
||||
<form
|
||||
className="m-0"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="email"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="password"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
className="field-link mt-0 mb-3 small"
|
||||
href="http://localhost:18000/enterprise/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Sign in with your company or school
|
||||
</a>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Sign in
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage should match forget password alert message snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="mw-500"
|
||||
>
|
||||
<div
|
||||
className="fade alert-content undefined alert alert-success show"
|
||||
id="confirmation-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
Check your email
|
||||
</div>
|
||||
<p>
|
||||
<span>
|
||||
You entered
|
||||
<strong
|
||||
className="data-hj-suppress"
|
||||
>
|
||||
test@example.com
|
||||
</strong>
|
||||
. If this email address is associated with your edX account, we will send a message with password recovery instructions to this email address.
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
If you need further assistance,
|
||||
<a
|
||||
className="alert-link"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
>
|
||||
contact technical support
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
First time here?
|
||||
<a
|
||||
className="ml-1"
|
||||
href="/register"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Create an account
|
||||
.
|
||||
</a>
|
||||
</p>
|
||||
<hr
|
||||
className="mt-0 border-gray-200"
|
||||
/>
|
||||
<h1
|
||||
className="text-left mt-2 mb-3 h3"
|
||||
>
|
||||
Sign in
|
||||
</h1>
|
||||
<form
|
||||
className="m-0"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="email"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="password"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
className="field-link mt-0 mb-3 small"
|
||||
href="http://localhost:18000/enterprise/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Sign in with your company or school
|
||||
</a>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Sign in
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage should match pending button state snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="mw-500"
|
||||
>
|
||||
<p>
|
||||
First time here?
|
||||
<a
|
||||
className="ml-1"
|
||||
href="/register"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Create an account
|
||||
.
|
||||
</a>
|
||||
</p>
|
||||
<hr
|
||||
className="mt-0 border-gray-200"
|
||||
/>
|
||||
<h1
|
||||
className="text-left mt-2 mb-3 h3"
|
||||
>
|
||||
Sign in
|
||||
</h1>
|
||||
<form
|
||||
className="m-0"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="email"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="password"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
className="field-link mt-0 mb-3 small"
|
||||
href="http://localhost:18000/enterprise/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Sign in with your company or school
|
||||
</a>
|
||||
<button
|
||||
aria-disabled={true}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-pending disabled btn btn-brand"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className="pgn__stateful-btn-icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-spinner fa-w-16 fa-spin "
|
||||
data-icon="spinner"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M304 48c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48zm-48 368c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm208-208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zM96 256c0-26.51-21.49-48-48-48S0 229.49 0 256s21.49 48 48 48 48-21.49 48-48zm12.922 99.078c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.491-48-48-48zm294.156 0c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48c0-26.509-21.49-48-48-48zM108.922 60.922c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.491-48-48-48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Sign in
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LoginPage should show error message 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center m-4"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="mw-500"
|
||||
>
|
||||
<div
|
||||
className="fade alert-content undefined alert alert-danger show"
|
||||
id="login-failure-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
We couldn't sign you in.
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
Email or password is incorrect.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
First time here?
|
||||
<a
|
||||
className="ml-1"
|
||||
href="/register"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Create an account
|
||||
.
|
||||
</a>
|
||||
</p>
|
||||
<hr
|
||||
className="mt-0 border-gray-200"
|
||||
/>
|
||||
<h1
|
||||
className="text-left mt-2 mb-3 h3"
|
||||
>
|
||||
Sign in
|
||||
</h1>
|
||||
<form
|
||||
className="m-0"
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="email"
|
||||
name="email"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="email"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label pt-10 focus-out"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
aria-invalid={false}
|
||||
autoComplete="on"
|
||||
className="form-control border-gray-600"
|
||||
id="password"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
required={true}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span />
|
||||
</div>
|
||||
<button
|
||||
className="mt-2 field-link small"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-caret-right fa-w-6 mr-1"
|
||||
data-icon="caret-right"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 192 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Need help signing in?
|
||||
</button>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
className="field-link mt-0 mb-3 small"
|
||||
href="http://localhost:18000/enterprise/login"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Sign in with your company or school
|
||||
</a>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-default btn btn-brand"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Sign in
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
93
src/legacy/register/OptionalFields.jsx
Normal file
93
src/legacy/register/OptionalFields.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { EDUCATION_LEVELS, GENDER_OPTIONS, YEAR_OF_BIRTH_OPTIONS } from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
import { AuthnValidationFormGroup } from '../common-components';
|
||||
|
||||
const OptionalFields = (props) => {
|
||||
const { intl, onChangeHandler, values } = props;
|
||||
|
||||
const getOptions = () => ({
|
||||
yearOfBirthOptions: [{
|
||||
value: '',
|
||||
label: intl.formatMessage(messages['registration.year.of.birth.label']),
|
||||
}].concat(YEAR_OF_BIRTH_OPTIONS),
|
||||
educationLevelOptions: EDUCATION_LEVELS.map(key => ({
|
||||
value: key,
|
||||
label: intl.formatMessage(messages[`registration.field.education.levels.${key || 'label'}`]),
|
||||
})),
|
||||
genderOptions: GENDER_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: intl.formatMessage(messages[`registration.field.gender.options.${key || 'label'}`]),
|
||||
})),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['registration.field.gender.options.label'])}
|
||||
for="gender"
|
||||
name="gender"
|
||||
type="select"
|
||||
key="gender"
|
||||
value={values.gender}
|
||||
className="mb-20 opt-inline-field data-hj-suppress"
|
||||
onChange={(e) => onChangeHandler('gender', e.target.value)}
|
||||
selectOptions={getOptions().genderOptions}
|
||||
inputFieldStyle="border-gray-600 custom-select-size"
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['registration.year.of.birth.label'])}
|
||||
for="yearOfBirth"
|
||||
name="yearOfBirth"
|
||||
type="select"
|
||||
key="yearOfBirth"
|
||||
value={values.yearOfBirth}
|
||||
className="mb-20 opt-inline-field opt-year-field data-hj-suppress"
|
||||
onChange={(e) => onChangeHandler('yearOfBirth', e.target.value)}
|
||||
selectOptions={getOptions().yearOfBirthOptions}
|
||||
inputFieldStyle="border-gray-600 custom-select-size"
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['registration.field.education.levels.label'])}
|
||||
for="levelOfEducation"
|
||||
name="levelOfEducation"
|
||||
type="select"
|
||||
key="levelOfEducation"
|
||||
value={values.levelOfEducation}
|
||||
className="mb-20 data-hj-suppress"
|
||||
onChange={(e) => onChangeHandler('levelOfEducation', e.target.value)}
|
||||
selectOptions={getOptions().educationLevelOptions}
|
||||
inputFieldStyle="border-gray-600 custom-select-size"
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['registration.goals.label'])}
|
||||
for="goals"
|
||||
name="goals"
|
||||
type="textarea"
|
||||
key="goals"
|
||||
value={values.goals}
|
||||
className="mb-20"
|
||||
onChange={(e) => onChangeHandler('goals', e.target.value)}
|
||||
inputFieldStyle="border-gray-600 custom-select-size"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
OptionalFields.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
values: PropTypes.shape({
|
||||
gender: PropTypes.string,
|
||||
goals: PropTypes.string,
|
||||
levelOfEducation: PropTypes.string,
|
||||
yearOfBirth: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(OptionalFields);
|
||||
86
src/legacy/register/RegistrationFailure.jsx
Normal file
86
src/legacy/register/RegistrationFailure.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './data/constants';
|
||||
import messages from './messages';
|
||||
import { DEFAULT_STATE, PENDING_STATE } from '../data/constants';
|
||||
import { windowScrollTo } from '../data/utils';
|
||||
|
||||
const RegistrationFailureMessage = (props) => {
|
||||
const errorMessage = props.errors;
|
||||
const { errorCode } = props.errors;
|
||||
const userErrors = [];
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isSubmitted && props.submitButtonState !== PENDING_STATE) {
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
let serverError;
|
||||
switch (errorCode) {
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
serverError = (
|
||||
<li key={INTERNAL_SERVER_ERROR} className="text-left">
|
||||
{props.intl.formatMessage(messages['registration.request.server.error'])}
|
||||
</li>
|
||||
);
|
||||
userErrors.push(serverError);
|
||||
break;
|
||||
case FORBIDDEN_REQUEST:
|
||||
userErrors.push(
|
||||
(
|
||||
<li key={FORBIDDEN_REQUEST} className="text-left">
|
||||
{props.intl.formatMessage(messages['register.rate.limit.reached.message'])}
|
||||
</li>
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
Object.keys(errorMessage).forEach((key) => {
|
||||
if (key !== 'error_code') {
|
||||
const errors = errorMessage[key];
|
||||
const suppressionClass = ['email', 'username'].includes(key) ? 'data-hj-suppress' : '';
|
||||
const errorList = errors.map((error) => (
|
||||
(error.user_message) ? (
|
||||
<li key={error} className={`text-left ${suppressionClass}`}>
|
||||
{error.user_message}
|
||||
</li>
|
||||
) : null
|
||||
));
|
||||
userErrors.push(errorList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
!userErrors.length ? null : (
|
||||
<Alert id="validation-errors" variant="danger">
|
||||
<Alert.Heading>{props.intl.formatMessage(messages['registration.request.failure.header'])}</Alert.Heading>
|
||||
<ul>{userErrors}</ul>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
RegistrationFailureMessage.defaultProps = {
|
||||
errors: '',
|
||||
submitButtonState: DEFAULT_STATE,
|
||||
isSubmitted: false,
|
||||
};
|
||||
|
||||
RegistrationFailureMessage.propTypes = {
|
||||
errors: PropTypes.shape({
|
||||
email: PropTypes.array,
|
||||
username: PropTypes.array,
|
||||
errorCode: PropTypes.string,
|
||||
}),
|
||||
submitButtonState: PropTypes.string,
|
||||
isSubmitted: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RegistrationFailureMessage);
|
||||
783
src/legacy/register/RegistrationPage.jsx
Normal file
783
src/legacy/register/RegistrationPage.jsx
Normal file
@@ -0,0 +1,783 @@
|
||||
import React from 'react';
|
||||
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { connect } from 'react-redux';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
injectIntl, intlShape, getCountryList, getLocale, FormattedMessage,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Form, Hyperlink, StatefulButton } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { registerNewUser, fetchRealtimeValidations } from './data/actions';
|
||||
import { registrationRequestSelector } from './data/selectors';
|
||||
import messages from './messages';
|
||||
import OptionalFields from './OptionalFields';
|
||||
import RegistrationFailure from './RegistrationFailure';
|
||||
|
||||
import {
|
||||
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
|
||||
InstitutionLogistration, AuthnValidationFormGroup,
|
||||
} from '../common-components';
|
||||
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import {
|
||||
DEFAULT_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getTpaProvider, getTpaHint, updatePathWithQueryParams, getAllPossibleQueryParam, setSurveyCookie,
|
||||
} from '../data/utils';
|
||||
|
||||
class RegistrationPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
this.intl = props.intl;
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
this.tpaHint = getTpaHint();
|
||||
|
||||
this.state = {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
gender: '',
|
||||
yearOfBirth: '',
|
||||
goals: '',
|
||||
levelOfEducation: '',
|
||||
enableOptionalField: false,
|
||||
validationAlertMessages: {
|
||||
name: [{ user_message: '' }],
|
||||
username: [{ user_message: '' }],
|
||||
email: [{ user_message: '' }],
|
||||
password: [{ user_message: '' }],
|
||||
country: [{ user_message: '' }],
|
||||
},
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
institutionLogin: false,
|
||||
formValid: false,
|
||||
startTime: Date.now(),
|
||||
updateFieldErrors: false,
|
||||
updateAlertErrors: false,
|
||||
registrationErrorsUpdated: false,
|
||||
optimizelyExperimentName: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const payload = { ...this.queryParams };
|
||||
|
||||
if (this.tpaHint) {
|
||||
payload.tpa_hint = this.tpaHint;
|
||||
}
|
||||
this.props.getThirdPartyAuthContext(payload);
|
||||
this.getExperiments();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.statusCode !== 403 && this.props.validations !== nextProps.validations) {
|
||||
const { errors } = this.state;
|
||||
const { fieldName } = this.state;
|
||||
const errorMsg = nextProps.validations.validation_decisions[fieldName];
|
||||
errors[fieldName] = errorMsg;
|
||||
this.setState({
|
||||
errors,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.thirdPartyAuthContext.pipelineUserDetails !== nextProps.thirdPartyAuthContext.pipelineUserDetails) {
|
||||
this.setState({
|
||||
...nextProps.thirdPartyAuthContext.pipelineUserDetails,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.registrationError !== nextProps.registrationError) {
|
||||
this.setState({
|
||||
formValid: false,
|
||||
registrationErrorsUpdated: true,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.registrationErrorsUpdated && this.props.registrationError === nextProps.registrationError) {
|
||||
this.setState({
|
||||
formValid: false,
|
||||
registrationErrorsUpdated: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getExperiments = () => {
|
||||
const { optimizelyExperimentName } = window;
|
||||
|
||||
if (optimizelyExperimentName) {
|
||||
this.setState({ optimizelyExperimentName });
|
||||
}
|
||||
};
|
||||
|
||||
getCountryOptions = () => {
|
||||
const { intl } = this.props;
|
||||
return [{
|
||||
value: '',
|
||||
label: intl.formatMessage(messages['registration.country.label']),
|
||||
}].concat(getCountryList(getLocale()).map(({ code, name }) => ({ value: code, label: name })));
|
||||
}
|
||||
|
||||
getOptionalFields() {
|
||||
const values = {};
|
||||
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',');
|
||||
optionalFields.forEach((key) => {
|
||||
values[camelCase(key)] = this.state[camelCase(key)];
|
||||
});
|
||||
|
||||
return (
|
||||
<OptionalFields
|
||||
values={values}
|
||||
onChangeHandler={(fieldName, value) => { this.setState({ [fieldName]: value }); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleInstitutionLogin = () => {
|
||||
this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin }));
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const totalRegistrationTime = (Date.now() - this.state.startTime) / 1000;
|
||||
let payload = {
|
||||
name: this.state.name,
|
||||
username: this.state.username,
|
||||
email: this.state.email,
|
||||
country: this.state.country,
|
||||
honor_code: true,
|
||||
};
|
||||
|
||||
if (this.props.thirdPartyAuthContext.currentProvider) {
|
||||
payload.social_auth_provider = this.props.thirdPartyAuthContext.currentProvider;
|
||||
} else {
|
||||
payload.password = this.state.password;
|
||||
}
|
||||
|
||||
const postParams = getAllPossibleQueryParam();
|
||||
payload = { ...payload, ...postParams };
|
||||
|
||||
let finalValidation = this.state.formValid;
|
||||
if (!this.state.formValid) {
|
||||
Object.keys(payload).forEach(key => {
|
||||
finalValidation = this.validateInput(key, payload[key], payload);
|
||||
});
|
||||
}
|
||||
// Since optional fields are not validated we can add it to payload after required fields
|
||||
// have been validated. This will save us unwanted calls to validateInput()
|
||||
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',');
|
||||
optionalFields.forEach((key) => {
|
||||
const stateKey = camelCase(key);
|
||||
if (this.state[stateKey]) {
|
||||
payload[key] = this.state[stateKey];
|
||||
}
|
||||
});
|
||||
if (finalValidation) {
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
this.props.registerNewUser(payload);
|
||||
}
|
||||
}
|
||||
|
||||
checkNoFieldErrors(validations) {
|
||||
const keyValidList = Object.entries(validations).map(([key]) => !validations[key]);
|
||||
return keyValidList.every((current) => current === true);
|
||||
}
|
||||
|
||||
checkNoAlertErrors(validations) {
|
||||
const keyValidList = Object.entries(validations).map(([key]) => {
|
||||
const validation = validations[key][0];
|
||||
return !validation.user_message;
|
||||
});
|
||||
return keyValidList.every((current) => current === true);
|
||||
}
|
||||
|
||||
handleOnBlur(e) {
|
||||
const payload = {
|
||||
email: this.state.email,
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
name: this.state.name,
|
||||
honor_code: true,
|
||||
country: this.state.country,
|
||||
};
|
||||
const { name, value } = e.target;
|
||||
this.setState({
|
||||
updateFieldErrors: false,
|
||||
updateAlertErrors: false,
|
||||
fieldName: e.target.name,
|
||||
}, () => {
|
||||
this.validateInput(name, value, payload, false);
|
||||
});
|
||||
}
|
||||
|
||||
handleOnChange(e) {
|
||||
if (!(e.target.name === 'username' && e.target.value.length > 30)) {
|
||||
this.setState({
|
||||
[e.target.name]: e.target.value,
|
||||
updateFieldErrors: false,
|
||||
updateAlertErrors: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOnFocus(e) {
|
||||
const { errors } = this.state;
|
||||
errors[e.target.name] = '';
|
||||
this.setState({ errors });
|
||||
}
|
||||
|
||||
handleOnOptional(e) {
|
||||
const optionalEnable = this.state.enableOptionalField;
|
||||
const targetValue = e.target.id === 'additionalFields' ? !optionalEnable : e.target.checked;
|
||||
this.setState({
|
||||
enableOptionalField: targetValue,
|
||||
updateAlertErrors: false,
|
||||
updateFieldErrors: false,
|
||||
});
|
||||
sendTrackEvent('edx.bi.user.register.optional_fields_selected', {});
|
||||
}
|
||||
|
||||
handleLoginLinkClickEvent() {
|
||||
sendTrackEvent('edx.bi.login_form.toggled', { category: 'user-engagement' });
|
||||
}
|
||||
|
||||
validateInput(inputName, value, payload, updateAlertMessage = true) {
|
||||
const { errors } = this.state;
|
||||
const { intl, statusCode } = this.props;
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
|
||||
let {
|
||||
formValid,
|
||||
updateFieldErrors,
|
||||
updateAlertErrors,
|
||||
} = this.state;
|
||||
switch (inputName) {
|
||||
case 'email':
|
||||
if (value.length < 1) {
|
||||
errors.email = intl.formatMessage(messages['email.validation.message']);
|
||||
} else if (value.length <= 2) {
|
||||
errors.email = intl.formatMessage(messages['email.ratelimit.less.chars.validation.message']);
|
||||
} else if (!emailRegex.test(value)) {
|
||||
errors.email = intl.formatMessage(messages['email.ratelimit.incorrect.format.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.email = '';
|
||||
}
|
||||
break;
|
||||
case 'name':
|
||||
if (value.length < 1) {
|
||||
errors.name = intl.formatMessage(messages['fullname.validation.message']);
|
||||
} else {
|
||||
errors.name = '';
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (value.length < 1) {
|
||||
errors.username = intl.formatMessage(messages['username.validation.message']);
|
||||
} else if (value.length <= 1 || value.length > 30) {
|
||||
errors.username = intl.formatMessage(messages['username.ratelimit.less.chars.message']);
|
||||
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
|
||||
errors.username = intl.formatMessage(messages['username.format.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.username = '';
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
if (value.length < 1) {
|
||||
errors.password = intl.formatMessage(messages['register.page.password.validation.message']);
|
||||
} else if (value.length < 8) {
|
||||
errors.password = intl.formatMessage(messages['email.ratelimit.password.validation.message']);
|
||||
} else if (!value.match(/.*[0-9].*/i)) {
|
||||
errors.password = intl.formatMessage(messages['username.number.validation.message']);
|
||||
} else if (!value.match(/.*[a-zA-Z].*/i)) {
|
||||
errors.password = intl.formatMessage(messages['username.character.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.password = '';
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
if (!value) {
|
||||
errors.country = intl.formatMessage(messages['country.validation.message']);
|
||||
} else {
|
||||
errors.country = '';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateAlertMessage) {
|
||||
updateFieldErrors = true;
|
||||
updateAlertErrors = true;
|
||||
formValid = this.checkNoFieldErrors(errors);
|
||||
}
|
||||
this.setState({
|
||||
formValid,
|
||||
updateFieldErrors,
|
||||
updateAlertErrors,
|
||||
errors,
|
||||
});
|
||||
return formValid;
|
||||
}
|
||||
|
||||
updateFieldErrors(registrationError) {
|
||||
const {
|
||||
errors,
|
||||
} = this.state;
|
||||
Object.entries(registrationError).map(([key]) => {
|
||||
if (registrationError[key]) {
|
||||
errors[key] = registrationError[key][0].user_message;
|
||||
}
|
||||
return errors;
|
||||
});
|
||||
}
|
||||
|
||||
updateValidationAlertMessages() {
|
||||
const {
|
||||
errors,
|
||||
validationAlertMessages,
|
||||
} = this.state;
|
||||
Object.entries(errors).map(([key, value]) => {
|
||||
if (validationAlertMessages[key]) {
|
||||
validationAlertMessages[key][0].user_message = value;
|
||||
}
|
||||
return validationAlertMessages;
|
||||
});
|
||||
}
|
||||
|
||||
renderErrors() {
|
||||
let errorsObject = null;
|
||||
let { registrationErrorsUpdated } = this.state;
|
||||
const {
|
||||
updateAlertErrors,
|
||||
updateFieldErrors,
|
||||
validationAlertMessages,
|
||||
} = this.state;
|
||||
const { registrationError, submitState } = this.props;
|
||||
if (registrationError && registrationErrorsUpdated) {
|
||||
if (updateFieldErrors && submitState !== PENDING_STATE) {
|
||||
this.updateFieldErrors(registrationError);
|
||||
}
|
||||
registrationErrorsUpdated = false;
|
||||
errorsObject = registrationError;
|
||||
} else {
|
||||
if (updateAlertErrors && submitState !== PENDING_STATE) {
|
||||
this.updateValidationAlertMessages();
|
||||
}
|
||||
errorsObject = !this.checkNoAlertErrors(validationAlertMessages) ? validationAlertMessages : {};
|
||||
}
|
||||
return (
|
||||
<RegistrationFailure
|
||||
errors={errorsObject}
|
||||
isSubmitted={updateAlertErrors}
|
||||
submitButtonState={submitState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 tpa-container">
|
||||
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
thirdPartyComponent = <Skeleton height={36} count={2} />;
|
||||
}
|
||||
return thirdPartyComponent;
|
||||
}
|
||||
|
||||
renderForm(currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl) {
|
||||
if (this.state.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'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.registrationResult.success) {
|
||||
setSurveyCookie('register');
|
||||
window.optimizely = window.optimizely || [];
|
||||
|
||||
// Fire optimizely events
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'van_504_total_registrations',
|
||||
});
|
||||
|
||||
if (this.state.optimizelyExperimentName !== 'progressiveProfilingConcept1') {
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'van_504_conversion_rate',
|
||||
});
|
||||
['yearOfBirth', 'gender', 'levelOfEducation'].forEach(fieldName => {
|
||||
if (this.state[fieldName]) {
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: `van_504_${fieldName}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['register.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={this.props.registrationResult.success}
|
||||
redirectUrl={this.props.registrationResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
redirectToWelcomePage={this.state.optimizelyExperimentName === 'progressiveProfilingConcept1'}
|
||||
/>
|
||||
<div className="d-flex justify-content-center m-4">
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-500">
|
||||
{this.renderErrors()}
|
||||
{currentProvider && (
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={this.props.thirdPartyAuthContext.platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
{intl.formatMessage(messages['already.have.an.edx.account'])}
|
||||
<Hyperlink
|
||||
className="ml-1"
|
||||
destination={updatePathWithQueryParams(LOGIN_PAGE)}
|
||||
onClick={this.handleLoginLinkClickEvent}
|
||||
>
|
||||
{intl.formatMessage(messages['sign.in.hyperlink'])}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<hr className="mb-3 border-gray-200" />
|
||||
<h1 className="mb-3 h3">{intl.formatMessage(messages['create.a.new.account'])}</h1>
|
||||
<Form className="form-group">
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['fullname.label'])}
|
||||
for="name"
|
||||
name="name"
|
||||
type="text"
|
||||
invalid={this.state.errors.name !== ''}
|
||||
ariaInvalid={this.state.errors.name !== ''}
|
||||
invalidMessage={this.state.errors.name}
|
||||
value={this.state.name}
|
||||
onBlur={(e) => this.handleOnBlur(e)}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
onFocus={(e) => this.handleOnFocus(e)}
|
||||
helpText={intl.formatMessage(messages['helptext.name'])}
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['username.label'])}
|
||||
for="username"
|
||||
name="username"
|
||||
type="text"
|
||||
className="data-hj-suppress"
|
||||
invalid={this.state.errors.username !== ''}
|
||||
ariaInvalid={this.state.errors.username !== ''}
|
||||
invalidMessage={this.state.errors.username}
|
||||
value={this.state.username}
|
||||
onBlur={(e) => this.handleOnBlur(e)}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
onFocus={(e) => this.handleOnFocus(e)}
|
||||
helpText={intl.formatMessage(messages['helptext.username'])}
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['register.page.email.label'])}
|
||||
for="email"
|
||||
name="email"
|
||||
type="text"
|
||||
className="data-hj-suppress"
|
||||
invalid={this.state.errors.email !== ''}
|
||||
ariaInvalid={this.state.errors.email !== ''}
|
||||
invalidMessage={this.state.errors.email}
|
||||
value={this.state.email}
|
||||
onBlur={(e) => this.handleOnBlur(e)}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
onFocus={(e) => this.handleOnFocus(e)}
|
||||
helpText={intl.formatMessage(messages['helptext.email'])}
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
{!currentProvider && (
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['password.label'])}
|
||||
for="password"
|
||||
name="password"
|
||||
type="password"
|
||||
invalid={this.state.errors.password !== ''}
|
||||
ariaInvalid={this.state.errors.password !== ''}
|
||||
invalidMessage={this.state.errors.password}
|
||||
value={this.state.password}
|
||||
onBlur={(e) => this.handleOnBlur(e)}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
onFocus={(e) => this.handleOnFocus(e)}
|
||||
helpText={intl.formatMessage(messages['helptext.password'])}
|
||||
inputFieldStyle="border-gray-600"
|
||||
/>
|
||||
)}
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['registration.country.label'])}
|
||||
for="country"
|
||||
name="country"
|
||||
type="select"
|
||||
key="country"
|
||||
invalid={this.state.errors.country !== ''}
|
||||
ariaInvalid={this.state.errors.country !== ''}
|
||||
invalidMessage={intl.formatMessage(messages['country.validation.message'])}
|
||||
className="mb-0 data-hj-suppress"
|
||||
value={this.state.country}
|
||||
onBlur={(e) => this.handleOnBlur(e)}
|
||||
onChange={(e) => this.handleOnChange(e)}
|
||||
onFocus={(e) => this.handleOnFocus(e)}
|
||||
selectOptions={this.getCountryOptions()}
|
||||
inputFieldStyle="border-gray-600 custom-select-size"
|
||||
/>
|
||||
<div id="honor-code" className="pt-10 small">
|
||||
<FormattedMessage
|
||||
id="register.page.terms.of.service.and.honor.code"
|
||||
tagName="p"
|
||||
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
|
||||
Member process your personal data in accordance with the {privacyPolicy}."
|
||||
description="Text that appears on registration form stating honor code and privacy policy"
|
||||
values={{
|
||||
platformName: getConfig().SITE_NAME,
|
||||
tosAndHonorCode: (
|
||||
<Hyperlink destination={getConfig().TOS_AND_HONOR_CODE} target="_blank">
|
||||
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
privacyPolicy: (
|
||||
<Hyperlink destination={getConfig().PRIVACY_POLICY} target="_blank">
|
||||
{intl.formatMessage(messages['privacy.policy'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{getConfig().REGISTRATION_OPTIONAL_FIELDS && this.state.optimizelyExperimentName !== 'progressiveProfilingConcept1' ? (
|
||||
<AuthnValidationFormGroup
|
||||
label={intl.formatMessage(messages['support.education.research'])}
|
||||
for="optional"
|
||||
name="optional"
|
||||
type="checkbox"
|
||||
value={this.state.enableOptionalField}
|
||||
onClick={(e) => this.handleOnOptional(e)}
|
||||
onBlur={null}
|
||||
onChange={(e) => this.handleOnOptional(e)}
|
||||
optionalFieldCheckbox
|
||||
isChecked={this.state.enableOptionalField}
|
||||
checkboxMessage={intl.formatMessage(messages['support.education.research'])}
|
||||
/>
|
||||
) : null}
|
||||
{ this.state.enableOptionalField ? this.getOptionalFields() : null }
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
state={submitState}
|
||||
className="mt-3"
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['create.account.button']),
|
||||
}}
|
||||
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
|
||||
onClick={this.handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
|
||||
&& !currentProvider ? (
|
||||
<div className="d-block mb-4 mt-4">
|
||||
<hr className="mt-0 border-gray-200" />
|
||||
<span className="d-block mb-4 text-left">
|
||||
{intl.formatMessage(messages['create.an.account.using'])}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{this.renderThirdPartyAuth(providers,
|
||||
secondaryProviders,
|
||||
currentProvider,
|
||||
thirdPartyAuthApiStatus,
|
||||
intl)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, submitState, thirdPartyAuthApiStatus } = this.props;
|
||||
const {
|
||||
currentProvider, finishAuthUrl, providers, secondaryProviders,
|
||||
} = this.props.thirdPartyAuthContext;
|
||||
|
||||
if (this.tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders);
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? (<EnterpriseSSO provider={provider} intl={intl} />)
|
||||
: this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
return this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RegistrationPage.defaultProps = {
|
||||
registrationResult: null,
|
||||
registerNewUser: null,
|
||||
registrationError: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
validations: null,
|
||||
statusCode: null,
|
||||
};
|
||||
|
||||
RegistrationPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func,
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
registrationError: PropTypes.shape({
|
||||
email: PropTypes.array,
|
||||
username: PropTypes.array,
|
||||
country: PropTypes.array,
|
||||
password: PropTypes.array,
|
||||
name: PropTypes.array,
|
||||
}),
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
pipelineUserDetails: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
fullname: PropTypes.string,
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
fetchRealtimeValidations: PropTypes.func.isRequired,
|
||||
validations: PropTypes.shape({
|
||||
validation_decisions: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
statusCode: PropTypes.number,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const registrationResult = registrationRequestSelector(state);
|
||||
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
|
||||
return {
|
||||
registrationError: state.register.registrationError,
|
||||
submitState: state.register.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
registrationResult,
|
||||
thirdPartyAuthContext,
|
||||
validations: state.register.validations,
|
||||
statusCode: state.register.statusCode,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
getThirdPartyAuthContext,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
},
|
||||
)(injectIntl(RegistrationPage));
|
||||
45
src/legacy/register/data/actions.js
Normal file
45
src/legacy/register/data/actions.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const REGISTER_FORM_VALIDATIONS = new AsyncActionType('REGISTRATION', 'GET_FORM_VALIDATIONS');
|
||||
|
||||
// Register
|
||||
export const registerNewUser = registrationInfo => ({
|
||||
type: REGISTER_NEW_USER.BASE,
|
||||
payload: { registrationInfo },
|
||||
});
|
||||
|
||||
export const registerNewUserBegin = () => ({
|
||||
type: REGISTER_NEW_USER.BEGIN,
|
||||
});
|
||||
|
||||
export const registerNewUserSuccess = (redirectUrl, success) => ({
|
||||
type: REGISTER_NEW_USER.SUCCESS,
|
||||
payload: { redirectUrl, success },
|
||||
|
||||
});
|
||||
|
||||
export const registerNewUserFailure = (error) => ({
|
||||
type: REGISTER_NEW_USER.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
// Realtime Field validations
|
||||
export const fetchRealtimeValidations = (formPayload) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsBegin = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsSuccess = (validations) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
|
||||
payload: { validations },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsFailure = (error, statusCode) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.FAILURE,
|
||||
payload: { error, statusCode },
|
||||
});
|
||||
30
src/legacy/register/data/constants.js
Normal file
30
src/legacy/register/data/constants.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Registration Error Codes
|
||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||
export const FORBIDDEN_REQUEST = 'forbidden-request';
|
||||
|
||||
export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
let startYear = currentYear - 120;
|
||||
while (startYear < currentYear) {
|
||||
startYear += 1;
|
||||
|
||||
years.push({ value: startYear.toString(), label: startYear });
|
||||
}
|
||||
return years.reverse();
|
||||
})();
|
||||
|
||||
export const EDUCATION_LEVELS = [
|
||||
'',
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'other',
|
||||
];
|
||||
|
||||
export const GENDER_OPTIONS = ['', 'f', 'm', 'o'];
|
||||
51
src/legacy/register/data/reducers.js
Normal file
51
src/legacy/register/data/reducers.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { REGISTER_NEW_USER, REGISTER_FORM_VALIDATIONS } from './actions';
|
||||
|
||||
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
export const defaultState = {
|
||||
registrationError: null,
|
||||
registrationResult: {},
|
||||
formData: null,
|
||||
validations: null,
|
||||
statusCode: null,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case REGISTER_NEW_USER.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
submitState: PENDING_STATE,
|
||||
};
|
||||
case REGISTER_NEW_USER.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
registrationResult: action.payload,
|
||||
};
|
||||
case REGISTER_NEW_USER.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
registrationError: action.payload.error,
|
||||
submitState: DEFAULT_STATE,
|
||||
};
|
||||
case REGISTER_FORM_VALIDATIONS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case REGISTER_FORM_VALIDATIONS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
validations: action.payload.validations,
|
||||
};
|
||||
case REGISTER_FORM_VALIDATIONS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
validations: action.payload.error,
|
||||
statusCode: action.payload.statusCode,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
69
src/legacy/register/data/sagas.js
Normal file
69
src/legacy/register/data/sagas.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
REGISTER_NEW_USER,
|
||||
registerNewUserBegin,
|
||||
registerNewUserFailure,
|
||||
registerNewUserSuccess,
|
||||
REGISTER_FORM_VALIDATIONS,
|
||||
fetchRealtimeValidationsBegin,
|
||||
fetchRealtimeValidationsSuccess,
|
||||
fetchRealtimeValidationsFailure,
|
||||
} from './actions';
|
||||
import { INTERNAL_SERVER_ERROR } from './constants';
|
||||
|
||||
// Services
|
||||
import { getFieldsValidations, registerRequest } from './service';
|
||||
|
||||
export function* handleNewUserRegistration(action) {
|
||||
try {
|
||||
yield put(registerNewUserBegin());
|
||||
|
||||
const { redirectUrl, success } = yield call(registerRequest, action.payload.registrationInfo);
|
||||
|
||||
yield put(registerNewUserSuccess(
|
||||
redirectUrl,
|
||||
success,
|
||||
));
|
||||
} catch (e) {
|
||||
const statusCodes = [400, 409];
|
||||
if (e.response && statusCodes.includes(e.response.status)) {
|
||||
yield put(registerNewUserFailure(e.response.data));
|
||||
logInfo(e);
|
||||
} else if (e.response.status === 403) {
|
||||
yield put(registerNewUserFailure(camelCaseObject(e.response.data)));
|
||||
logInfo(e);
|
||||
} else {
|
||||
yield put(registerNewUserFailure({ errorCode: INTERNAL_SERVER_ERROR }));
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchRealtimeValidations(action) {
|
||||
try {
|
||||
yield put(fetchRealtimeValidationsBegin());
|
||||
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
|
||||
|
||||
yield put(fetchRealtimeValidationsSuccess(
|
||||
fieldValidations,
|
||||
));
|
||||
} catch (e) {
|
||||
const statusCodes = [403];
|
||||
if (e.response && statusCodes.includes(e.response.status)) {
|
||||
yield put(fetchRealtimeValidationsFailure(e.response.data, e.response.status));
|
||||
logInfo(e);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
|
||||
yield takeEvery(REGISTER_FORM_VALIDATIONS.BASE, fetchRealtimeValidations);
|
||||
}
|
||||
10
src/legacy/register/data/selectors.js
Normal file
10
src/legacy/register/data/selectors.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const storeName = 'register';
|
||||
|
||||
export const registerSelector = state => ({ ...state[storeName] });
|
||||
|
||||
export const registrationRequestSelector = createSelector(
|
||||
registerSelector,
|
||||
register => register.registrationResult,
|
||||
);
|
||||
45
src/legacy/register/data/service.js
Normal file
45
src/legacy/register/data/service.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getHttpClient, getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import querystring from 'querystring';
|
||||
|
||||
export async function registerRequest(registrationInformation) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v2/account/registration/`,
|
||||
querystring.stringify(registrationInformation),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
|
||||
return {
|
||||
redirectUrl: data.redirect_url || `${getConfig().LMS_BASE_URL}/dashboard`,
|
||||
success: data.success || false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getFieldsValidations(formPayload) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
};
|
||||
|
||||
const { data } = await getHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/validation/registration`,
|
||||
querystring.stringify(formPayload),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
|
||||
return {
|
||||
fieldValidations: data,
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user