+ >
+ );
+};
+
+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);
diff --git a/src/common-components/NotFoundPage.jsx b/src/legacy/common-components/NotFoundPage.jsx
similarity index 100%
rename from src/common-components/NotFoundPage.jsx
rename to src/legacy/common-components/NotFoundPage.jsx
diff --git a/src/common-components/RedirectLogistration.jsx b/src/legacy/common-components/RedirectLogistration.jsx
similarity index 100%
rename from src/common-components/RedirectLogistration.jsx
rename to src/legacy/common-components/RedirectLogistration.jsx
diff --git a/src/common-components/RegisterFaIcons.jsx b/src/legacy/common-components/RegisterFaIcons.jsx
similarity index 100%
rename from src/common-components/RegisterFaIcons.jsx
rename to src/legacy/common-components/RegisterFaIcons.jsx
diff --git a/src/legacy/common-components/SocialAuthProviders.jsx b/src/legacy/common-components/SocialAuthProviders.jsx
new file mode 100644
index 00000000..ad29ce51
--- /dev/null
+++ b/src/legacy/common-components/SocialAuthProviders.jsx
@@ -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) => (
+
+ ));
+
+ 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);
diff --git a/src/common-components/SwitchContent.jsx b/src/legacy/common-components/SwitchContent.jsx
similarity index 100%
rename from src/common-components/SwitchContent.jsx
rename to src/legacy/common-components/SwitchContent.jsx
diff --git a/src/legacy/common-components/ThirdPartyAuthAlert.jsx b/src/legacy/common-components/ThirdPartyAuthAlert.jsx
new file mode 100644
index 00000000..0504a9f2
--- /dev/null
+++ b/src/legacy/common-components/ThirdPartyAuthAlert.jsx
@@ -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 = (
+
+ );
+ } else {
+ message = (
+
+ );
+ }
+
+ return { message };
+};
+
+ThirdPartyAuthAlert.defaultProps = {
+ referrer: LOGIN_PAGE,
+};
+
+ThirdPartyAuthAlert.propTypes = {
+ currentProvider: PropTypes.string.isRequired,
+ platformName: PropTypes.string.isRequired,
+ referrer: PropTypes.string,
+};
+
+export default ThirdPartyAuthAlert;
diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/legacy/common-components/UnAuthOnlyRoute.jsx
similarity index 100%
rename from src/common-components/UnAuthOnlyRoute.jsx
rename to src/legacy/common-components/UnAuthOnlyRoute.jsx
diff --git a/src/common-components/data/actions.js b/src/legacy/common-components/data/actions.js
similarity index 100%
rename from src/common-components/data/actions.js
rename to src/legacy/common-components/data/actions.js
diff --git a/src/common-components/data/reducers.js b/src/legacy/common-components/data/reducers.js
similarity index 100%
rename from src/common-components/data/reducers.js
rename to src/legacy/common-components/data/reducers.js
diff --git a/src/common-components/data/sagas.js b/src/legacy/common-components/data/sagas.js
similarity index 100%
rename from src/common-components/data/sagas.js
rename to src/legacy/common-components/data/sagas.js
diff --git a/src/common-components/data/selectors.js b/src/legacy/common-components/data/selectors.js
similarity index 100%
rename from src/common-components/data/selectors.js
rename to src/legacy/common-components/data/selectors.js
diff --git a/src/legacy/common-components/data/service.js b/src/legacy/common-components/data/service.js
new file mode 100644
index 00000000..566a6fb8
--- /dev/null
+++ b/src/legacy/common-components/data/service.js
@@ -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' })),
+ };
+}
diff --git a/src/common-components/data/tests/sagas.test.js b/src/legacy/common-components/data/tests/sagas.test.js
similarity index 96%
rename from src/common-components/data/tests/sagas.test.js
rename to src/legacy/common-components/data/tests/sagas.test.js
index fe0feb8a..ffbc33da 100644
--- a/src/common-components/data/tests/sagas.test.js
+++ b/src/legacy/common-components/data/tests/sagas.test.js
@@ -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();
diff --git a/src/legacy/common-components/index.jsx b/src/legacy/common-components/index.jsx
new file mode 100644
index 00000000..20894220
--- /dev/null
+++ b/src/legacy/common-components/index.jsx
@@ -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';
diff --git a/src/legacy/common-components/messages.jsx b/src/legacy/common-components/messages.jsx
new file mode 100644
index 00000000..d667457d
--- /dev/null
+++ b/src/legacy/common-components/messages.jsx
@@ -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;
diff --git a/src/legacy/common-components/tests/AuthnValidationFormGroup.test.jsx b/src/legacy/common-components/tests/AuthnValidationFormGroup.test.jsx
new file mode 100644
index 00000000..6a2eeebb
--- /dev/null
+++ b/src/legacy/common-components/tests/AuthnValidationFormGroup.test.jsx
@@ -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();
+ 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();
+
+ 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();
+ 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();
+ expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label sr-only');
+ });
+});
diff --git a/src/legacy/common-components/tests/ConfirmationAlert.test.jsx b/src/legacy/common-components/tests/ConfirmationAlert.test.jsx
new file mode 100644
index 00000000..b80f1dbb
--- /dev/null
+++ b/src/legacy/common-components/tests/ConfirmationAlert.test.jsx
@@ -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(
+
+
+ ,
+ );
+
+ 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);
+ });
+});
diff --git a/src/common-components/tests/SocialAuthProviders.test.jsx b/src/legacy/common-components/tests/SocialAuthProviders.test.jsx
similarity index 100%
rename from src/common-components/tests/SocialAuthProviders.test.jsx
rename to src/legacy/common-components/tests/SocialAuthProviders.test.jsx
diff --git a/src/legacy/common-components/tests/ThirdPartyAuthAlert.test.jsx b/src/legacy/common-components/tests/ThirdPartyAuthAlert.test.jsx
new file mode 100644
index 00000000..548bb513
--- /dev/null
+++ b/src/legacy/common-components/tests/ThirdPartyAuthAlert.test.jsx
@@ -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(
+
+
+ ,
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('should match register page third party auth alert message snapshot', () => {
+ props = {
+ ...props,
+ referrer: 'register',
+ };
+
+ const tree = renderer.create(
+
+
+ ,
+ ).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/src/common-components/tests/UnAuthOnlyRoute.test.jsx b/src/legacy/common-components/tests/UnAuthOnlyRoute.test.jsx
similarity index 100%
rename from src/common-components/tests/UnAuthOnlyRoute.test.jsx
rename to src/legacy/common-components/tests/UnAuthOnlyRoute.test.jsx
diff --git a/src/legacy/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap b/src/legacy/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap
new file mode 100644
index 00000000..972437cf
--- /dev/null
+++ b/src/legacy/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap
@@ -0,0 +1,156 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SocialAuthProviders should match social auth provider with default icon snapshot 1`] = `
+
+`;
+
+exports[`SocialAuthProviders should match social auth provider with iconClass snapshot 1`] = `
+
+`;
+
+exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
+Array [
+ ,
+ ,
+]
+`;
diff --git a/src/legacy/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap b/src/legacy/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap
new file mode 100644
index 00000000..c38d0308
--- /dev/null
+++ b/src/legacy/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
+
+
+
+ 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.
+
+
+
+`;
+
+exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
+
+
+
+ You've successfully signed into Google. We just need a little more information before you start learning with edX.
+
+
+
+`;
diff --git a/src/data/configureStore.js b/src/legacy/data/configureStore.js
similarity index 100%
rename from src/data/configureStore.js
rename to src/legacy/data/configureStore.js
diff --git a/src/legacy/data/constants.js b/src/legacy/data/constants.js
new file mode 100644
index 00000000..45f2bfb0
--- /dev/null
+++ b/src/legacy/data/constants.js
@@ -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'];
diff --git a/src/data/reducers.js b/src/legacy/data/reducers.js
similarity index 100%
rename from src/data/reducers.js
rename to src/legacy/data/reducers.js
diff --git a/src/data/sagas.js b/src/legacy/data/sagas.js
similarity index 100%
rename from src/data/sagas.js
rename to src/legacy/data/sagas.js
diff --git a/src/data/utils/cookies.js b/src/legacy/data/utils/cookies.js
similarity index 100%
rename from src/data/utils/cookies.js
rename to src/legacy/data/utils/cookies.js
diff --git a/src/data/utils/dataUtils.js b/src/legacy/data/utils/dataUtils.js
similarity index 100%
rename from src/data/utils/dataUtils.js
rename to src/legacy/data/utils/dataUtils.js
diff --git a/src/data/utils/dataUtils.test.js b/src/legacy/data/utils/dataUtils.test.js
similarity index 100%
rename from src/data/utils/dataUtils.test.js
rename to src/legacy/data/utils/dataUtils.test.js
diff --git a/src/data/utils/index.js b/src/legacy/data/utils/index.js
similarity index 100%
rename from src/data/utils/index.js
rename to src/legacy/data/utils/index.js
diff --git a/src/data/utils/reduxUtils.js b/src/legacy/data/utils/reduxUtils.js
similarity index 100%
rename from src/data/utils/reduxUtils.js
rename to src/legacy/data/utils/reduxUtils.js
diff --git a/src/data/utils/reduxUtils.test.js b/src/legacy/data/utils/reduxUtils.test.js
similarity index 100%
rename from src/data/utils/reduxUtils.test.js
rename to src/legacy/data/utils/reduxUtils.test.js
diff --git a/src/legacy/forgot-password/ForgotPasswordPage.jsx b/src/legacy/forgot-password/ForgotPasswordPage.jsx
new file mode 100644
index 00000000..7d8d2c5b
--- /dev/null
+++ b/src/legacy/forgot-password/ForgotPasswordPage.jsx
@@ -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 (
+
+ {header}
+
+
+ );
+};
+
+RequestInProgressAlert.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(RequestInProgressAlert);
diff --git a/src/forgot-password/data/actions.js b/src/legacy/forgot-password/data/actions.js
similarity index 100%
rename from src/forgot-password/data/actions.js
rename to src/legacy/forgot-password/data/actions.js
diff --git a/src/legacy/forgot-password/data/reducers.js b/src/legacy/forgot-password/data/reducers.js
new file mode 100644
index 00000000..3dfa9b43
--- /dev/null
+++ b/src/legacy/forgot-password/data/reducers.js
@@ -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;
diff --git a/src/forgot-password/data/sagas.js b/src/legacy/forgot-password/data/sagas.js
similarity index 100%
rename from src/forgot-password/data/sagas.js
rename to src/legacy/forgot-password/data/sagas.js
diff --git a/src/forgot-password/data/selectors.js b/src/legacy/forgot-password/data/selectors.js
similarity index 100%
rename from src/forgot-password/data/selectors.js
rename to src/legacy/forgot-password/data/selectors.js
diff --git a/src/forgot-password/data/service.js b/src/legacy/forgot-password/data/service.js
similarity index 100%
rename from src/forgot-password/data/service.js
rename to src/legacy/forgot-password/data/service.js
diff --git a/src/forgot-password/data/tests/sagas.test.js b/src/legacy/forgot-password/data/tests/sagas.test.js
similarity index 96%
rename from src/forgot-password/data/tests/sagas.test.js
rename to src/legacy/forgot-password/data/tests/sagas.test.js
index d3e85654..9e4de10e 100644
--- a/src/forgot-password/data/tests/sagas.test.js
+++ b/src/legacy/forgot-password/data/tests/sagas.test.js
@@ -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();
diff --git a/src/forgot-password/index.js b/src/legacy/forgot-password/index.js
similarity index 100%
rename from src/forgot-password/index.js
rename to src/legacy/forgot-password/index.js
diff --git a/src/legacy/forgot-password/messages.js b/src/legacy/forgot-password/messages.js
new file mode 100644
index 00000000..8c33acc5
--- /dev/null
+++ b/src/legacy/forgot-password/messages.js
@@ -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;
diff --git a/src/legacy/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/legacy/forgot-password/tests/ForgotPasswordPage.test.jsx
new file mode 100644
index 00000000..5b83b0fb
--- /dev/null
+++ b/src/legacy/forgot-password/tests/ForgotPasswordPage.test.jsx
@@ -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 => (
+
+ {children}
+
+ );
+
+ beforeEach(() => {
+ store = mockStore();
+ props = {
+ forgotPassword: jest.fn(),
+ status: null,
+ };
+ });
+
+ it('should match default section snapshot', () => {
+ const tree = renderer.create(reduxWrapper())
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('should match forbidden section snapshot', () => {
+ props = {
+ ...props,
+ status: 'forbidden',
+ };
+ const tree = renderer.create(reduxWrapper())
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('should match pending section snapshot', () => {
+ props = {
+ ...props,
+ status: 'pending',
+ };
+ const tree = renderer.create(reduxWrapper())
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('should match success section snapshot', () => {
+ props = {
+ ...props,
+ status: 'complete',
+ };
+ renderer.create(
+ reduxWrapper(
+
+
+ ,
+ ),
+ );
+ expect(history.location.pathname).toEqual('/login');
+ });
+
+ it('should display need other help signing in button', () => {
+ const wrapper = mount(reduxWrapper());
+ 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());
+
+ 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());
+
+ 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());
+
+ 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());
+ expect(forgotPasswordPage.find('.alert-danger').text()).toEqual(rateLimitMessage);
+ });
+
+ it('should not display any error message on change event', () => {
+ const forgotPasswordPage = mount(reduxWrapper());
+
+ 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());
+ 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());
+ expect(forgotPage.find()).toBeTruthy();
+ });
+});
diff --git a/src/legacy/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap b/src/legacy/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap
new file mode 100644
index 00000000..b6581961
--- /dev/null
+++ b/src/legacy/forgot-password/tests/__snapshots__/ForgotPasswordPage.test.jsx.snap
@@ -0,0 +1,369 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ForgotPasswordPage should match default section snapshot 1`] = `
+
+
+
+
+
+`;
+
+exports[`ForgotPasswordPage should match forbidden section snapshot 1`] = `
+
+
+
+
+
+`;
+
+exports[`ForgotPasswordPage should match pending section snapshot 1`] = `
+
+
+
+
+
+`;
diff --git a/src/i18n/index.jsx b/src/legacy/i18n/index.jsx
similarity index 100%
rename from src/i18n/index.jsx
rename to src/legacy/i18n/index.jsx
diff --git a/src/i18n/messages/ar.json b/src/legacy/i18n/messages/ar.json
similarity index 100%
rename from src/i18n/messages/ar.json
rename to src/legacy/i18n/messages/ar.json
diff --git a/src/i18n/messages/ca.json b/src/legacy/i18n/messages/ca.json
similarity index 100%
rename from src/i18n/messages/ca.json
rename to src/legacy/i18n/messages/ca.json
diff --git a/src/i18n/messages/es_419.json b/src/legacy/i18n/messages/es_419.json
similarity index 100%
rename from src/i18n/messages/es_419.json
rename to src/legacy/i18n/messages/es_419.json
diff --git a/src/i18n/messages/fr.json b/src/legacy/i18n/messages/fr.json
similarity index 100%
rename from src/i18n/messages/fr.json
rename to src/legacy/i18n/messages/fr.json
diff --git a/src/i18n/messages/he.json b/src/legacy/i18n/messages/he.json
similarity index 100%
rename from src/i18n/messages/he.json
rename to src/legacy/i18n/messages/he.json
diff --git a/src/i18n/messages/id.json b/src/legacy/i18n/messages/id.json
similarity index 100%
rename from src/i18n/messages/id.json
rename to src/legacy/i18n/messages/id.json
diff --git a/src/i18n/messages/ko_kr.json b/src/legacy/i18n/messages/ko_kr.json
similarity index 100%
rename from src/i18n/messages/ko_kr.json
rename to src/legacy/i18n/messages/ko_kr.json
diff --git a/src/i18n/messages/pl.json b/src/legacy/i18n/messages/pl.json
similarity index 100%
rename from src/i18n/messages/pl.json
rename to src/legacy/i18n/messages/pl.json
diff --git a/src/i18n/messages/pt_br.json b/src/legacy/i18n/messages/pt_br.json
similarity index 100%
rename from src/i18n/messages/pt_br.json
rename to src/legacy/i18n/messages/pt_br.json
diff --git a/src/i18n/messages/ru.json b/src/legacy/i18n/messages/ru.json
similarity index 100%
rename from src/i18n/messages/ru.json
rename to src/legacy/i18n/messages/ru.json
diff --git a/src/i18n/messages/th.json b/src/legacy/i18n/messages/th.json
similarity index 100%
rename from src/i18n/messages/th.json
rename to src/legacy/i18n/messages/th.json
diff --git a/src/i18n/messages/uk.json b/src/legacy/i18n/messages/uk.json
similarity index 100%
rename from src/i18n/messages/uk.json
rename to src/legacy/i18n/messages/uk.json
diff --git a/src/i18n/messages/zh_CN.json b/src/legacy/i18n/messages/zh_CN.json
similarity index 100%
rename from src/i18n/messages/zh_CN.json
rename to src/legacy/i18n/messages/zh_CN.json
diff --git a/src/legacy/i18n/transifex_input.json b/src/legacy/i18n/transifex_input.json
new file mode 100644
index 00000000..a2d67e72
--- /dev/null
+++ b/src/legacy/i18n/transifex_input.json
@@ -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"
+}
\ No newline at end of file
diff --git a/src/legacy/index.jsx b/src/legacy/index.jsx
new file mode 100755
index 00000000..eb48a68c
--- /dev/null
+++ b/src/legacy/index.jsx
@@ -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 = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default LegacyApp;
diff --git a/src/index.scss b/src/legacy/index.scss
similarity index 100%
rename from src/index.scss
rename to src/legacy/index.scss
diff --git a/src/legacy/login/AccountActivationMessage.jsx b/src/legacy/login/AccountActivationMessage.jsx
new file mode 100644
index 00000000..c86cd5db
--- /dev/null
+++ b/src/legacy/login/AccountActivationMessage.jsx
@@ -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 = (
+
+ {intl.formatMessage(messages['account.activation.support.link'])}
+
+ );
+
+ heading = intl.formatMessage(messages['account.activation.error.message.title']);
+ activationMessage = (
+
+ );
+ break;
+ }
+ default:
+ break;
+ }
+
+ return activationMessage ? (
+
+ {heading && {heading}}
+ {activationMessage}
+
+ ) : null;
+};
+
+AccountActivationMessage.propTypes = {
+ messageType: PropTypes.string.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(AccountActivationMessage);
diff --git a/src/legacy/login/LoginFailure.jsx b/src/legacy/login/LoginFailure.jsx
new file mode 100644
index 00000000..60efa5f7
--- /dev/null
+++ b/src/legacy/login/LoginFailure.jsx
@@ -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 = (
+
+`;
+
+exports[`LoginPage should match forget password alert message snapshot 1`] = `
+
+
+
+
+
+
+ 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.
+
+
+);
+
+describe('UnAuthOnlyRoute', () => {
+ const routerWrapper = () => (
+
+
+
+ );
+
+ it('should redirect to dashboard if already logged in', () => {
+ const dashboardURL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
+ delete window.location;
+ window.location = { href: '' };
+ const user = {
+ username: 'gonzo',
+ other: 'data',
+ };
+ const mockUseContext = jest.fn().mockImplementation(() => ({
+ authenticatedUser: user,
+ config: getConfig(),
+ }));
+
+ React.useContext = mockUseContext;
+ mount(routerWrapper());
+
+ expect(window.location.href).toBe(dashboardURL);
+ });
+
+ it('should render test login components', () => {
+ const mockUseContext = jest.fn().mockImplementation(() => ({
+ authenticatedUser: null,
+ config: {},
+ }));
+
+ React.useContext = mockUseContext;
+ const wrapper = mount(routerWrapper());
+
+ expect(wrapper.find('span').text()).toBe('Login Page');
+ });
+});
diff --git a/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap b/src/redesign/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap
similarity index 100%
rename from src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap
rename to src/redesign/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap
diff --git a/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap b/src/redesign/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap
similarity index 100%
rename from src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap
rename to src/redesign/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap
diff --git a/src/redesign/data/configureStore.js b/src/redesign/data/configureStore.js
new file mode 100644
index 00000000..16a19631
--- /dev/null
+++ b/src/redesign/data/configureStore.js
@@ -0,0 +1,33 @@
+import { getConfig } from '@edx/frontend-platform';
+import { applyMiddleware, createStore, compose } from 'redux';
+import thunkMiddleware from 'redux-thunk';
+import { composeWithDevTools } from 'redux-devtools-extension';
+import { createLogger } from 'redux-logger';
+import createSagaMiddleware from 'redux-saga';
+
+import createRootReducer from './reducers';
+import rootSaga from './sagas';
+
+const sagaMiddleware = createSagaMiddleware();
+
+function composeMiddleware() {
+ if (getConfig().ENVIRONMENT === 'development') {
+ const loggerMiddleware = createLogger({
+ collapsed: true,
+ });
+ return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
+ }
+
+ return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
+}
+
+export default function configureStore(initialState = {}) {
+ const store = createStore(
+ createRootReducer(),
+ initialState,
+ composeMiddleware(),
+ );
+ sagaMiddleware.run(rootSaga);
+
+ return store;
+}
diff --git a/src/data/constants.js b/src/redesign/data/constants.js
similarity index 100%
rename from src/data/constants.js
rename to src/redesign/data/constants.js
diff --git a/src/redesign/data/reducers.js b/src/redesign/data/reducers.js
new file mode 100755
index 00000000..4442848e
--- /dev/null
+++ b/src/redesign/data/reducers.js
@@ -0,0 +1,31 @@
+import { combineReducers } from 'redux';
+
+import {
+ reducer as loginReducer,
+ storeName as loginStoreName,
+} from '../login';
+import {
+ reducer as registerReducer,
+ storeName as registerStoreName,
+} from '../register';
+import {
+ reducer as commonComponentsReducer,
+ storeName as commonComponentsStoreName,
+} from '../common-components';
+import {
+ reducer as forgotPasswordReducer,
+ storeName as forgotPasswordStoreName,
+} from '../forgot-password';
+import {
+ reducer as resetPasswordReducer,
+ storeName as resetPasswordStoreName,
+} from '../reset-password';
+
+const createRootReducer = () => combineReducers({
+ [loginStoreName]: loginReducer,
+ [registerStoreName]: registerReducer,
+ [commonComponentsStoreName]: commonComponentsReducer,
+ [forgotPasswordStoreName]: forgotPasswordReducer,
+ [resetPasswordStoreName]: resetPasswordReducer,
+});
+export default createRootReducer;
diff --git a/src/redesign/data/sagas.js b/src/redesign/data/sagas.js
new file mode 100644
index 00000000..30ad4799
--- /dev/null
+++ b/src/redesign/data/sagas.js
@@ -0,0 +1,17 @@
+import { all } from 'redux-saga/effects';
+
+import { saga as registrationSaga } from '../register';
+import { saga as loginSaga } from '../login';
+import { saga as commonComponentsSaga } from '../common-components';
+import { saga as forgotPasswordSaga } from '../forgot-password';
+import { saga as resetPasswordSaga } from '../reset-password';
+
+export default function* rootSaga() {
+ yield all([
+ loginSaga(),
+ registrationSaga(),
+ commonComponentsSaga(),
+ forgotPasswordSaga(),
+ resetPasswordSaga(),
+ ]);
+}
diff --git a/src/redesign/data/utils/cookies.js b/src/redesign/data/utils/cookies.js
new file mode 100644
index 00000000..369913c6
--- /dev/null
+++ b/src/redesign/data/utils/cookies.js
@@ -0,0 +1,21 @@
+import Cookies from 'universal-cookie';
+import { getConfig } from '@edx/frontend-platform';
+
+export function setCookie(cookieName, cookieValue, cookieExpiry) {
+ const cookies = new Cookies();
+ const options = { domain: getConfig().COOKIE_DOMAIN, path: '/' };
+ if (cookieExpiry) {
+ options.expires = cookieExpiry;
+ }
+ cookies.set(cookieName, cookieValue, options);
+}
+
+export default function setSurveyCookie(surveyType) {
+ const cookieName = getConfig().USER_SURVEY_COOKIE_NAME;
+ if (cookieName) {
+ const signupTimestamp = (new Date()).getTime();
+ // set expiry to exactly 24 hours from now
+ const cookieExpiry = new Date(signupTimestamp + 1 * 864e5);
+ setCookie(cookieName, surveyType, cookieExpiry);
+ }
+}
diff --git a/src/redesign/data/utils/dataUtils.js b/src/redesign/data/utils/dataUtils.js
new file mode 100644
index 00000000..b61a4d20
--- /dev/null
+++ b/src/redesign/data/utils/dataUtils.js
@@ -0,0 +1,80 @@
+// Utility functions
+
+import * as QueryString from 'query-string';
+import { AUTH_PARAMS } from '../constants';
+
+export default function processLink(link) {
+ let matches;
+ link.replace(/(.*?)([^<]+)<\/a>(.*)/g, function () { // eslint-disable-line func-names
+ matches = Array.prototype.slice.call(arguments, 1, 5); // eslint-disable-line prefer-rest-params
+ });
+ return matches;
+}
+
+export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
+ let tpaProvider = null;
+ let skipHintedLogin = false;
+ [...primaryProviders, ...secondaryProviders].forEach((provider) => {
+ if (provider.id === tpaHintProvider) {
+ tpaProvider = provider;
+ if (provider.skipHintedLogin) {
+ skipHintedLogin = true;
+ }
+ }
+ });
+ return { provider: tpaProvider, skipHintedLogin };
+};
+
+export const getTpaHint = () => {
+ const params = QueryString.parse(window.location.search);
+ let tpaHint = null;
+ tpaHint = params.tpa_hint;
+ if (!tpaHint) {
+ const { next } = params;
+ if (next) {
+ const index = next.indexOf('tpa_hint=');
+ if (index !== -1) {
+ tpaHint = next.substring(index + 'tpa_hint='.length, next.length);
+ }
+ }
+ }
+ return tpaHint;
+};
+
+export const updatePathWithQueryParams = (path) => {
+ const queryParams = window.location.search;
+
+ if (!queryParams) {
+ return path;
+ }
+
+ return `${path}${queryParams}`;
+};
+
+export const getAllPossibleQueryParam = () => {
+ const urlParams = QueryString.parse(window.location.search);
+ const params = {};
+ Object.entries(urlParams).forEach(([key, value]) => {
+ if (AUTH_PARAMS.indexOf(key) > -1) {
+ params[key] = value;
+ }
+ });
+
+ return params;
+};
+
+export const getActivationStatus = () => {
+ const params = QueryString.parse(window.location.search);
+
+ return params.account_activation_status;
+};
+
+export const isScrollBehaviorSupported = () => 'scrollBehavior' in document.documentElement.style;
+
+export const windowScrollTo = (options) => {
+ if (isScrollBehaviorSupported()) {
+ return window.scrollTo(options);
+ }
+
+ return window.scrollTo(options.top, options.left);
+};
diff --git a/src/redesign/data/utils/dataUtils.test.js b/src/redesign/data/utils/dataUtils.test.js
new file mode 100644
index 00000000..b2dd3c58
--- /dev/null
+++ b/src/redesign/data/utils/dataUtils.test.js
@@ -0,0 +1,32 @@
+import { LOGIN_PAGE } from '../constants';
+import processLink, { updatePathWithQueryParams } from './dataUtils';
+
+describe('processLink', () => {
+ it('should use the provided processLink function to', () => {
+ const expectedHref = 'http://test.server.com/';
+ const expectedText = 'test link';
+ const link = `${expectedText}`;
+
+ const matches = processLink(link);
+
+ expect(matches[1]).toEqual(expectedHref);
+ expect(matches[2]).toEqual(expectedText);
+ });
+});
+
+describe('updatePathWithQueryParams', () => {
+ it('should append query params into the path', () => {
+ const params = '?course_id=testCourseId';
+ const expectedPath = `${LOGIN_PAGE}${params}`;
+
+ Object.defineProperty(window, 'location', {
+ value: {
+ href: 'http://localhost/',
+ search: params,
+ },
+ });
+ const updatedPath = updatePathWithQueryParams(LOGIN_PAGE);
+
+ expect(updatedPath).toEqual(expectedPath);
+ });
+});
diff --git a/src/redesign/data/utils/index.js b/src/redesign/data/utils/index.js
new file mode 100644
index 00000000..d452dc5d
--- /dev/null
+++ b/src/redesign/data/utils/index.js
@@ -0,0 +1,11 @@
+export {
+ default,
+ getTpaProvider,
+ getTpaHint,
+ updatePathWithQueryParams,
+ getAllPossibleQueryParam,
+ getActivationStatus,
+ windowScrollTo,
+} from './dataUtils';
+export { default as AsyncActionType } from './reduxUtils';
+export { default as setSurveyCookie, setCookie } from './cookies';
diff --git a/src/redesign/data/utils/reduxUtils.js b/src/redesign/data/utils/reduxUtils.js
new file mode 100644
index 00000000..45b0d762
--- /dev/null
+++ b/src/redesign/data/utils/reduxUtils.js
@@ -0,0 +1,34 @@
+/**
+ * Helper class to save time when writing out action types for asynchronous methods. Also helps
+ * ensure that actions are namespaced.
+ */
+export default class AsyncActionType {
+ constructor(topic, name) {
+ this.topic = topic;
+ this.name = name;
+ }
+
+ get BASE() {
+ return `${this.topic}__${this.name}`;
+ }
+
+ get BEGIN() {
+ return `${this.topic}__${this.name}__BEGIN`;
+ }
+
+ get SUCCESS() {
+ return `${this.topic}__${this.name}__SUCCESS`;
+ }
+
+ get FAILURE() {
+ return `${this.topic}__${this.name}__FAILURE`;
+ }
+
+ get RESET() {
+ return `${this.topic}__${this.name}__RESET`;
+ }
+
+ get FORBIDDEN() {
+ return `${this.topic}__${this.name}__FORBIDDEN`;
+ }
+}
diff --git a/src/redesign/data/utils/reduxUtils.test.js b/src/redesign/data/utils/reduxUtils.test.js
new file mode 100644
index 00000000..3e5a3d80
--- /dev/null
+++ b/src/redesign/data/utils/reduxUtils.test.js
@@ -0,0 +1,14 @@
+import AsyncActionType from './reduxUtils';
+
+describe('AsyncActionType', () => {
+ it('should return well formatted action strings', () => {
+ const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
+
+ expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
+ expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
+ expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
+ expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
+ expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
+ expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
+ });
+});
diff --git a/src/forgot-password/ForgotPasswordAlert.jsx b/src/redesign/forgot-password/ForgotPasswordAlert.jsx
similarity index 100%
rename from src/forgot-password/ForgotPasswordAlert.jsx
rename to src/redesign/forgot-password/ForgotPasswordAlert.jsx
diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/redesign/forgot-password/ForgotPasswordPage.jsx
similarity index 100%
rename from src/forgot-password/ForgotPasswordPage.jsx
rename to src/redesign/forgot-password/ForgotPasswordPage.jsx
diff --git a/src/redesign/forgot-password/data/actions.js b/src/redesign/forgot-password/data/actions.js
new file mode 100644
index 00000000..dcdd871c
--- /dev/null
+++ b/src/redesign/forgot-password/data/actions.js
@@ -0,0 +1,26 @@
+import { AsyncActionType } from '../../data/utils';
+
+export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
+
+// Forgot Password
+export const forgotPassword = email => ({
+ type: FORGOT_PASSWORD.BASE,
+ payload: { email },
+});
+
+export const forgotPasswordBegin = () => ({
+ type: FORGOT_PASSWORD.BEGIN,
+});
+
+export const forgotPasswordSuccess = email => ({
+ type: FORGOT_PASSWORD.SUCCESS,
+ payload: { email },
+});
+
+export const forgotPasswordForbidden = () => ({
+ type: FORGOT_PASSWORD.FORBIDDEN,
+});
+
+export const forgotPasswordServerError = () => ({
+ type: FORGOT_PASSWORD.FAILURE,
+});
diff --git a/src/forgot-password/data/reducers.js b/src/redesign/forgot-password/data/reducers.js
similarity index 100%
rename from src/forgot-password/data/reducers.js
rename to src/redesign/forgot-password/data/reducers.js
diff --git a/src/redesign/forgot-password/data/sagas.js b/src/redesign/forgot-password/data/sagas.js
new file mode 100644
index 00000000..439f6bf0
--- /dev/null
+++ b/src/redesign/forgot-password/data/sagas.js
@@ -0,0 +1,36 @@
+import { logError, logInfo } from '@edx/frontend-platform/logging';
+import { call, put, takeEvery } from 'redux-saga/effects';
+
+// Actions
+import {
+ FORGOT_PASSWORD,
+ forgotPasswordBegin,
+ forgotPasswordSuccess,
+ forgotPasswordForbidden,
+ forgotPasswordServerError,
+} from './actions';
+
+import { forgotPassword } from './service';
+
+// Services
+export function* handleForgotPassword(action) {
+ try {
+ yield put(forgotPasswordBegin());
+
+ yield call(forgotPassword, action.payload.email);
+
+ yield put(forgotPasswordSuccess(action.payload.email));
+ } catch (e) {
+ if (e.response && e.response.status === 403) {
+ yield put(forgotPasswordForbidden());
+ logInfo(e);
+ } else {
+ yield put(forgotPasswordServerError());
+ logError(e);
+ }
+ }
+}
+
+export default function* saga() {
+ yield takeEvery(FORGOT_PASSWORD.BASE, handleForgotPassword);
+}
diff --git a/src/redesign/forgot-password/data/selectors.js b/src/redesign/forgot-password/data/selectors.js
new file mode 100644
index 00000000..dbb3f10e
--- /dev/null
+++ b/src/redesign/forgot-password/data/selectors.js
@@ -0,0 +1,10 @@
+import { createSelector } from 'reselect';
+
+export const storeName = 'forgotPassword';
+
+export const forgotPasswordSelector = state => ({ ...state[storeName] });
+
+export const forgotPasswordResultSelector = createSelector(
+ forgotPasswordSelector,
+ forgotPassword => forgotPassword,
+);
diff --git a/src/redesign/forgot-password/data/service.js b/src/redesign/forgot-password/data/service.js
new file mode 100644
index 00000000..25020c56
--- /dev/null
+++ b/src/redesign/forgot-password/data/service.js
@@ -0,0 +1,23 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import formurlencoded from 'form-urlencoded';
+
+// eslint-disable-next-line import/prefer-default-export
+export async function forgotPassword(email) {
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ isPublic: true,
+ };
+
+ const { data } = await getAuthenticatedHttpClient()
+ .post(
+ `${getConfig().LMS_BASE_URL}/account/password`,
+ formurlencoded({ email }),
+ requestConfig,
+ )
+ .catch((e) => {
+ throw (e);
+ });
+
+ return data;
+}
diff --git a/src/redesign/forgot-password/data/tests/sagas.test.js b/src/redesign/forgot-password/data/tests/sagas.test.js
new file mode 100644
index 00000000..9e4de10e
--- /dev/null
+++ b/src/redesign/forgot-password/data/tests/sagas.test.js
@@ -0,0 +1,67 @@
+import { runSaga } from 'redux-saga';
+
+import * as actions from '../actions';
+import { handleForgotPassword } from '../sagas';
+import * as api from '../service';
+import initializeMockLogging from '../../../../setupTest';
+
+const { loggingService } = initializeMockLogging();
+
+describe('handleForgotPassword', () => {
+ const params = {
+ payload: {
+ formData: {
+ email: 'test@test.com',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ loggingService.logError.mockReset();
+ loggingService.logInfo.mockReset();
+ });
+
+ it('should handle 500 error code', async () => {
+ const passwordErrorResponse = { response: { status: 500 } };
+
+ const forgotPasswordRequest = jest.spyOn(api, 'forgotPassword').mockImplementation(
+ () => Promise.reject(passwordErrorResponse),
+ );
+
+ const dispatched = [];
+ await runSaga(
+ { dispatch: (action) => dispatched.push(action) },
+ handleForgotPassword,
+ params,
+ );
+
+ expect(loggingService.logError).toHaveBeenCalled();
+ expect(dispatched).toEqual([
+ actions.forgotPasswordBegin(),
+ actions.forgotPasswordServerError(),
+ ]);
+ forgotPasswordRequest.mockClear();
+ });
+
+ it('should handle rate limit error', async () => {
+ const forbiddenErrorResponse = { response: { status: 403 } };
+
+ const forbiddenPasswordRequest = jest.spyOn(api, 'forgotPassword').mockImplementation(
+ () => Promise.reject(forbiddenErrorResponse),
+ );
+
+ const dispatched = [];
+ await runSaga(
+ { dispatch: (action) => dispatched.push(action) },
+ handleForgotPassword,
+ params,
+ );
+
+ expect(loggingService.logInfo).toHaveBeenCalled();
+ expect(dispatched).toEqual([
+ actions.forgotPasswordBegin(),
+ actions.forgotPasswordForbidden(null),
+ ]);
+ forbiddenPasswordRequest.mockClear();
+ });
+});
diff --git a/src/redesign/forgot-password/index.js b/src/redesign/forgot-password/index.js
new file mode 100644
index 00000000..2d30ff06
--- /dev/null
+++ b/src/redesign/forgot-password/index.js
@@ -0,0 +1,5 @@
+export { default } from './ForgotPasswordPage';
+export { default as reducer } from './data/reducers';
+export { FORGOT_PASSWORD } from './data/actions';
+export { default as saga } from './data/sagas';
+export { storeName, forgotPasswordResultSelector } from './data/selectors';
diff --git a/src/forgot-password/messages.js b/src/redesign/forgot-password/messages.js
similarity index 100%
rename from src/forgot-password/messages.js
rename to src/redesign/forgot-password/messages.js
diff --git a/src/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/redesign/forgot-password/tests/ForgotPasswordPage.test.jsx
similarity index 100%
rename from src/forgot-password/tests/ForgotPasswordPage.test.jsx
rename to src/redesign/forgot-password/tests/ForgotPasswordPage.test.jsx
diff --git a/src/redesign/i18n/index.jsx b/src/redesign/i18n/index.jsx
new file mode 100644
index 00000000..57b2ee41
--- /dev/null
+++ b/src/redesign/i18n/index.jsx
@@ -0,0 +1,33 @@
+import arMessages from './messages/ar.json';
+import caMessages from './messages/ca.json';
+// no need to import en messages-- they are in the defaultMessage field
+import es419Messages from './messages/es_419.json';
+import frMessages from './messages/fr.json';
+import zhcnMessages from './messages/zh_CN.json';
+import heMessages from './messages/he.json';
+import idMessages from './messages/id.json';
+import kokrMessages from './messages/ko_kr.json';
+import plMessages from './messages/pl.json';
+import ptbrMessages from './messages/pt_br.json';
+import ruMessages from './messages/ru.json';
+import thMessages from './messages/th.json';
+import ukMessages from './messages/uk.json';
+
+const messages = {
+ ar: arMessages,
+ es: es419Messages, // Prospectus uses es language code for spanish, added `es` option and pointed to es-419 strings.
+ 'es-419': es419Messages,
+ fr: frMessages,
+ 'zh-cn': zhcnMessages,
+ ca: caMessages,
+ he: heMessages,
+ id: idMessages,
+ 'ko-kr': kokrMessages,
+ pl: plMessages,
+ 'pt-br': ptbrMessages,
+ ru: ruMessages,
+ th: thMessages,
+ uk: ukMessages,
+};
+
+export default messages;
diff --git a/src/redesign/i18n/messages/ar.json b/src/redesign/i18n/messages/ar.json
new file mode 100644
index 00000000..804d7611
--- /dev/null
+++ b/src/redesign/i18n/messages/ar.json
@@ -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": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
+ "login.third.party.auth.account.not.linked.message": "لقد نجحت في تسجيل الدخول إلى {currentProvider}، ولكن حساب {currentProvider} لا يحتوي على حساب {platformName} مرتبط. لربط حساباتك، قم بتسجيل الدخول الآن باستخدام كلمة مرور {platformName}.",
+ "register.third.party.auth.account.not.linked.message": "لقد نجحت في تسجيل الدخول إلى {currentProvider}. نحتاج إلى القليل من المعلومات قبل بدء التعلّم مع {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": "أنت بحاجة لتنشيط حسابك من أجل تسجيل الدخول {lineBreak}\n{lineBreak}لقد أرسلنا للتو رابط التفعيل إلى البريد الإلكتروني {email}. تحقق من مجلدات الرسائل غير المرغوب فيها أو {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"
+}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/ca.json b/src/redesign/i18n/messages/ca.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/ca.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/es_419.json b/src/redesign/i18n/messages/es_419.json
new file mode 100644
index 00000000..d92ad83d
--- /dev/null
+++ b/src/redesign/i18n/messages/es_419.json
@@ -0,0 +1,146 @@
+{
+ "forgot.password.confirmation.message": "Introduciste {strongEmail}. Si esta dirección de correo electrónico está asociada con tu\n cuenta de edX, enviaremos un mensaje con instrucciones para recuperar tu contraseña a ese correo.",
+ "forgot.password.technical.support.help.message": "Si necesitas ayuda adicional, {technicalSupportLink}.",
+ "institution.login.page.sub.heading": "Elige tu institución en la siguiente lista:",
+ "forgot.password.confirmation.title": "Verifica tu correo electrónico",
+ "forgot.password.confirmation.support.link": "contacta con el equipo de soporte técnico",
+ "forgot.password.confirmation.info": "Si no recibes un mensaje de recuperación de tu contraseña en un minuto, verifica que introduciste la dirección de correo electrónico correcta, o verifica tu carpeta de correo no deseado.",
+ "internal.server.error.message": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
+ "server.ratelimit.error.message": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo.",
+ "enterprisetpa.title.heading": "¿Deseas iniciar sesión con tus credenciales de {providerName}?",
+ "enterprisetpa.sso.button.title": "Inicio de sesión con {providerName}",
+ "enterprisetpa.login.button.text": "Mostrar otras formas de iniciar sesión o de registrarme",
+ "sso.sign.in.with": "Inicio de sesión con {providerName}",
+ "sso.create.account.using": "Crear una cuenta con {providerName}",
+ "error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, verifica la URL y vuelve a intentarlo.",
+ "login.third.party.auth.account.not.linked.message": "Te has registrado correctamente en {currentProvider}, pero tu cuenta de {currentProvider} no tiene una cuenta de {platformName} asociada. Para asociar tus cuentas, inicia sesión ahora usando tu contraseña de {platformName}.",
+ "register.third.party.auth.account.not.linked.message": "Has iniciado sesión correctamente en {currentProvider}. Solo necesitamos un poco más de información para que puedas empezar a aprender con {platformName}.",
+ "forgot.password.page.title": "Olvidé la contraseña | {siteName}",
+ "forgot.password.page.heading": "Ayuda con la contraseña",
+ "forgot.password.page.instructions": "Por favor introduce tu dirección de correo electrónico de inicio de sesión o de recuperación a continuación y te enviaremos un correo electrónico con instrucciones.",
+ "forgot.password.page.invalid.email.message": "La dirección de correo que has introducido no está en el formato correcto.",
+ "forgot.password.page.email.field.label": "Correo electrónico",
+ "forgot.password.page.submit.button": "Recuperar mi contraseña",
+ "forgot.password.request.server.error": "No hemos podido enviar el correo electrónico de recuperación de la contraseña.",
+ "forgot.password.error.message.title": "Ha ocurrido un error.",
+ "forgot.password.request.in.progress.message": "Su solicitud anterior está en progreso, por favor inténtalo de nuevo en unos minutos.",
+ "forgot.password.empty.email.field.error": "Por favor, introduce tu correo electrónico.",
+ "forgot.password.invalid.email": "Ha ocurrido un error.",
+ "forgot.password.invalid.email.message": "La dirección de correo que has ingresado no está en el formato correcto.",
+ "forgot.password.email.help.text": "El correo electrónico que utilizaste para registrarte en {platformName}",
+ "account.activation.error.message": "Algo no funcionó correctamente, por favor {supportLink} para resolver este problema.",
+ "non.compliant.password.error": "{passwordComplaintRequirements} {lineBreak}Tu contraseña actual no cumple con los nuevos requisitos\n de seguridad. Acabamos de enviar un mensaje de actualización de contraseña al correo electrónico asociado con esta cuenta.\n Gracias por ayudarnos a mantener tus datos seguros.",
+ "login.inactive.user.error": "Para iniciar sesión, debes activar tu cuenta..{lineBreak}\n {lineBreak} Acabamos de enviar un enlace de activación a {email}. Si no recibes un correo electrónico,\n revisa tus carpetas de spam o {supportLink}.",
+ "login.reset.password.message.with.link": "Si has olvidado tu contraseña, haz clic en {resetLink} para restablecerla.",
+ "login.locked.reset.password.message.with.link": "Para estar seguro, puedes restablecer tu contraseña {resetLink} antes de volver a intentarlo.",
+ "login.page.title": "Login | {siteName}",
+ "sign.in.button": "Iniciar sesión",
+ "need.help.signing.in.collapsible.menu": "¿Necesitas ayuda para iniciar sesión?",
+ "forgot.password.link": "Olvidé mi contraseña",
+ "other.sign.in.issues": "Otros problemas de inicio de sesión ",
+ "need.other.help.signing.in.collapsible.menu": "¿Necesitas más ayuda para iniciar sesión?",
+ "institution.login.button": "Usar información de mi universidad",
+ "institution.login.page.title": "Iniciar sesión con las credenciales de la institución/campus",
+ "institution.login.page.back.button": "Volver al inicio",
+ "create.an.account": "Crear una cuenta",
+ "or.sign.in.with": "o inicie sesión con",
+ "non.compliant.password.title": "Recientemente hemos cambiado los requisitos de las contraseñas",
+ "first.time.here": "Primera vez aquí?",
+ "email.label": "Correo electrónico",
+ "email.help.message": "La dirección de correo electrónico que usaste para registrarte en edX.",
+ "enterprise.login.link.text": "Iniciar sesión con tu compañía o universidad",
+ "email.format.validation.message": "La dirección de correo que has ingresado no está en el formato correcto.",
+ "email.format.validation.less.chars.message": "El correo electrónico debe tener al menos 3 caracteres.",
+ "email.validation.message": "Por favor, introduce tu correo electrónico.",
+ "password.validation.message": "Por favor, introduzca tu contraseña.",
+ "password.label": "Contraseña (obligatorio)",
+ "register.link": "Crear una cuenta",
+ "sign.in.heading": "Iniciar sesión",
+ "account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
+ "account.activation.success.message": "Ahora recibirás por correo electrónico actualizaciones y alertas relacionadas con los cursos en los que estás inscrito. Inicia sesión para continuar.",
+ "account.already.activated.message": "La cuenta ya ha sido activada.",
+ "account.activation.error.message.title": "Tu cuenta no ha podido ser activada",
+ "account.activation.support.link": "contacta al equipo de soporte de edX",
+ "login.rate.limit.reached.message": "Demasiados intentos fallidos de inicio de sesión. Inténtelo de nuevo más tarde.",
+ "login.failure.header.title": "No se ha podido iniciar tu sesión.",
+ "contact.support.link": "entrar en contacto con el soporte de {platformName}",
+ "login.failed.link.text": "aquí",
+ "login.incorrect.credentials.error": "Correo electrónico o contraseña incorrectos.",
+ "login.failed.attempt.error": "Tienes {remainAttempts} más intentos de inicio de sesión antes de que tu cuenta se bloquee temporalmente.",
+ "login.locked.out.error.message": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en {lockedOutPeriod} minutos.",
+ "register.page.title": "Register | {siteName}",
+ "create.account.button": "Crear cuenta",
+ "already.have.an.edx.account": "¿Ya tienes una cuenta en edX?",
+ "sign.in.hyperlink": "Iniciar sesión ",
+ "create.an.account.using": "o crea una cuenta con",
+ "create.a.new.account": "Crear una nueva cuenta",
+ "register.institution.login.button": "Usar mis credenciales de la institución o el Campus",
+ "register.institution.login.page.title": "Registro con credenciales de la institución/campus",
+ "register.page.email.label": "Correo electrónico (obligatorio)",
+ "register.rate.limit.reached.message": "Demasiados intentos de registro fallidos. Vuelva a intentarlo más tarde.",
+ "email.ratelimit.less.chars.validation.message": "El correo electrónico debe tener 3 caracteres.",
+ "email.ratelimit.incorrect.format.validation.message": "La dirección de correo electrónico que has proporcionado no tiene el formato correcto.",
+ "email.ratelimit.password.validation.message": "Tu contraseña debe contener al menos 8 caracteres",
+ "register.page.password.validation.message": "Por favor, introduzca tu contraseña.",
+ "fullname.label": "Nombre completo (obligatorio) ",
+ "fullname.validation.message": "Por favor, introduce tu nombre completo.",
+ "username.label": "Nombre de usuario público (obligatorio)",
+ "username.validation.message": "Por favor, introduce tu nombre de usuario público.",
+ "username.format.validation.message": "Los nombres de usuario únicamente pueden contener las letras (A-Z, a-z), números (0-9), guión bajo (_) y guiones (-).",
+ "username.character.validation.message": "Tu contraseña debe contener al menos una letra.",
+ "username.number.validation.message": "Tu contraseña debe contener al menos un número.",
+ "username.ratelimit.less.chars.message": "El nombre de usuario público debe tener al menos 2 caracteres.",
+ "country.validation.message": "Selecciona tu país o región de residencia. ",
+ "support.education.research": "Apoya la investigación sobre educación proporcionando información adicional. (Opcional)",
+ "registration.request.server.error": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
+ "registration.request.failure.header": "No pudimos crear tu cuenta.",
+ "helptext.name": "Este nombre será utilizado por los certificados que obtengas.",
+ "helptext.username": "El nombre bajo el cual te presentarás en tus cursos no puede ser cambiado después.",
+ "helptext.password": "Tu contraseña debe contener al menos 8 caracteres, incluyendo 1 letra y 1 número.",
+ "helptext.email": "Esto es lo que usará para iniciar sesión.",
+ "terms.of.service.and.honor.code": "Condiciones de servicio y código de honor",
+ "privacy.policy": "Política de privacidad ",
+ "registration.year.of.birth.label": "Año de nacimiento (opcional)",
+ "registration.country.label": "País o región de residencia (obligatorio)",
+ "registration.field.gender.options.label": "Género (opcional)",
+ "registration.goals.label": "Díganos por qué estás interesado en edX (opcional)",
+ "registration.field.gender.options.f": "Femenino ",
+ "registration.field.gender.options.m": "Masculino",
+ "registration.field.gender.options.o": "Otro/Prefiero no decir",
+ "registration.field.education.levels.label": "Nivel más alto de educación completado (opcional)",
+ "registration.field.education.levels.p": "Doctorado",
+ "registration.field.education.levels.m": "Maestría o magíster",
+ "registration.field.education.levels.b": "Pregrado o Licenciatura",
+ "registration.field.education.levels.a": "Grado técnico - tecnológico",
+ "registration.field.education.levels.hs": "Enseñanza secundaria",
+ "registration.field.education.levels.jhs": "Formación media",
+ "registration.field.education.levels.el": "Enseñanza primaria",
+ "registration.field.education.levels.none": "Ninguna educación formal",
+ "registration.field.education.levels.other": "Otra educación",
+ "register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
+ "reset.password.request.invalid.token.description.message": "Este enlace para restablecer la contraseña no es válido. Es posible que ya haya sido utilizado.\n Para restablecer tu contraseña, ve a la página {loginPasswordLink} y selecciona {forgotPassword}",
+ "reset.password.page.title": "Restablecer contraseña | {siteName}",
+ "reset.password.page.heading": "Restablece tu contraseña",
+ "reset.password.page.instructions": "Ingresa y confirma tu nueva contraseña.",
+ "reset.password.page.invalid.match.message": "Las contraseñas no son iguales.",
+ "forgot.password.page.new.field.label": "Nueva contraseña",
+ "forgot.password.page.confirm.field.label": "Confirmar contraseña",
+ "reset.password.page.submit.button": "Restablecer mi contraseña",
+ "reset.password.request.success.header.message": "Restablecimiento de la contraseña completado.",
+ "forgot.password.confirmation.sign.in.link": "iniciar sesión",
+ "reset.password.request.forgot.password.text": "Olvidé mi contraseña",
+ "reset.password.request.invalid.token.header": "Enlace de restablecimiento de contraseña inválido",
+ "reset.password.empty.new.password.field.error": "Por favor, introduce tu nueva contraseña.",
+ "forgot.password.empty.new.password.error.heading": "No hemos podido restablecer tu contraseña.",
+ "reset.password.request.server.error": "No se ha podido restablecer la contraseña",
+ "reset.password.token.validation.sever.error": "Fallo de validación del token",
+ "reset.server.ratelimit.error": "Demasiadas solicitudes.",
+ "reset.password.confirmation.support.link": "Inicia sesión en tu cuenta.",
+ "reset.password.request.success.header.description.message": "Su contraseña ha sido restablecida. {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"
+}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/fr.json b/src/redesign/i18n/messages/fr.json
new file mode 100644
index 00000000..a2d67e72
--- /dev/null
+++ b/src/redesign/i18n/messages/fr.json
@@ -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"
+}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/he.json b/src/redesign/i18n/messages/he.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/he.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/id.json b/src/redesign/i18n/messages/id.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/id.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/ko_kr.json b/src/redesign/i18n/messages/ko_kr.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/ko_kr.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/pl.json b/src/redesign/i18n/messages/pl.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/pl.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/pt_br.json b/src/redesign/i18n/messages/pt_br.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/pt_br.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/ru.json b/src/redesign/i18n/messages/ru.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/ru.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/th.json b/src/redesign/i18n/messages/th.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/th.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/uk.json b/src/redesign/i18n/messages/uk.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/src/redesign/i18n/messages/uk.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/redesign/i18n/messages/zh_CN.json b/src/redesign/i18n/messages/zh_CN.json
new file mode 100644
index 00000000..a2d67e72
--- /dev/null
+++ b/src/redesign/i18n/messages/zh_CN.json
@@ -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"
+}
\ No newline at end of file
diff --git a/src/redesign/index.jsx b/src/redesign/index.jsx
new file mode 100755
index 00000000..38cd1cbd
--- /dev/null
+++ b/src/redesign/index.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Redirect, Route, Switch } from 'react-router-dom';
+
+import { AppProvider } from '@edx/frontend-platform/react';
+
+import {
+ BaseComponent, UnAuthOnlyRoute, registerIcons, NotFoundPage, Logistration,
+} from './common-components';
+import {
+ LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, WELCOME_PAGE,
+} from './data/constants';
+import configureStore from './data/configureStore';
+
+import ForgotPasswordPage from './forgot-password';
+import ResetPasswordPage from './reset-password';
+import WelcomePage from './welcome';
+import './index.scss';
+
+registerIcons();
+
+const RedesignApp = () => (
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default RedesignApp;
diff --git a/src/redesign/index.scss b/src/redesign/index.scss
new file mode 100755
index 00000000..3b10b284
--- /dev/null
+++ b/src/redesign/index.scss
@@ -0,0 +1,10 @@
+@import "~@edx/brand/paragon/fonts";
+@import "~@edx/brand/paragon/variables";
+@import "~@edx/paragon/scss/core/core";
+@import "~@edx/brand/paragon/overrides";
+
+@import "~@edx/frontend-component-header/dist/index";
+
+@import '@edx/frontend-component-cookie-policy-banner/build/_cookie-policy-banner';
+
+@import "./style";
diff --git a/src/login/AccountActivationMessage.jsx b/src/redesign/login/AccountActivationMessage.jsx
similarity index 100%
rename from src/login/AccountActivationMessage.jsx
rename to src/redesign/login/AccountActivationMessage.jsx
diff --git a/src/login/LoginFailure.jsx b/src/redesign/login/LoginFailure.jsx
similarity index 100%
rename from src/login/LoginFailure.jsx
rename to src/redesign/login/LoginFailure.jsx
diff --git a/src/login/LoginPage.jsx b/src/redesign/login/LoginPage.jsx
similarity index 100%
rename from src/login/LoginPage.jsx
rename to src/redesign/login/LoginPage.jsx
diff --git a/src/login/data/actions.js b/src/redesign/login/data/actions.js
similarity index 100%
rename from src/login/data/actions.js
rename to src/redesign/login/data/actions.js
diff --git a/src/redesign/login/data/constants.js b/src/redesign/login/data/constants.js
new file mode 100644
index 00000000..a213714c
--- /dev/null
+++ b/src/redesign/login/data/constants.js
@@ -0,0 +1,16 @@
+// Login Error Codes
+export const INACTIVE_USER = 'inactive-user';
+export const INTERNAL_SERVER_ERROR = 'internal-server-error';
+export const INVALID_FORM = 'invalid-form';
+export const NON_COMPLIANT_PASSWORD_EXCEPTION = 'NonCompliantPasswordException';
+export const FORBIDDEN_REQUEST = 'forbidden-request';
+export const FAILED_LOGIN_ATTEMPT = 'failed-login-attempt';
+export const ACCOUNT_LOCKED_OUT = 'account-locked-out';
+export const INCORRECT_EMAIL_PASSWORD = 'incorrect-email-or-password';
+
+// Account Activation Message
+export const ACCOUNT_ACTIVATION_MESSAGE = {
+ INFO: 'info',
+ SUCCESS: 'success',
+ ERROR: 'error',
+};
diff --git a/src/login/data/reducers.js b/src/redesign/login/data/reducers.js
similarity index 100%
rename from src/login/data/reducers.js
rename to src/redesign/login/data/reducers.js
diff --git a/src/redesign/login/data/sagas.js b/src/redesign/login/data/sagas.js
new file mode 100644
index 00000000..825ed738
--- /dev/null
+++ b/src/redesign/login/data/sagas.js
@@ -0,0 +1,50 @@
+import { call, put, takeEvery } from 'redux-saga/effects';
+
+import { camelCaseObject } from '@edx/frontend-platform';
+import { logError, logInfo } from '@edx/frontend-platform/logging';
+import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
+
+// Actions
+import {
+ LOGIN_REQUEST,
+ loginRequestBegin,
+ loginRequestFailure,
+ loginRequestSuccess,
+} from './actions';
+
+// Services
+import {
+ loginRequest,
+} from './service';
+
+export function* handleLoginRequest(action) {
+ try {
+ yield put(loginRequestBegin());
+
+ const { redirectUrl, success } = yield call(loginRequest, action.payload.creds);
+
+ yield put(loginRequestSuccess(
+ redirectUrl,
+ success,
+ ));
+ } catch (e) {
+ const statusCodes = [400];
+ if (e.response) {
+ const { status } = e.response;
+ if (statusCodes.includes(status)) {
+ yield put(loginRequestFailure(camelCaseObject(e.response.data)));
+ logInfo(e);
+ } else if (status === 403) {
+ yield put(loginRequestFailure({ errorCode: FORBIDDEN_REQUEST }));
+ logInfo(e);
+ } else {
+ yield put(loginRequestFailure({ errorCode: INTERNAL_SERVER_ERROR }));
+ logError(e);
+ }
+ }
+ }
+}
+
+export default function* saga() {
+ yield takeEvery(LOGIN_REQUEST.BASE, handleLoginRequest);
+}
diff --git a/src/redesign/login/data/selectors.js b/src/redesign/login/data/selectors.js
new file mode 100644
index 00000000..a409a5ee
--- /dev/null
+++ b/src/redesign/login/data/selectors.js
@@ -0,0 +1,15 @@
+import { createSelector } from 'reselect';
+
+export const storeName = 'login';
+
+export const loginSelector = state => ({ ...state[storeName] });
+
+export const loginRequestSelector = createSelector(
+ loginSelector,
+ login => login.loginResult,
+);
+
+export const loginErrorSelector = createSelector(
+ loginSelector,
+ login => login.loginError,
+);
diff --git a/src/login/data/service.js b/src/redesign/login/data/service.js
similarity index 100%
rename from src/login/data/service.js
rename to src/redesign/login/data/service.js
diff --git a/src/redesign/login/data/tests/sagas.test.js b/src/redesign/login/data/tests/sagas.test.js
new file mode 100644
index 00000000..a7f40180
--- /dev/null
+++ b/src/redesign/login/data/tests/sagas.test.js
@@ -0,0 +1,111 @@
+import { runSaga } from 'redux-saga';
+
+import { camelCaseObject } from '@edx/frontend-platform';
+
+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';
+
+const { loggingService } = initializeMockLogging();
+
+describe('handleLoginRequest', () => {
+ const params = {
+ payload: {
+ formData: {
+ email: 'test@test.com',
+ password: 'test-password',
+ },
+ },
+ };
+
+ const testErrorResponse = async (loginErrorResponse, expectedLogFunc, expectedDispatchers) => {
+ const loginRequest = jest.spyOn(api, 'loginRequest').mockImplementation(() => Promise.reject(loginErrorResponse));
+
+ const dispatched = [];
+ await runSaga(
+ { dispatch: (action) => dispatched.push(action) },
+ handleLoginRequest,
+ params,
+ );
+
+ expect(loginRequest).toHaveBeenCalledTimes(1);
+ expect(expectedLogFunc).toHaveBeenCalled();
+ expect(dispatched).toEqual(expectedDispatchers);
+ loginRequest.mockClear();
+ };
+
+ beforeEach(() => {
+ loggingService.logError.mockReset();
+ loggingService.logInfo.mockReset();
+ });
+
+ it('should call service and dispatch success action', async () => {
+ const data = { redirectUrl: '/dashboard', success: true };
+ const loginRequest = jest.spyOn(api, 'loginRequest')
+ .mockImplementation(() => Promise.resolve(data));
+
+ const dispatched = [];
+ await runSaga(
+ { dispatch: (action) => dispatched.push(action) },
+ handleLoginRequest,
+ params,
+ );
+
+ expect(loginRequest).toHaveBeenCalledTimes(1);
+ expect(dispatched).toEqual([
+ actions.loginRequestBegin(),
+ actions.loginRequestSuccess(data.redirectUrl, data.success),
+ ]);
+ loginRequest.mockClear();
+ });
+
+ it('should call service and dispatch error action', async () => {
+ const loginErrorResponse = {
+ response: {
+ status: 400,
+ data: {
+ login_error: 'something went wrong',
+ },
+ },
+ };
+
+ await testErrorResponse(loginErrorResponse, loggingService.logInfo, [
+ actions.loginRequestBegin(),
+ actions.loginRequestFailure(camelCaseObject(loginErrorResponse.response.data)),
+ ]);
+ });
+
+ it('should handle rate limit error code', async () => {
+ const loginErrorResponse = {
+ response: {
+ status: 403,
+ data: {
+ errorCode: FORBIDDEN_REQUEST,
+ },
+ },
+ };
+
+ await testErrorResponse(loginErrorResponse, loggingService.logInfo, [
+ actions.loginRequestBegin(),
+ actions.loginRequestFailure(loginErrorResponse.response.data),
+ ]);
+ });
+
+ it('should handle 500 error code', async () => {
+ const loginErrorResponse = {
+ response: {
+ status: 500,
+ data: {
+ errorCode: INTERNAL_SERVER_ERROR,
+ },
+ },
+ };
+
+ await testErrorResponse(loginErrorResponse, loggingService.logError, [
+ actions.loginRequestBegin(),
+ actions.loginRequestFailure(loginErrorResponse.response.data),
+ ]);
+ });
+});
diff --git a/src/redesign/login/index.js b/src/redesign/login/index.js
new file mode 100644
index 00000000..769161d2
--- /dev/null
+++ b/src/redesign/login/index.js
@@ -0,0 +1,4 @@
+export { default as LoginPage } from './LoginPage';
+export { default as reducer } from './data/reducers';
+export { default as saga } from './data/sagas';
+export { storeName } from './data/selectors';
diff --git a/src/login/messages.jsx b/src/redesign/login/messages.jsx
similarity index 100%
rename from src/login/messages.jsx
rename to src/redesign/login/messages.jsx
diff --git a/src/redesign/login/tests/AccountActivationMessage.test.jsx b/src/redesign/login/tests/AccountActivationMessage.test.jsx
new file mode 100644
index 00000000..4c20ba13
--- /dev/null
+++ b/src/redesign/login/tests/AccountActivationMessage.test.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
+
+import AccountActivationMessage from '../AccountActivationMessage';
+import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
+
+const IntlAccountActivationMessage = injectIntl(AccountActivationMessage);
+
+describe('AccountActivationMessage', () => {
+ it('should match account already activated message', () => {
+ const accountActivationMessage = mount(
+
+
+ ,
+ );
+
+ const expectedMessage = 'This account has already been activated.';
+ expect(accountActivationMessage.find('#account-activation-message').find('div').first().text()).toEqual(expectedMessage);
+ });
+
+ it('should match account activated success message', () => {
+ const accountActivationMessage = mount(
+
+
+ ,
+ );
+
+ const expectedMessage = 'Success! You have activated your account.'
+ + 'You will now receive email updates and alerts from us related to '
+ + 'the courses you are enrolled in. Sign in to continue.';
+ expect(accountActivationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
+ });
+
+ it('should match account activation error message', () => {
+ const accountActivationMessage = mount(
+
+
+ ,
+ );
+
+ const expectedMessage = 'Your account could not be activated'
+ + 'Something went wrong, please contact support to resolve this issue.';
+ expect(accountActivationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
+ });
+
+ it('should not display anything for invalid message type', () => {
+ const accountActivationMessage = mount(
+
+
+ ,
+ );
+
+ expect(accountActivationMessage).toEqual({});
+ });
+});
diff --git a/src/login/tests/LoginFailure.test.jsx b/src/redesign/login/tests/LoginFailure.test.jsx
similarity index 100%
rename from src/login/tests/LoginFailure.test.jsx
rename to src/redesign/login/tests/LoginFailure.test.jsx
diff --git a/src/login/tests/LoginPage.test.jsx b/src/redesign/login/tests/LoginPage.test.jsx
similarity index 100%
rename from src/login/tests/LoginPage.test.jsx
rename to src/redesign/login/tests/LoginPage.test.jsx
diff --git a/src/register/CountryDropdown.jsx b/src/redesign/register/CountryDropdown.jsx
similarity index 100%
rename from src/register/CountryDropdown.jsx
rename to src/redesign/register/CountryDropdown.jsx
diff --git a/src/register/OptionalFields.jsx b/src/redesign/register/OptionalFields.jsx
similarity index 100%
rename from src/register/OptionalFields.jsx
rename to src/redesign/register/OptionalFields.jsx
diff --git a/src/register/RegistrationFailure.jsx b/src/redesign/register/RegistrationFailure.jsx
similarity index 100%
rename from src/register/RegistrationFailure.jsx
rename to src/redesign/register/RegistrationFailure.jsx
diff --git a/src/register/RegistrationPage.jsx b/src/redesign/register/RegistrationPage.jsx
similarity index 100%
rename from src/register/RegistrationPage.jsx
rename to src/redesign/register/RegistrationPage.jsx
diff --git a/src/register/UsernameField.jsx b/src/redesign/register/UsernameField.jsx
similarity index 100%
rename from src/register/UsernameField.jsx
rename to src/redesign/register/UsernameField.jsx
diff --git a/src/register/data/actions.js b/src/redesign/register/data/actions.js
similarity index 100%
rename from src/register/data/actions.js
rename to src/redesign/register/data/actions.js
diff --git a/src/register/data/constants.js b/src/redesign/register/data/constants.js
similarity index 100%
rename from src/register/data/constants.js
rename to src/redesign/register/data/constants.js
diff --git a/src/register/data/reducers.js b/src/redesign/register/data/reducers.js
similarity index 100%
rename from src/register/data/reducers.js
rename to src/redesign/register/data/reducers.js
diff --git a/src/register/data/sagas.js b/src/redesign/register/data/sagas.js
similarity index 100%
rename from src/register/data/sagas.js
rename to src/redesign/register/data/sagas.js
diff --git a/src/register/data/selectors.js b/src/redesign/register/data/selectors.js
similarity index 100%
rename from src/register/data/selectors.js
rename to src/redesign/register/data/selectors.js
diff --git a/src/register/data/service.js b/src/redesign/register/data/service.js
similarity index 100%
rename from src/register/data/service.js
rename to src/redesign/register/data/service.js
diff --git a/src/register/data/tests/reducers.test.js b/src/redesign/register/data/tests/reducers.test.js
similarity index 100%
rename from src/register/data/tests/reducers.test.js
rename to src/redesign/register/data/tests/reducers.test.js
diff --git a/src/register/data/tests/sagas.test.js b/src/redesign/register/data/tests/sagas.test.js
similarity index 99%
rename from src/register/data/tests/sagas.test.js
rename to src/redesign/register/data/tests/sagas.test.js
index 9fb7e448..e55a6c31 100644
--- a/src/register/data/tests/sagas.test.js
+++ b/src/redesign/register/data/tests/sagas.test.js
@@ -7,7 +7,7 @@ import {
handleNewUserRegistration,
} from '../sagas';
import * as api from '../service';
-import initializeMockLogging from '../../../setupTest';
+import initializeMockLogging from '../../../../setupTest';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from '../constants';
const { loggingService } = initializeMockLogging();
diff --git a/src/redesign/register/index.js b/src/redesign/register/index.js
new file mode 100644
index 00000000..496c0f7b
--- /dev/null
+++ b/src/redesign/register/index.js
@@ -0,0 +1,4 @@
+export { default as RegistrationPage } from './RegistrationPage';
+export { default as reducer } from './data/reducers';
+export { default as saga } from './data/sagas';
+export { storeName } from './data/selectors';
diff --git a/src/register/messages.jsx b/src/redesign/register/messages.jsx
similarity index 100%
rename from src/register/messages.jsx
rename to src/redesign/register/messages.jsx
diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/redesign/register/tests/RegistrationPage.test.jsx
similarity index 100%
rename from src/register/tests/RegistrationPage.test.jsx
rename to src/redesign/register/tests/RegistrationPage.test.jsx
diff --git a/src/reset-password/ResetPasswordFailure.jsx b/src/redesign/reset-password/ResetPasswordFailure.jsx
similarity index 100%
rename from src/reset-password/ResetPasswordFailure.jsx
rename to src/redesign/reset-password/ResetPasswordFailure.jsx
diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/redesign/reset-password/ResetPasswordPage.jsx
similarity index 100%
rename from src/reset-password/ResetPasswordPage.jsx
rename to src/redesign/reset-password/ResetPasswordPage.jsx
diff --git a/src/reset-password/ResetPasswordSuccess.jsx b/src/redesign/reset-password/ResetPasswordSuccess.jsx
similarity index 100%
rename from src/reset-password/ResetPasswordSuccess.jsx
rename to src/redesign/reset-password/ResetPasswordSuccess.jsx
diff --git a/src/reset-password/data/actions.js b/src/redesign/reset-password/data/actions.js
similarity index 100%
rename from src/reset-password/data/actions.js
rename to src/redesign/reset-password/data/actions.js
diff --git a/src/reset-password/data/constants.js b/src/redesign/reset-password/data/constants.js
similarity index 100%
rename from src/reset-password/data/constants.js
rename to src/redesign/reset-password/data/constants.js
diff --git a/src/reset-password/data/reducers.js b/src/redesign/reset-password/data/reducers.js
similarity index 100%
rename from src/reset-password/data/reducers.js
rename to src/redesign/reset-password/data/reducers.js
diff --git a/src/reset-password/data/sagas.js b/src/redesign/reset-password/data/sagas.js
similarity index 100%
rename from src/reset-password/data/sagas.js
rename to src/redesign/reset-password/data/sagas.js
diff --git a/src/redesign/reset-password/data/selectors.js b/src/redesign/reset-password/data/selectors.js
new file mode 100644
index 00000000..a280d6f9
--- /dev/null
+++ b/src/redesign/reset-password/data/selectors.js
@@ -0,0 +1,10 @@
+import { createSelector } from 'reselect';
+
+export const storeName = 'resetPassword';
+
+export const resetPasswordSelector = state => ({ ...state[storeName] });
+
+export const resetPasswordResultSelector = createSelector(
+ resetPasswordSelector,
+ resetPassword => resetPassword,
+);
diff --git a/src/redesign/reset-password/data/service.js b/src/redesign/reset-password/data/service.js
new file mode 100644
index 00000000..8a4f234e
--- /dev/null
+++ b/src/redesign/reset-password/data/service.js
@@ -0,0 +1,64 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getHttpClient } from '@edx/frontend-platform/auth';
+import formurlencoded from 'form-urlencoded';
+
+// eslint-disable-next-line import/prefer-default-export
+export async function validateToken(token) {
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ };
+
+ const { data } = await getHttpClient()
+ .post(
+ `${getConfig().LMS_BASE_URL}/user_api/v1/account/password_reset/token/validate/`,
+ formurlencoded({ token }),
+ requestConfig,
+ )
+ .catch((e) => {
+ throw (e);
+ });
+ return data;
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export async function resetPassword(payload, token, queryParams) {
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ };
+ const url = new URL(`${getConfig().LMS_BASE_URL}/password/reset/${token}/`);
+
+ if (queryParams.is_account_recovery) {
+ url.searchParams.append('is_account_recovery', true);
+ }
+
+ const { data } = await getHttpClient()
+ .post(url.href, formurlencoded(payload), requestConfig)
+ .catch((e) => {
+ throw (e);
+ });
+ return data;
+}
+
+export async function validatePassword(password) {
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ };
+ const { data } = await getHttpClient()
+ .post(
+ `${getConfig().LMS_BASE_URL}/api/user/v1/validation/registration`,
+ formurlencoded({ password }),
+ requestConfig,
+ )
+ .catch((e) => {
+ throw (e);
+ });
+
+ let errorMessage = '';
+ // Be careful about grabbing this message, since we could have received an HTTP error or the
+ // endpoint didn't give us what we expect. We only care if we get a clear error message.
+ if (data.validation_decisions && data.validation_decisions.password) {
+ errorMessage = data.validation_decisions.password;
+ }
+
+ return errorMessage;
+}
diff --git a/src/reset-password/data/tests/sagas.test.js b/src/redesign/reset-password/data/tests/sagas.test.js
similarity index 98%
rename from src/reset-password/data/tests/sagas.test.js
rename to src/redesign/reset-password/data/tests/sagas.test.js
index f156d334..ebd89de1 100644
--- a/src/reset-password/data/tests/sagas.test.js
+++ b/src/redesign/reset-password/data/tests/sagas.test.js
@@ -10,7 +10,7 @@ import { PASSWORD_RESET } from '../constants';
import { handleResetPassword, handleValidateToken } from '../sagas';
import * as api from '../service';
-import initializeMockLogging from '../../../setupTest';
+import initializeMockLogging from '../../../../setupTest';
const { loggingService } = initializeMockLogging();
diff --git a/src/redesign/reset-password/index.js b/src/redesign/reset-password/index.js
new file mode 100644
index 00000000..2116170a
--- /dev/null
+++ b/src/redesign/reset-password/index.js
@@ -0,0 +1,5 @@
+export { default } from './ResetPasswordPage';
+export { default as reducer } from './data/reducers';
+export { RESET_PASSWORD } from './data/actions';
+export { default as saga } from './data/sagas';
+export { storeName } from './data/selectors';
diff --git a/src/reset-password/messages.js b/src/redesign/reset-password/messages.js
similarity index 100%
rename from src/reset-password/messages.js
rename to src/redesign/reset-password/messages.js
diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/redesign/reset-password/tests/ResetPasswordPage.test.jsx
similarity index 100%
rename from src/reset-password/tests/ResetPasswordPage.test.jsx
rename to src/redesign/reset-password/tests/ResetPasswordPage.test.jsx
diff --git a/src/welcome/WelcomePage.jsx b/src/redesign/welcome/WelcomePage.jsx
similarity index 100%
rename from src/welcome/WelcomePage.jsx
rename to src/redesign/welcome/WelcomePage.jsx
diff --git a/src/redesign/welcome/index.js b/src/redesign/welcome/index.js
new file mode 100644
index 00000000..b031301e
--- /dev/null
+++ b/src/redesign/welcome/index.js
@@ -0,0 +1 @@
+export { default } from './WelcomePage';
diff --git a/src/redesign/welcome/messages.jsx b/src/redesign/welcome/messages.jsx
new file mode 100644
index 00000000..d3244f2a
--- /dev/null
+++ b/src/redesign/welcome/messages.jsx
@@ -0,0 +1,110 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'optional.fields.page.title': {
+ id: 'optional.fields.page.title',
+ defaultMessage: 'Optional Fields | {siteName}',
+ description: 'optional fields page title',
+ },
+ 'optional.fields.page.heading': {
+ id: 'optional.fields.page.heading',
+ defaultMessage: 'Support education research by providing additional information.',
+ description: 'The page heading for the optional fields page.',
+ },
+ 'welcome.to.edx': {
+ id: 'welcome.to.edx',
+ defaultMessage: 'Welcome to edX, {username}!',
+ description: 'Welcome message on the optional fields page.',
+ },
+ 'registration.field.gender.options.label': {
+ id: 'registration.field.gender.options.label',
+ defaultMessage: 'Gender (optional)',
+ description: 'Placeholder for the gender options dropdown',
+ },
+ 'registration.field.gender.options.f': {
+ id: 'registration.field.gender.options.f',
+ defaultMessage: 'Female',
+ description: 'The label for the female gender option.',
+ },
+ 'registration.field.gender.options.m': {
+ id: 'registration.field.gender.options.m',
+ defaultMessage: 'Male',
+ description: 'The label for the male gender option.',
+ },
+ 'registration.field.gender.options.o': {
+ id: 'registration.field.gender.options.o',
+ defaultMessage: 'Other/Prefer not to say',
+ description: 'The label for catch-all gender option.',
+ },
+ 'registration.field.education.levels.label': {
+ id: 'registration.field.education.levels.label',
+ defaultMessage: 'Highest level of education completed (optional)',
+ description: 'Placeholder for the education levels dropdown.',
+ },
+ 'registration.field.education.levels.p': {
+ id: 'registration.field.education.levels.p',
+ defaultMessage: 'Doctorate',
+ description: 'Selected by the user if their highest level of education is a doctorate degree.',
+ },
+ 'registration.field.education.levels.m': {
+ id: 'registration.field.education.levels.m',
+ defaultMessage: "Master's or professional degree",
+ description: "Selected by the user if their highest level of education is a master's or professional degree from a college or university.",
+ },
+ 'registration.field.education.levels.b': {
+ id: 'registration.field.education.levels.b',
+ defaultMessage: "Bachelor's degree",
+ description: "Selected by the user if their highest level of education is a four year college or university bachelor's degree.",
+ },
+ 'registration.field.education.levels.a': {
+ id: 'registration.field.education.levels.a',
+ defaultMessage: "Associate's degree",
+ description: "Selected by the user if their highest level of education is an associate's degree. 1-2 years of college or university.",
+ },
+ 'registration.field.education.levels.hs': {
+ id: 'registration.field.education.levels.hs',
+ defaultMessage: 'Secondary/high school',
+ description: 'Selected by the user if their highest level of education is secondary or high school. 9-12 years of education.',
+ },
+ 'registration.field.education.levels.jhs': {
+ id: 'registration.field.education.levels.jhs',
+ defaultMessage: 'Junior secondary/junior high/middle school',
+ description: 'Selected by the user if their highest level of education is junior or middle school. 6-8 years of education.',
+ },
+ 'registration.field.education.levels.el': {
+ id: 'registration.field.education.levels.el',
+ defaultMessage: 'Elementary/primary school',
+ description: 'Selected by the user if their highest level of education is elementary or primary school. 1-5 years of education.',
+ },
+ 'registration.field.education.levels.none': {
+ id: 'registration.field.education.levels.none',
+ defaultMessage: 'No formal education',
+ description: 'Selected by the user to describe their education.',
+ },
+ 'registration.field.education.levels.other': {
+ id: 'registration.field.education.levels.other',
+ defaultMessage: 'Other education',
+ description: 'Selected by the user if they have a type of education not described by the other choices.',
+ },
+ 'registration.year.of.birth.label': {
+ id: 'registration.year.of.birth.label',
+ defaultMessage: 'Year of birth (optional)',
+ description: 'Placeholder for the year of birth options dropdown',
+ },
+ 'optional.fields.information.link': {
+ id: 'optional.fields.information.link',
+ defaultMessage: 'Learn more about how we use this information.',
+ description: 'Optional fields page information link',
+ },
+ 'optional.fields.submit.button': {
+ id: 'optional.fields.submit.button',
+ defaultMessage: 'Submit',
+ description: 'Submit button text',
+ },
+ 'optional.fields.skip.button': {
+ id: 'optional.fields.skip.button',
+ defaultMessage: 'Skip for now',
+ description: 'Skip button text',
+ },
+});
+export default messages;