logistration folder refactored to login and register

This commit is contained in:
adeelehsan
2021-01-21 02:28:53 +05:00
parent 5b58986b71
commit ad2ee9e013
64 changed files with 1025 additions and 1046 deletions

View File

@@ -12,7 +12,7 @@ const ConfirmationAlert = (props) => {
return (
<Alert id="confirmation-alert" variant="success">
<Alert.Heading>{intl.formatMessage(messages['authn.forgot.password.confirmation.title'])}</Alert.Heading>
<Alert.Heading>{intl.formatMessage(messages['forgot.password.confirmation.title'])}</Alert.Heading>
<p>
<FormattedMessage
id="logistration.forgot.password.confirmation.message"
@@ -22,16 +22,16 @@ const ConfirmationAlert = (props) => {
values={{ strongEmail: <strong>{email}</strong> }}
/>
</p>
<p>{intl.formatMessage(messages['authn.forgot.password.confirmation.info'])}</p>
<p>{intl.formatMessage(messages['forgot.password.confirmation.info'])}</p>
<p>
<FormattedMessage
id="authn.forgot.password.technical.support.help.message"
id="forgot.password.technical.support.help.message"
defaultMessage="If you need further assistance, {technicalSupportLink}."
description="Message to help user contact technical support"
values={{
technicalSupportLink: (
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK}>
{intl.formatMessage(messages['authn.forgot.password.confirmation.support.link'])}
{intl.formatMessage(messages['forgot.password.confirmation.support.link'])}
</Alert.Link>
),
}}

View File

@@ -40,7 +40,7 @@ const InstitutionLogistration = props => {
{headingTitle}
</h1>
<p className="mb-2">
{intl.formatMessage(messages['logistration.institution.login.page.sub.heading'])}
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
</p>
<div className="mb-2 ml-2">
<ul>

View File

@@ -0,0 +1,22 @@
import { AsyncActionType } from '../../data/utils';
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
payload: { urlParams },
});
export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});

View File

@@ -0,0 +1,32 @@
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
export const defaultState = {
thirdPartyAuthApiStatus: null,
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
default:
return state;
}
};
export default reducer;

View File

@@ -0,0 +1,34 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { logError } from '@edx/frontend-platform/logging';
// Actions
import {
THIRD_PARTY_AUTH_CONTEXT,
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextSuccess,
getThirdPartyAuthContextFailure,
} from './actions';
// Services
import {
getThirdPartyAuthContext,
} from './service';
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(getThirdPartyAuthContextSuccess(
thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export default function* saga() {
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
export const storeName = 'commonComponents';
export const commonComponentsSelector = state => ({ ...state[storeName] });
export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);

View File

@@ -0,0 +1,23 @@
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams,
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/api/third_party_auth_context`,
requestConfig,
)
.catch((e) => {
throw (e);
});
return {
thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
};
}

View File

@@ -0,0 +1,65 @@
import { runSaga } from 'redux-saga';
import * as actions from '../actions';
import { fetchThirdPartyAuthContext } from '../sagas';
import * as api from '../service';
import initializeMockLogging from '../../../setupTest';
const { loggingService } = initializeMockLogging();
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
};
const data = {
currentProvider: null,
providers: [],
secondaryProviders: [],
finishAuthUrl: null,
pipelineUserDetails: {},
};
beforeEach(() => {
loggingService.logError.mockReset();
});
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextSuccess(data),
]);
getThirdPartyAuthContext.mockClear();
});
it('should call service and dispatch error action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextFailure(),
]);
getThirdPartyAuthContext.mockClear();
});
});

View File

@@ -3,3 +3,10 @@ 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 reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';

View File

@@ -0,0 +1,28 @@
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',
},
});
export default messages;

View File

@@ -3,7 +3,7 @@ import renderer from 'react-test-renderer';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import SocialAuthProviders from '../SocialAuthProviders';
import { registerIcons } from '../../common-components';
import registerIcons from '../RegisterFaIcons';
registerIcons();

View File

@@ -1,9 +1,17 @@
import { combineReducers } from 'redux';
import {
reducer as logistrationReducer,
storeName as logistrationStoreName,
} from '../logistration';
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,
@@ -14,7 +22,9 @@ import {
} from '../reset-password';
const createRootReducer = () => combineReducers({
[logistrationStoreName]: logistrationReducer,
[loginStoreName]: loginReducer,
[registerStoreName]: registerReducer,
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
});

View File

@@ -1,12 +1,16 @@
import { all } from 'redux-saga/effects';
import { saga as registrationSaga } from '../logistration';
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(),
]);

View File

@@ -16,14 +16,14 @@ import { forgotPassword } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import RequestInProgressAlert from './RequestInProgressAlert';
import { LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import LoginHelpLinks from '../logistration/LoginHelpLinks';
import LoginHelpLinks from '../login/LoginHelpLinks';
const ForgotPasswordPage = (props) => {
const { intl, status } = props;
let invalidEmailMessage;
const validateEmail = (e, setFieldValue) => {
invalidEmailMessage = intl.formatMessage(messages['logisration.forgot.password.page.invalid.email.message']);
invalidEmailMessage = intl.formatMessage(messages['forgot.password.page.invalid.email.message']);
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
const inputEmail = e.target.value;
@@ -31,7 +31,7 @@ const ForgotPasswordPage = (props) => {
setFieldValue('email', inputEmail);
setFieldValue('isEmailValid', isEmailValid);
if (inputEmail.length < 3) {
invalidEmailMessage = `${intl.formatMessage(messages['logisration.forgot.password.page.email.invalid.length.message'])} ${invalidEmailMessage}`;
invalidEmailMessage = `${intl.formatMessage(messages['forgot.password.page.email.invalid.length.message'])} ${invalidEmailMessage}`;
}
};
@@ -59,10 +59,10 @@ const ForgotPasswordPage = (props) => {
<Form className="mw-500">
{status === 'forbidden' ? <RequestInProgressAlert /> : null}
<h3 className="mt-3">
{intl.formatMessage(messages['logisration.forgot.password.page.heading'])}
{intl.formatMessage(messages['forgot.password.page.heading'])}
</h3>
<p className="mb-4">
{intl.formatMessage(messages['logisration.forgot.password.page.instructions'])}
{intl.formatMessage(messages['forgot.password.page.instructions'])}
</p>
<ValidationFormGroup
className="mb-0 w-100"
@@ -71,7 +71,7 @@ const ForgotPasswordPage = (props) => {
invalidMessage={invalidEmailMessage}
>
<Form.Label htmlFor="forgot-password-input" className="h6 mr-1">
{intl.formatMessage(messages['logisration.forgot.password.page.email.field.label'])}
{intl.formatMessage(messages['forgot.password.page.email.field.label'])}
</Form.Label>
<Input
name="email"
@@ -82,7 +82,7 @@ const ForgotPasswordPage = (props) => {
onChange={e => validateEmail(e, setFieldValue)}
/>
<p className="mb-2">
{intl.formatMessage(messages['logisration.forgot.password.page.email.field.help.text'])}
{intl.formatMessage(messages['forgot.password.page.email.field.help.text'])}
</p>
</ValidationFormGroup>
<LoginHelpLinks page="forgot-password" />
@@ -91,7 +91,7 @@ const ForgotPasswordPage = (props) => {
className="btn-primary mt-3"
state={status}
labels={{
default: intl.formatMessage(messages['logisration.forgot.password.page.submit.button']),
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
}}
onClick={handleSubmit}
/>

View File

@@ -8,7 +8,7 @@ const RequestInProgressAlert = () => (
<Alert variant="warning">
<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />
<FormattedMessage
id="logistration.forgot.password.request.inprogress.message"
id="forgot.password.request.inprogress.message"
defaultMessage="Your previous request is still in progress, please try again in a few moments."
description="A message displayed when a previous password reset request is still in progress."
/>

View File

@@ -1,38 +1,38 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'logisration.forgot.password.page.heading': {
id: 'logisration.forgot.password.page.heading',
'forgot.password.page.heading': {
id: 'forgot.password.page.heading',
defaultMessage: 'Password assistance',
description: 'The page heading for the forgot password page.',
},
'logisration.forgot.password.page.instructions': {
id: 'logisration.forgot.password.page.instructions',
'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.',
},
'logisration.forgot.password.page.invalid.email.message': {
id: 'logisration.forgot.password.page.invalid.email.message',
'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.',
},
'logisration.forgot.password.page.email.field.label': {
id: 'logisration.forgot.password.page.email.field.label',
'forgot.password.page.email.field.label': {
id: 'forgot.password.page.email.field.label',
defaultMessage: 'Email',
description: 'Email field label for the forgot password page.',
},
'logisration.forgot.password.page.email.field.help.text': {
id: 'logisration.forgot.password.page.email.field.help.text',
'forgot.password.page.email.field.help.text': {
id: 'forgot.password.page.email.field.help.text',
defaultMessage: 'The email address you used to register with edX.',
description: 'Email field help text for the forgot password page.',
},
'logisration.forgot.password.page.submit.button': {
id: 'logisration.forgot.password.page.submit.button',
'forgot.password.page.submit.button': {
id: 'forgot.password.page.submit.button',
defaultMessage: 'Recover my password',
description: 'Submit button text for the forgot password page.',
},
'logisration.forgot.password.page.email.invalid.length.message': {
id: 'logisration.forgot.password.page.email.invalid.length.message',
'forgot.password.page.email.invalid.length.message': {
id: 'forgot.password.page.email.invalid.length.message',
defaultMessage: 'Email must have at least 3 characters.',
description: 'Invalid email address length message for the forgot password page.',
},

View File

@@ -11,7 +11,8 @@ import { Redirect, Route, Switch } from 'react-router-dom';
import { messages as headerMessages } from '@edx/frontend-component-header';
import configureStore from './data/configureStore';
import { LoginPage, RegistrationPage } from './logistration';
import { RegistrationPage } from './register';
import { LoginPage } from './login';
import {
LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM,
} from './data/constants';

View File

@@ -5,4 +5,4 @@
@import "~@edx/frontend-component-header/dist/index";
@import "./logistration/style";
@import "./style";

View File

@@ -17,25 +17,25 @@ const AccountActivationMessage = (props) => {
switch (messageType) {
case ACCOUNT_ACTIVATION_MESSAGE.SUCCESS: {
heading = intl.formatMessage(messages['authn.account.activation.success.message.title']);
activationMessage = intl.formatMessage(messages['authn.account.activation.success.message']);
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['authn.account.already.activated.message']);
activationMessage = intl.formatMessage(messages['account.already.activated.message']);
break;
}
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
const supportLink = (
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
{intl.formatMessage(messages['authn.account.activation.support.link'])}
{intl.formatMessage(messages['account.activation.support.link'])}
</Alert.Link>
);
heading = intl.formatMessage(messages['authn.account.activation.error.message.title']);
heading = intl.formatMessage(messages['account.activation.error.message.title']);
activationMessage = (
<FormattedMessage
id="authn.account.activation.error.message"
id="account.activation.error.message"
defaultMessage="Something went wrong, please {supportLink} to resolve this issue."
description="Account activation error message"
values={{ supportLink }}

View File

@@ -24,12 +24,12 @@ const LoginFailureMessage = (props) => {
errorList = (
<li key="password-non-compliance">
<FormattedMessage
id="login.non.compliant.password.error"
id="non.compliant.password.error"
defaultMessage="{passwordComplaintRequirements} {lineBreak}Your current password does not meet the new security
requirements. We just sent a password-reset message to the email address associated with this account.
Thank you for helping us keep your data safe."
values={{
passwordComplaintRequirements: <strong>{intl.formatMessage(messages['logistration.non.compliant.password.title'])}</strong>,
passwordComplaintRequirements: <strong>{intl.formatMessage(messages['non.compliant.password.title'])}</strong>,
lineBreak: <br />,
}}
/>
@@ -40,7 +40,7 @@ const LoginFailureMessage = (props) => {
case INACTIVE_USER: {
const supportLink = (
<Alert.Link href={context.supportLink}>
{intl.formatMessage(messages['logistration.contact.support.link'], { platformName: context.platformName })}
{intl.formatMessage(messages['contact.support.link'], { platformName: context.platformName })}
</Alert.Link>
);
errorList = (
@@ -70,7 +70,7 @@ const LoginFailureMessage = (props) => {
case INTERNAL_SERVER_ERROR:
errorList = (
<li key={INTERNAL_SERVER_ERROR}>
{intl.formatMessage(messages['authn.internal.server.error.message'])}
{intl.formatMessage(messages['internal.server.error.message'])}
</li>
);
break;
@@ -96,7 +96,7 @@ const LoginFailureMessage = (props) => {
return (
<Alert id="login-failure-alert" variant="danger">
<Alert.Heading>{intl.formatMessage(messages['logistration.login.failure.header.title'])}</Alert.Heading>
<Alert.Heading>{intl.formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
<ul>{errorList}</ul>
</Alert>
);

View File

@@ -5,7 +5,7 @@ import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import SwitchContent from './SwitchContent';
import SwitchContent from '../common-components/SwitchContent';
import {
LOGIN_PAGE,
REGISTER_PAGE,
@@ -24,28 +24,28 @@ const LoginHelpLinks = (props) => {
const forgotPasswordLink = () => (
<a className="field-link" href={RESET_PAGE}>
{intl.formatMessage(messages['logistration.forgot.password.link'])}
{intl.formatMessage(messages['forgot.password.link'])}
</a>
);
const signUpLink = () => (
<a className="field-link" href={REGISTER_PAGE}>
{intl.formatMessage(messages['logistration.register.link'])}
{intl.formatMessage(messages['register.link'])}
</a>
);
const loginIssueSupportURL = (config) => (config.LOGIN_ISSUE_SUPPORT_LINK
? (
<a className="field-link" href={config.LOGIN_ISSUE_SUPPORT_LINK}>
{intl.formatMessage(messages['logistration.other.sign.in.issues'])}
{intl.formatMessage(messages['other.sign.in.issues'])}
</a>
)
: null);
const getHelpButtonMessage = () => {
let mid = 'logistration.need.other.help.signing.in.collapsible.menu';
let mid = 'need.other.help.signing.in.collapsible.menu';
if (page === LOGIN_PAGE) {
mid = 'logistration.need.help.signing.in.collapsible.menu';
mid = 'need.help.signing.in.collapsible.menu';
}
return intl.formatMessage(messages[mid]);

View File

@@ -10,17 +10,18 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import AccountActivationMessage from './AccountActivationMessage';
import ConfirmationAlert from './ConfirmationAlert';
import { getThirdPartyAuthContext, loginRequest } from './data/actions';
import { loginErrorSelector, loginRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration';
import ConfirmationAlert from '../common-components/ConfirmationAlert';
import { loginRequest } from './data/actions';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { loginErrorSelector, loginRequestSelector } from './data/selectors';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import LoginHelpLinks from './LoginHelpLinks';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import SocialAuthProviders from './SocialAuthProviders';
import ThirdPartyAuthAlert from './ThirdPartyAuthAlert';
import { RedirectLogistration } from '../common-components';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
InstitutionLogistration,
} from '../common-components';
import {
DEFAULT_REDIRECT_URL, DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE, ENTERPRISE_LOGIN_URL, PENDING_STATE,
} from '../data/constants';
@@ -130,7 +131,7 @@ class LoginPage extends React.Component {
<RenderInstitutionButton
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={secondaryProviders}
buttonTitle={intl.formatMessage(messages['logistration.login.institution.login.button'])}
buttonTitle={intl.formatMessage(messages['institution.login.button'])}
/>
<div className="row tpa-container">
<SocialAuthProviders socialAuthProviders={providers} />
@@ -156,8 +157,8 @@ class LoginPage extends React.Component {
<InstitutionLogistration
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={thirdPartyAuthContext.secondaryProviders}
headingTitle={intl.formatMessage(messages['logistration.login.institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['logistration.login.institution.login.page.back.button'])}
headingTitle={intl.formatMessage(messages['institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['institution.login.page.back.button'])}
/>
);
}
@@ -183,24 +184,24 @@ class LoginPage extends React.Component {
{this.props.forgotPassword.status === 'complete' ? <ConfirmationAlert email={this.props.forgotPassword.email} /> : null}
<div className="d-flex flex-row">
<p>
{intl.formatMessage(messages['logistration.first.time.here'])}
{intl.formatMessage(messages['first.time.here'])}
<Hyperlink className="ml-1" destination={REGISTER_PAGE}>
{intl.formatMessage(messages['logistration.create.an.account'])}.
{intl.formatMessage(messages['create.an.account'])}.
</Hyperlink>
</p>
</div>
<h2 className="text-left mt-2 mb-3">
{intl.formatMessage(messages['logistration.sign.in.heading'])}
{intl.formatMessage(messages['sign.in.heading'])}
</h2>
<Form className="m-0">
<ValidationFormGroup
for="email"
invalid={this.state.errors.email !== ''}
invalidMessage={intl.formatMessage(messages['logistration.email.format.validation.message'])}
invalidMessage={intl.formatMessage(messages['email.format.validation.message'])}
className="mb-0 w-100"
>
<Form.Label htmlFor="loginEmail" className="h6 mr-1">
{intl.formatMessage(messages['logistration.login.page.email.label'])}
{intl.formatMessage(messages['email.label'])}
</Form.Label>
<Input
name="email"
@@ -210,16 +211,16 @@ class LoginPage extends React.Component {
value={this.state.email}
onChange={e => this.handleOnChange(e)}
/>
<p className="mb-4">{intl.formatMessage(messages['logistration.email.help.message'])}</p>
<p className="mb-4">{intl.formatMessage(messages['email.help.message'])}</p>
</ValidationFormGroup>
<ValidationFormGroup
for="password"
invalid={this.state.errors.password !== ''}
invalidMessage={intl.formatMessage(messages['logistration.login.page.password.validation.message'])}
invalidMessage={intl.formatMessage(messages['password.validation.message'])}
className="mb-0 w-100"
>
<Form.Label htmlFor="loginPassword" className="h6 mr-1">
{intl.formatMessage(messages['logistration.password'])}
{intl.formatMessage(messages['password.label'])}
</Form.Label>
<Input
name="password"
@@ -231,14 +232,14 @@ class LoginPage extends React.Component {
</ValidationFormGroup>
<LoginHelpLinks page={LOGIN_PAGE} />
<Hyperlink className="field-link mt-0 mb-3" destination={this.getEnterPriseLoginURL()}>
{intl.formatMessage(messages['logistration.enterprise.login.link.text'])}
{intl.formatMessage(messages['enterprise.login.link.text'])}
</Hyperlink>
<StatefulButton
type="submit"
className="btn-primary"
state={submitState}
labels={{
default: intl.formatMessage(messages['logistration.sign.in.button']),
default: intl.formatMessage(messages['sign.in.button']),
}}
onClick={this.handleSubmit}
/>
@@ -246,7 +247,7 @@ class LoginPage extends React.Component {
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
&& !currentProvider ? (
<div className="mb-4 pt-10">
<h4>{intl.formatMessage(messages['logistration.or.sign.in.with'])}</h4>
<h4>{intl.formatMessage(messages['or.sign.in.with'])}</h4>
</div>
) : null}
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
@@ -302,8 +303,8 @@ const mapStateToProps = state => {
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
const loginError = loginErrorSelector(state);
return {
submitState: state.logistration.submitState,
thirdPartyAuthApiStatus: state.logistration.thirdPartyAuthApiStatus,
submitState: state.login.submitState,
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
forgotPassword,
loginError,
loginResult,

23
src/login/data/actions.js Normal file
View File

@@ -0,0 +1,23 @@
import { AsyncActionType } from '../../data/utils';
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
// Login
export const loginRequest = creds => ({
type: LOGIN_REQUEST.BASE,
payload: { creds },
});
export const loginRequestBegin = () => ({
type: LOGIN_REQUEST.BEGIN,
});
export const loginRequestSuccess = (redirectUrl, success) => ({
type: LOGIN_REQUEST.SUCCESS,
payload: { redirectUrl, success },
});
export const loginRequestFailure = (loginError) => ({
type: LOGIN_REQUEST.FAILURE,
payload: { loginError },
});

View File

@@ -0,0 +1,33 @@
import { LOGIN_REQUEST } from './actions';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = {
loginError: null,
loginResult: {},
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case LOGIN_REQUEST.BEGIN:
return {
...state,
submitState: PENDING_STATE,
};
case LOGIN_REQUEST.SUCCESS:
return {
...state,
loginResult: action.payload,
};
case LOGIN_REQUEST.FAILURE:
return {
...state,
loginError: action.payload.loginError,
submitState: DEFAULT_STATE,
};
default:
return state;
}
};
export default reducer;

45
src/login/data/sagas.js Normal file
View File

@@ -0,0 +1,45 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { 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) {
if (statusCodes.includes(e.response.status)) {
yield put(loginRequestFailure(camelCaseObject(e.response.data)));
} else {
yield put(loginRequestFailure(camelCaseObject({ errorCode: INTERNAL_SERVER_ERROR })));
}
}
logError(e);
}
}
export default function* saga() {
yield takeEvery(LOGIN_REQUEST.BASE, handleLoginRequest);
}

View File

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

26
src/login/data/service.js Normal file
View File

@@ -0,0 +1,26 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import querystring from 'querystring';
// eslint-disable-next-line import/prefer-default-export
export async function loginRequest(creds) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/user_api/v1/account/login_session/`,
querystring.stringify(creds),
requestConfig,
)
.catch((e) => {
throw (e);
});
return {
redirectUrl: data.redirect_url || `${getConfig().LMS_BASE_URL}/dashboard`,
success: data.success || false,
};
}

View File

@@ -0,0 +1,99 @@
import { runSaga } from 'redux-saga';
import { camelCaseObject } from '@edx/frontend-platform';
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',
},
},
};
beforeEach(() => {
loggingService.logError.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',
},
},
};
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(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.loginRequestBegin(),
actions.loginRequestFailure(camelCaseObject(loginErrorResponse.response.data)),
]);
loginRequest.mockClear();
});
it('should handle 500 error code', async () => {
const loginErrorResponse = {
response: {
status: 500,
data: {
errorCode: 'internal-server-error',
},
},
};
const loginRequest = jest.spyOn(api, 'loginRequest').mockImplementation(() => Promise.reject(loginErrorResponse));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleLoginRequest,
params,
);
expect(dispatched).toEqual([
actions.loginRequestBegin(),
actions.loginRequestFailure(camelCaseObject(loginErrorResponse.response.data)),
]);
loginRequest.mockClear();
});
});

4
src/login/index.js Normal file
View File

@@ -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';

162
src/login/messages.jsx Normal file
View File

@@ -0,0 +1,162 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'sign.in.button': {
id: 'sign.in.button',
defaultMessage: 'Sign in',
description: 'Button label that appears on login page',
},
'need.help.signing.in.collapsible.menu': {
id: 'need.help.signing.in.collapsible.menu',
defaultMessage: 'Need help signing in?',
description: 'A button for collapsible need help signing in menu on login page',
},
'forgot.password.link': {
id: 'forgot.password.link',
defaultMessage: 'Forgot my password',
description: 'Forgot password link',
},
'other.sign.in.issues': {
id: 'other.sign.in.issues',
defaultMessage: 'Other sign-in issues',
description: 'A link that redirects to sign-in issues help',
},
'need.other.help.signing.in.collapsible.menu': {
id: 'need.other.help.signing.in.collapsible.menu',
defaultMessage: 'Need other help signing in?',
description: 'A button for collapsible need other help signing in menu on forgot password page',
},
'institution.login.button': {
id: 'institution.login.button',
defaultMessage: 'Use my university info',
description: 'shows institutions list',
},
'institution.login.page.title': {
id: 'institution.login.page.title',
defaultMessage: 'Sign in with Institution/Campus Credentials',
description: 'Heading of institution page',
},
'institution.login.page.sub.heading': {
id: 'institution.login.page.sub.heading',
defaultMessage: 'Choose your institution from the list below:',
description: 'Heading of the institutions list',
},
'institution.login.page.back.button': {
id: 'institution.login.page.back.button',
defaultMessage: 'Back to sign in',
description: 'return to login page',
},
'create.an.account': {
id: 'create.an.account',
defaultMessage: 'Create an Account',
description: 'Message on button to return to register page',
},
'institution.login.sign.in': {
id: 'institution.login.sign.in',
defaultMessage: 'Sign In',
description: 'Sign In text',
},
'or.sign.in.with': {
id: 'or.sign.in.with',
defaultMessage: 'or sign in with',
description: 'gives hint about other sign in options',
},
'non.compliant.password.title': {
id: 'non.compliant.password.title',
defaultMessage: 'We recently changed our password requirements',
description: 'A title that appears in bold before error message for non-compliant password',
},
'first.time.here': {
id: 'first.time.here',
defaultMessage: 'First time here?',
description: 'A question that appears before sign up link',
},
'email.label': {
id: 'email.label',
defaultMessage: 'Email',
description: 'Label that appears above email field',
},
'email.help.message': {
id: 'email.help.message',
defaultMessage: 'The email address you used to register with edX.',
description: 'Message that appears below email field on login page',
},
'enterprise.login.link.text': {
id: 'enterprise.login.link.text',
defaultMessage: 'Sign in with your company or school',
description: 'Company or school login link text.',
},
'email.format.validation.message': {
id: 'email.format.validation.message',
defaultMessage: 'The email address you\'ve provided isn\'t formatted correctly.',
description: 'Validation message that appears when email address format is incorrect',
},
'password.validation.message': {
id: 'password.validation.message',
defaultMessage: 'Please enter your password.',
description: 'Validation message that appears when password is empty',
},
'password.label': {
id: 'password.label',
defaultMessage: 'Password',
description: 'Text that appears above password field or as a placeholder',
},
'register.link': {
id: 'register.link',
defaultMessage: 'Create an account',
description: 'Register page link',
},
'sign.in.heading': {
id: 'sign.in.heading',
defaultMessage: 'Sign In',
description: 'Sign In text',
},
// Account Activation Strings
'account.activation.success.message.title': {
id: 'account.activation.success.message.title',
defaultMessage: 'Success! You have activated your account.',
description: 'Account Activation success message title',
},
'account.activation.success.message': {
id: 'account.activation.success.message',
defaultMessage: 'You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign In to continue.',
description: 'Message show to learners when their account has been activated successfully',
},
'account.already.activated.message': {
id: 'account.already.activated.message',
defaultMessage: 'This account has already been activated.',
description: 'Message shown when learner account has already been activated',
},
'account.activation.error.message.title': {
id: 'account.activation.error.message.title',
defaultMessage: 'Your account could not be activated',
description: 'Account Activation error message title',
},
'account.activation.support.link': {
id: 'account.activation.support.link',
defaultMessage: 'contact support',
description: 'Link text used in account activation error message to go to learner help center',
},
'internal.server.error.message': {
id: 'internal.server.error.message',
defaultMessage: 'An error has occurred. Try refreshing the page, or check your Internet connection.',
description: 'Error message that appears when server responds with 500 error code',
},
'login.rate.limit.reached.message': {
id: 'login.rate.limit.reached.message',
defaultMessage: 'Too many failed login attempts. Try again later.',
description: 'Error message that appears when an anonymous user has made too many failed login attempts',
},
'login.failure.header.title': {
id: 'login.failure.header.title',
defaultMessage: 'We couldn\'t sign you in.',
description: 'Login failure header message.',
},
'contact.support.link': {
id: 'contact.support.link',
defaultMessage: 'contact {platformName} Support',
description: 'Link text used in inactive user error message to go to learner help center',
},
});
export default messages;

View File

@@ -7,7 +7,7 @@ import configureStore from 'redux-mock-store';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import LoginPage from '../LoginPage';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import { RenderInstitutionButton } from '../../common-components';
import { PENDING_STATE } from '../../data/constants';
const IntlLoginPage = injectIntl(LoginPage);
@@ -16,9 +16,13 @@ const mockStore = configureStore();
describe('LoginPage', () => {
const initialState = {
forgotPassword: { status: null },
logistration: {
login: {
forgotPassword: { status: null },
loginResult: { success: false, redirectUrl: '' },
response_error: null,
},
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
@@ -68,8 +72,8 @@ describe('LoginPage', () => {
it('should match pending button state snapshot', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
login: {
...initialState.login,
submitState: PENDING_STATE,
},
});
@@ -92,10 +96,10 @@ describe('LoginPage', () => {
it('should match TPA provider snapshot', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
providers: [appleProvider],
},
},
@@ -108,8 +112,8 @@ describe('LoginPage', () => {
it('should show error message', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
login: {
...initialState.login,
loginError: { value: 'Email or password is incorrect.' },
},
});
@@ -156,8 +160,8 @@ describe('LoginPage', () => {
const dasboardUrl = 'http://test.com/testing-dashboard/';
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
login: {
...initialState.login,
loginResult: {
success: true,
redirectUrl: dasboardUrl,
@@ -174,14 +178,17 @@ describe('LoginPage', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
login: {
...initialState.login,
loginResult: {
success: true,
redirectUrl: '',
},
},
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
finishAuthUrl: authCompleteUrl,
},
},
@@ -197,10 +204,10 @@ describe('LoginPage', () => {
const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard';
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
providers: [{
...appleProvider,
loginUrl,
@@ -221,10 +228,10 @@ describe('LoginPage', () => {
it('should match third party auth alert', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: 'Apple',
platformName: 'edX',
},
@@ -241,10 +248,10 @@ describe('LoginPage', () => {
it('should display institution login button', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
@@ -261,10 +268,10 @@ describe('LoginPage', () => {
it('should display institution login page', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},

View File

@@ -345,25 +345,6 @@ exports[`LoginPage should match default section snapshot 1`] = `
</span>
</button>
</form>
<div
className="mb-4 pt-10"
>
<h4>
or sign in with
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
</div>
</div>
</div>
@@ -560,25 +541,6 @@ exports[`LoginPage should match forget password alert message snapshot 1`] = `
</span>
</button>
</form>
<div
className="mb-4 pt-10"
>
<h4>
or sign in with
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
</div>
</div>
</div>
@@ -746,25 +708,6 @@ exports[`LoginPage should match pending button state snapshot 1`] = `
</span>
</button>
</form>
<div
className="mb-4 pt-10"
>
<h4>
or sign in with
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
</div>
</div>
</div>
@@ -939,25 +882,6 @@ exports[`LoginPage should show error message 1`] = `
</span>
</button>
</form>
<div
className="mb-4 pt-10"
>
<h4>
or sign in with
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
</div>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import { createSelector } from 'reselect';
export const storeName = 'logistration';
export const logistrationSelector = state => ({ ...state[storeName] });
export const loginRequestSelector = createSelector(
logistrationSelector,
logistration => logistration.loginResult,
);
export const loginErrorSelector = createSelector(
logistrationSelector,
logistration => logistration.loginError,
);
export const registrationRequestSelector = createSelector(
logistrationSelector,
logistration => logistration.registrationResult,
);
export const thirdPartyAuthContextSelector = createSelector(
logistrationSelector,
logistration => logistration.thirdPartyAuthContext,
);

View File

@@ -1,285 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'logistration.sign.in.button': {
id: 'logistration.sign.in.button',
defaultMessage: 'Sign in',
description: 'Button label that appears on login page',
},
'logistration.create.account.button': {
id: 'logistration.create.account.button',
defaultMessage: 'Create Account',
description: 'Button label that appears on register page',
},
'logistration.need.help.signing.in.collapsible.menu': {
id: 'logistration.need.help.signing.in.collapsible.menu',
defaultMessage: 'Need help signing in?',
description: 'A button for collapsible need help signing in menu on login page',
},
'logistration.need.other.help.signing.in.collapsible.menu': {
id: 'logistration.need.other.help.signing.in.collapsible.menu',
defaultMessage: 'Need other help signing in?',
description: 'A button for collapsible need other help signing in menu on forgot password page',
},
'logistration.register.link': {
id: 'logistration.register.link',
defaultMessage: 'Create an account',
description: 'Register page link',
},
'logistration.forgot.password.link': {
id: 'logistration.forgot.password.link',
defaultMessage: 'Forgot my password',
description: 'Forgot password link',
},
'logistration.already.have.an.edx.account': {
id: 'logistration.already.have.an.edx.account',
defaultMessage: 'Already have an edX account?',
description: 'A message on registration page asking the user if he already has an edX account',
},
'logistration.sign.in.hyperlink': {
id: 'logistration.sign.in.hyperlink',
defaultMessage: ' Sign in.',
description: 'Text for the hyperlink that takes user to login page',
},
'logistration.create.an.account.using': {
id: 'logistration.create.an.account.using',
defaultMessage: 'Create an account using',
description: 'A message that appears before social auth buttons',
},
'logistration.create.a.new.one.here': {
id: 'logistration.create.a.new.one.here',
defaultMessage: 'or create a new one here',
description: 'Text that appears after social auth buttons and before the registration form',
},
'logistration.other.sign.in.issues': {
id: 'logistration.other.sign.in.issues',
defaultMessage: 'Other sign-in issues',
description: 'A link that redirects to sign-in issues help',
},
'logistration.login.institution.login.button': {
id: 'logistration.login.institution.login.button',
defaultMessage: 'Use my university info',
description: 'shows institutions list',
},
'logistration.login.institution.login.page.title': {
id: 'logistration.login.institution.login.page.title',
defaultMessage: 'Sign in with Institution/Campus Credentials',
description: 'Heading of institution page',
},
'logistration.institution.login.page.sub.heading': {
id: 'logistration.institution.login.page.sub.heading',
defaultMessage: 'Choose your institution from the list below:',
description: 'Heading of the institutions list',
},
'logistration.login.institution.login.page.back.button': {
id: 'logistration.login.institution.login.page.back.button',
defaultMessage: 'Back to sign in',
description: 'return to login page',
},
'logistration.register.institution.login.button': {
id: 'logistration.register.institution.login.button',
defaultMessage: 'Use my institution/campus credentials',
description: 'shows institutions list',
},
'logistration.register.institution.login.page.title': {
id: 'logistration.register.institution.login.page.title',
defaultMessage: 'Register with Institution/Campus Credentials',
description: 'Heading of institution page',
},
'logistration.create.an.account': {
id: 'logistration.create.an.account',
defaultMessage: 'Create an Account',
description: 'Message on button to return to register page',
},
'logistration.sign.in.heading': {
id: 'logistration.sign.in.heading',
defaultMessage: 'Sign In',
description: 'Sign In text',
},
'logistration.or.sign.in.with': {
id: 'logistration.or.sign.in.with',
defaultMessage: 'or sign in with',
description: 'gives hint about other sign in options',
},
'logistration.non.compliant.password.title': {
id: 'logistration.non.compliant.password.title',
defaultMessage: 'We recently changed our password requirements',
description: 'A title that appears in bold before error message for non-compliant password',
},
'logistration.first.time.here': {
id: 'logistration.first.time.here',
defaultMessage: 'First time here?',
description: 'A question that appears before sign up link',
},
'logistration.login.page.email.label': {
id: 'logistration.login.page.email.label',
defaultMessage: 'Email',
description: 'Label that appears above email field',
},
'logistration.register.page.email.label': {
id: 'logistration.register.page.email.label',
defaultMessage: 'Email (required)',
description: 'Label that appears above email field on register page',
},
'logistration.email.format.validation.message': {
id: 'logistration.email.format.validation.message',
defaultMessage: 'The email address you\'ve provided isn\'t formatted correctly.',
description: 'Validation message that appears when email address format is incorrect',
},
'logistration.email.validation.message': {
id: 'logistration.email.validation.message',
defaultMessage: 'Please enter your Email.',
description: 'Validation message that appears when email address is empty',
},
'logistration.email.ratelimit.less.chars.validation.message': {
id: 'logistration.email.ratelimit.less.chars.validation.message',
defaultMessage: 'Email must have 3 characters.',
description: 'Validation message that appears when email address is less than 3 characters',
},
'logistration.email.ratelimit.incorrect.format.validation.message': {
id: 'logistration.email.ratelimit.incorrect.format.validation.message',
defaultMessage: 'The email address you provided isn\'t formatted correctly.',
description: 'Validation message that appears when email address is not formatted correctly with no backend validations available.',
},
'logistration.email.ratelimit.password.validation.message': {
id: 'logistration.email.ratelimit.password.validation.message',
defaultMessage: 'Your password must contain at least 8 characters',
description: 'Validation message that appears when password is not formatted correctly with no backend validations available.',
},
'logistration.email.help.message': {
id: 'logistration.email.help.message',
defaultMessage: 'The email address you used to register with edX.',
description: 'Message that appears below email field on login page',
},
'logistration.password': {
id: 'logistration.password',
defaultMessage: 'Password',
description: 'Text that appears above password field or as a placeholder',
},
'logistration.password.label': {
id: 'logistration.password.label',
defaultMessage: 'Password (required)',
description: 'Label that appears above password field',
},
'logistration.login.page.password.validation.message': {
id: 'logistration.login.page.password.validation.message',
defaultMessage: 'Please enter your password.',
description: 'Validation message that appears when password is empty',
},
'logistration.register.page.password.validation.message': {
id: 'logistration.register.page.password.validation.message',
defaultMessage: 'Please enter your Password.',
description: 'Validation message that appears when password is non compliant with edX requirement',
},
'logistration.fullname.label': {
id: 'logistration.fullname.label',
defaultMessage: 'Full Name (required)',
description: 'Label that appears above fullname field',
},
'logistration.fullname.validation.message': {
id: 'logistration.fullname.validation.message',
defaultMessage: 'Please enter your Full Name.',
description: 'Validation message that appears when fullname is empty',
},
'logistration.username.label': {
id: 'logistration.username.label',
defaultMessage: 'Public Username (required)',
description: 'Label that appears above username field',
},
'logistration.username.validation.message': {
id: 'logistration.username.validation.message',
defaultMessage: 'Please enter your Public Username.',
description: 'Validation message that appears when username is invalid',
},
'logistration.username.ratelimit.less.chars.message': {
id: 'logistration.username.ratelimit.less.chars.message',
defaultMessage: 'Public Username must have atleast 2 characters.',
description: 'Validation message that appears when username is less than 2 characters and with no backend validations available.',
},
'logistration.country.validation.message': {
id: 'logistration.country.validation.message',
defaultMessage: 'Select your country or region of residence.',
description: 'Validation message that appears when country is not selected',
},
'logistration.support.education.research': {
id: 'logistration.support.education.research',
defaultMessage: 'Support education research by providing additional information',
description: 'Text for a checkbox to ask user for if they are willing to provide extra information for education research',
},
'logistration.register.optional.label': {
id: 'logistration.register.optional.label',
defaultMessage: '(optional)',
description: 'Text that appears with optional field labels',
},
'logistration.enterprise.login.link.text': {
id: 'logistration.enterprise.login.link.text',
defaultMessage: 'Sign in with your company or school',
description: 'Company or school login link text.',
},
// Account Activation Strings
'authn.account.activation.success.message.title': {
id: 'authn.account.activation.success.message.title',
defaultMessage: 'Success! You have activated your account.',
description: 'Account Activation success message title',
},
'authn.account.activation.success.message': {
id: 'authn.account.activation.success.message',
defaultMessage: 'You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign In to continue.',
description: 'Message show to learners when their account has been activated successfully',
},
'authn.account.already.activated.message': {
id: 'authn.account.already.activated.message',
defaultMessage: 'This account has already been activated.',
description: 'Message shown when learner account has already been activated',
},
'authn.account.activation.error.message.title': {
id: 'authn.account.activation.error.message.title',
defaultMessage: 'Your account could not be activated',
description: 'Account Activation error message title',
},
'authn.account.activation.support.link': {
id: 'authn.account.activation.support.link',
defaultMessage: 'contact support',
description: 'Link text used in account activation error message to go to learner help center',
},
// Confirmation Alert Message
'authn.forgot.password.confirmation.title': {
id: 'authn.forgot.password.confirmation.title',
defaultMessage: 'Check Your Email',
description: 'Forgot password confirmation message title',
},
'authn.forgot.password.confirmation.support.link': {
id: 'authn.forgot.password.confirmation.support.link',
defaultMessage: 'contact technical support',
description: 'Technical support link text',
},
'authn.forgot.password.confirmation.info': {
id: 'authn.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',
},
// Login Failure Messages
'logistration.login.failure.header.title': {
id: 'logistration.login.failure.header.title',
defaultMessage: 'We couldn\'t sign you in.',
description: 'Login failure header message.',
},
'logistration.contact.support.link': {
id: 'logistration.contact.support.link',
defaultMessage: 'contact {platformName} Support',
description: 'Link text used in inactive user error message to go to learner help center',
},
'login.rate.limit.reached.message': {
id: 'login.rate.limit.reached.message',
defaultMessage: 'Too many failed login attempts. Try again later.',
description: 'Error message that appears when an anonymous user has made too many failed login attempts',
},
'authn.internal.server.error.message': {
id: 'authn.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',
},
});
export default messages;

View File

@@ -32,7 +32,7 @@ const RegistrationFailureMessage = (props) => {
<Alert variant="danger">
<Alert.Heading className="text-danger">
<FormattedMessage
id="logistration.registration.request.failure.header.description.message"
id="registration.request.failure.header.description.message"
defaultMessage="We couldn't create your account."
description="error message when registration failure."
/>

View File

@@ -15,14 +15,18 @@ import {
} from '@edx/frontend-platform/i18n';
import camelCase from 'lodash.camelcase';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import {
getThirdPartyAuthContext,
registerNewUser,
fetchRegistrationForm,
fetchRealtimeValidations,
} from './data/actions';
import { registrationRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
import { RedirectLogistration } from '../common-components';
import { registrationRequestSelector } from './data/selectors';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
InstitutionLogistration,
} from '../common-components';
import RegistrationFailure from './RegistrationFailure';
import {
DEFAULT_REDIRECT_URL,
@@ -34,9 +38,6 @@ import {
REGISTRATION_OPTIONAL_MAP,
REGISTRATION_EXTRA_FIELDS,
} from '../data/constants';
import SocialAuthProviders from './SocialAuthProviders';
import ThirdPartyAuthAlert from './ThirdPartyAuthAlert';
import InstitutionLogistration, { RenderInstitutionButton } from './InstitutionLogistration';
import messages from './messages';
import { processLink } from '../data/utils/dataUtils';
@@ -262,11 +263,11 @@ class RegistrationPage extends React.Component {
if (this.props.statusCode !== 403 && validations && validations.email) {
validationErrorsAlertMessages.email = [{ user_message: validations.email }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.email.validation.message');
const errorEmpty = this.generateUserMessage(value.length < 1, 'email.validation.message');
validationErrorsAlertMessages.email = errorEmpty;
} else {
const errorCharlength = this.generateUserMessage(value.length <= 2, 'logistration.email.ratelimit.less.chars.validation.message');
const formatError = this.generateUserMessage(!value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i), 'logistration.email.ratelimit.incorrect.format.validation.message');
const errorCharlength = this.generateUserMessage(value.length <= 2, 'email.ratelimit.less.chars.validation.message');
const formatError = this.generateUserMessage(!value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i), 'email.ratelimit.incorrect.format.validation.message');
validationErrorsAlertMessages.email = errorCharlength;
validationErrorsAlertMessages.emailFormat = formatError;
}
@@ -275,7 +276,7 @@ class RegistrationPage extends React.Component {
if (this.props.statusCode !== 403 && validations && validations.name) {
validationErrorsAlertMessages.name = [{ user_message: validations.name }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.fullname.validation.message');
const errorEmpty = this.generateUserMessage(value.length < 1, 'fullname.validation.message');
validationErrorsAlertMessages.name = errorEmpty;
} else {
validationErrorsAlertMessages.name = [{ user_message: '' }];
@@ -285,10 +286,10 @@ class RegistrationPage extends React.Component {
if (this.props.statusCode !== 403 && validations && validations.username) {
validationErrorsAlertMessages.username = [{ user_message: validations.username }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.username.validation.message');
const errorEmpty = this.generateUserMessage(value.length < 1, 'username.validation.message');
validationErrorsAlertMessages.username = errorEmpty;
} else {
const errorCharLength = this.generateUserMessage(value.length <= 1, 'logistration.username.ratelimit.less.chars.message');
const errorCharLength = this.generateUserMessage(value.length <= 1, 'username.ratelimit.less.chars.message');
validationErrorsAlertMessages.username = errorCharLength;
}
break;
@@ -296,10 +297,10 @@ class RegistrationPage extends React.Component {
if (this.props.statusCode !== 403 && validations && validations.password) {
validationErrorsAlertMessages.password = [{ user_message: validations.password }];
} else if (value.length < 1) {
const errorEmpty = this.generateUserMessage(value.length < 1, 'logistration.register.page.password.validation.message');
const errorEmpty = this.generateUserMessage(value.length < 1, 'register.page.password.validation.message');
validationErrorsAlertMessages.password = errorEmpty;
} else {
const errorCharLength = this.generateUserMessage(value.length < 8, 'logistration.email.ratelimit.password.validation.message');
const errorCharLength = this.generateUserMessage(value.length < 8, 'email.ratelimit.password.validation.message');
validationErrorsAlertMessages.password = errorCharLength;
}
break;
@@ -307,7 +308,7 @@ class RegistrationPage extends React.Component {
if (this.props.statusCode !== 403 && validations && validations.country) {
validationErrorsAlertMessages.country = [{ user_message: validations.country }];
} else {
const emptyError = this.generateUserMessage(value === '', 'logistration.country.validation.message');
const emptyError = this.generateUserMessage(value === '', 'country.validation.message');
validationErrorsAlertMessages.country = emptyError;
}
break;
@@ -469,7 +470,7 @@ class RegistrationPage extends React.Component {
className={cssClass}
>
<label htmlFor={field.name} className="h6 pt-10">
{field.label} {this.props.intl.formatMessage(messages['logistration.register.optional.label'])}
{field.label} {this.props.intl.formatMessage(messages['register.optional.label'])}
</label>
<Input {...props} />
</ValidationFormGroup>
@@ -493,13 +494,13 @@ class RegistrationPage extends React.Component {
<RenderInstitutionButton
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
buttonTitle={intl.formatMessage(messages['logistration.register.institution.login.button'])}
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
/>
<div className="row tpa-container">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
</div>
<h4 className="d-block mx-auto mt-4">
{intl.formatMessage(messages['logistration.create.a.new.one.here'])}
{intl.formatMessage(messages['create.a.new.one.here'])}
</h4>
</>
);
@@ -536,8 +537,8 @@ class RegistrationPage extends React.Component {
<InstitutionLogistration
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
headingTitle={intl.formatMessage(messages['logistration.register.institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['logistration.create.an.account'])}
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['create.an.account'])}
/>
);
}
@@ -561,17 +562,17 @@ class RegistrationPage extends React.Component {
/>
)}
<div className="text-left">
<span>{intl.formatMessage(messages['logistration.already.have.an.edx.account'])}</span>
<a href={LOGIN_PAGE}>{intl.formatMessage(messages['logistration.sign.in.hyperlink'])}</a>
<span>{intl.formatMessage(messages['already.have.an.edx.account'])}</span>
<a href={LOGIN_PAGE}>{intl.formatMessage(messages['sign.in.hyperlink'])}</a>
</div>
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
&& !currentProvider ? (
<div className="d-block mb-4 mt-4">
<h4 className="d-block mx-auto mb-4">
{intl.formatMessage(messages['logistration.create.an.account.using'])}
{intl.formatMessage(messages['create.an.account.using'])}
</h4>
</div>
) : <h2 className="mt-4">{intl.formatMessage(messages['logistration.create.an.account'])}</h2>}
) : <h2 className="mt-4">{intl.formatMessage(messages['create.an.account'])}</h2>}
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
<Form className="mb-4 form-group">
<ValidationFormGroup
@@ -582,7 +583,7 @@ class RegistrationPage extends React.Component {
helpText="This name will be used by any certificates that you earn."
>
<label htmlFor="name" className="h6 pt-10">
{intl.formatMessage(messages['logistration.fullname.label'])}
{intl.formatMessage(messages['fullname.label'])}
</label>
<Input
name="name"
@@ -604,7 +605,7 @@ class RegistrationPage extends React.Component {
helpText="The name that will identify you in your courses. It cannot be changed later."
>
<label htmlFor="username" className="h6 pt-10">
{intl.formatMessage(messages['logistration.username.label'])}
{intl.formatMessage(messages['username.label'])}
</label>
<Input
name="username"
@@ -627,7 +628,7 @@ class RegistrationPage extends React.Component {
helpText="This is what you will use to login."
>
<label htmlFor="email" className="h6 pt-10">
{intl.formatMessage(messages['logistration.register.page.email.label'])}
{intl.formatMessage(messages['register.page.email.label'])}
</label>
<Input
name="email"
@@ -650,7 +651,7 @@ class RegistrationPage extends React.Component {
helpText="Your password must contain at least 8 characters, including 1 letter & 1 number."
>
<label htmlFor="password" className="h6 pt-10">
{intl.formatMessage(messages['logistration.password.label'])}
{intl.formatMessage(messages['password.label'])}
</label>
<Input
name="password"
@@ -680,7 +681,7 @@ class RegistrationPage extends React.Component {
required
/>
<p role="presentation" id="additionalFields" className="mb-0" onClick={e => this.handleOnOptional(e)}>
{intl.formatMessage(messages['logistration.support.education.research'])}
{intl.formatMessage(messages['support.education.research'])}
</p>
</ValidationFormGroup>
{ this.state.enableOptionalField ? this.addExtraOptionalFields() : null}
@@ -689,7 +690,7 @@ class RegistrationPage extends React.Component {
className="btn-primary mt-10"
state={submitState}
labels={{
default: intl.formatMessage(messages['logistration.create.account.button']),
default: intl.formatMessage(messages['create.account.button']),
}}
onClick={this.handleSubmit}
/>
@@ -770,14 +771,14 @@ const mapStateToProps = state => {
const registrationResult = registrationRequestSelector(state);
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
return {
registrationError: state.logistration.registrationError,
submitState: state.logistration.submitState,
thirdPartyAuthApiStatus: state.logistration.thirdPartyAuthApiStatus,
registrationError: state.register.registrationError,
submitState: state.register.submitState,
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
registrationResult,
thirdPartyAuthContext,
formData: state.logistration.formData,
validations: state.logistration.validations,
statusCode: state.logistration.statusCode,
formData: state.register.formData,
validations: state.register.validations,
statusCode: state.register.statusCode,
};
};

View File

@@ -1,8 +1,6 @@
import { AsyncActionType } from '../../data/utils';
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
export const REGISTER_FORM = new AsyncActionType('REGISTRATION', 'GET_FORM_FIELDS');
export const REGISTER_FORM_VALIDATIONS = new AsyncActionType('REGISTRATION', 'GET_FORM_VALIDATIONS');
@@ -28,45 +26,6 @@ export const registerNewUserFailure = (error) => ({
payload: { error },
});
// Login
export const loginRequest = creds => ({
type: LOGIN_REQUEST.BASE,
payload: { creds },
});
export const loginRequestBegin = () => ({
type: LOGIN_REQUEST.BEGIN,
});
export const loginRequestSuccess = (redirectUrl, success) => ({
type: LOGIN_REQUEST.SUCCESS,
payload: { redirectUrl, success },
});
export const loginRequestFailure = (loginError) => ({
type: LOGIN_REQUEST.FAILURE,
payload: { loginError },
});
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
payload: { urlParams },
});
export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});
// Registration Form Fields
export const fetchRegistrationForm = () => ({
type: REGISTER_FORM.BASE,

View File

@@ -1,16 +1,12 @@
import {
REGISTER_NEW_USER,
LOGIN_REQUEST,
THIRD_PARTY_AUTH_CONTEXT,
REGISTER_FORM,
REGISTER_FORM_VALIDATIONS,
} from './actions';
import { DEFAULT_STATE, PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = {
loginError: null,
loginResult: {},
registrationError: null,
registrationResult: {},
formData: null,
@@ -36,38 +32,6 @@ const reducer = (state = defaultState, action) => {
registrationError: action.payload.error,
submitState: DEFAULT_STATE,
};
case LOGIN_REQUEST.BEGIN:
return {
...state,
submitState: PENDING_STATE,
};
case LOGIN_REQUEST.SUCCESS:
return {
...state,
loginResult: action.payload,
};
case LOGIN_REQUEST.FAILURE:
return {
...state,
loginError: action.payload.loginError,
submitState: DEFAULT_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
case REGISTER_FORM.BEGIN:
return {
...state,

View File

@@ -1,8 +1,6 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
// Actions
import {
@@ -10,18 +8,10 @@ import {
registerNewUserBegin,
registerNewUserFailure,
registerNewUserSuccess,
LOGIN_REQUEST,
loginRequestBegin,
loginRequestFailure,
loginRequestSuccess,
REGISTER_FORM_VALIDATIONS,
fetchRealtimeValidationsBegin,
fetchRealtimeValidationsSuccess,
fetchRealtimeValidationsFailure,
THIRD_PARTY_AUTH_CONTEXT,
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextSuccess,
getThirdPartyAuthContextFailure,
REGISTER_FORM,
fetchRegistrationFormBegin,
fetchRegistrationFormSuccess,
@@ -32,9 +22,7 @@ import {
import {
getFieldsValidations,
getRegistrationForm,
getThirdPartyAuthContext,
registerRequest,
loginRequest,
} from './service';
export function* handleNewUserRegistration(action) {
@@ -56,46 +44,6 @@ export function* handleNewUserRegistration(action) {
}
}
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)));
} else if (status === 403) {
yield put(loginRequestFailure({ errorCode: FORBIDDEN_REQUEST }));
} else {
yield put(loginRequestFailure({ errorCode: INTERNAL_SERVER_ERROR }));
}
}
logError(e);
}
}
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(getThirdPartyAuthContextSuccess(
thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export function* fetchRegistrationForm() {
try {
yield put(fetchRegistrationFormBegin());
@@ -129,8 +77,6 @@ export function* fetchRealtimeValidations(action) {
export default function* saga() {
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
yield takeEvery(LOGIN_REQUEST.BASE, handleLoginRequest);
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
yield takeEvery(REGISTER_FORM.BASE, fetchRegistrationForm);
yield takeEvery(REGISTER_FORM_VALIDATIONS.BASE, fetchRealtimeValidations);
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
export const storeName = 'register';
export const registerSelector = state => ({ ...state[storeName] });
export const registrationRequestSelector = createSelector(
registerSelector,
register => register.registrationResult,
);

View File

@@ -1,4 +1,4 @@
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import querystring from 'querystring';
@@ -24,48 +24,6 @@ export async function registerRequest(registrationInformation) {
};
}
export async function loginRequest(creds) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/user_api/v1/account/login_session/`,
querystring.stringify(creds),
requestConfig,
)
.catch((e) => {
throw (e);
});
return {
redirectUrl: data.redirect_url || `${getConfig().LMS_BASE_URL}/dashboard`,
success: data.success || false,
};
}
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' })),
};
}
export async function getRegistrationForm() {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },

View File

@@ -1,78 +1,16 @@
import { runSaga } from 'redux-saga';
import { camelCaseObject } from '@edx/frontend-platform';
import * as actions from '../actions';
import {
fetchRealtimeValidations,
fetchThirdPartyAuthContext,
fetchRegistrationForm,
handleLoginRequest,
handleNewUserRegistration,
} from '../sagas';
import * as api from '../service';
import initializeMockLogging from '../../../setupTest';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from '../constants';
const { loggingService } = initializeMockLogging();
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
};
const data = {
currentProvider: null,
providers: [],
secondaryProviders: [],
finishAuthUrl: null,
pipelineUserDetails: {},
};
beforeEach(() => {
loggingService.logError.mockReset();
});
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextSuccess(data),
]);
getThirdPartyAuthContext.mockClear();
});
it('should call service and dispatch error action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextFailure(),
]);
getThirdPartyAuthContext.mockClear();
});
});
describe('fetchRegistrationForm', () => {
const data = {
fields: [{
@@ -212,105 +150,6 @@ describe('fetchRealtimeValidations', () => {
});
});
describe('handleLoginRequest', () => {
const params = {
payload: {
formData: {
email: 'test@test.com',
password: 'test-password',
},
},
};
const testErrorResponse = async (loginErrorResponse, 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(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual(expectedDispatchers);
loginRequest.mockClear();
};
beforeEach(() => {
loggingService.logError.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, [
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, [
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, [
actions.loginRequestBegin(),
actions.loginRequestFailure(loginErrorResponse.response.data),
]);
});
});
describe('handleNewUserRegistration', () => {
const params = {
payload: {

View File

@@ -1,4 +1,3 @@
export { default as LoginPage } from './LoginPage';
export { default as RegistrationPage } from './RegistrationPage';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';

121
src/register/messages.jsx Normal file
View File

@@ -0,0 +1,121 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'create.account.button': {
id: 'create.account.button',
defaultMessage: 'Create Account',
description: 'Button label that appears on register page',
},
'already.have.an.edx.account': {
id: 'already.have.an.edx.account',
defaultMessage: 'Already have an edX account?',
description: 'A message on registration page asking the user if he already has an edX account',
},
'sign.in.hyperlink': {
id: 'sign.in.hyperlink',
defaultMessage: ' Sign in.',
description: 'Text for the hyperlink that takes user to login page',
},
'create.an.account.using': {
id: 'create.an.account.using',
defaultMessage: 'Create an account using',
description: 'A message that appears before social auth buttons',
},
'create.a.new.one.here': {
id: 'create.a.new.one.here',
defaultMessage: 'or create a new one here',
description: 'Text that appears after social auth buttons and before the registration form',
},
'register.institution.login.button': {
id: 'register.institution.login.button',
defaultMessage: 'Use my institution/campus credentials',
description: 'shows institutions list',
},
'register.institution.login.page.title': {
id: 'register.institution.login.page.title',
defaultMessage: 'Register with Institution/Campus Credentials',
description: 'Heading of institution page',
},
'create.an.account': {
id: 'create.an.account',
defaultMessage: 'Create an Account',
description: 'Message on button to return to register page',
},
'register.page.email.label': {
id: 'register.page.email.label',
defaultMessage: 'Email (required)',
description: 'Label that appears above email field on register page',
},
'email.validation.message': {
id: 'email.validation.message',
defaultMessage: 'Please enter your Email.',
description: 'Validation message that appears when email address is empty',
},
'email.ratelimit.less.chars.validation.message': {
id: 'email.ratelimit.less.chars.validation.message',
defaultMessage: 'Email must have 3 characters.',
description: 'Validation message that appears when email address is less than 3 characters',
},
'email.ratelimit.incorrect.format.validation.message': {
id: 'email.ratelimit.incorrect.format.validation.message',
defaultMessage: 'The email address you provided isn\'t formatted correctly.',
description: 'Validation message that appears when email address is not formatted correctly with no backend validations available.',
},
'email.ratelimit.password.validation.message': {
id: 'email.ratelimit.password.validation.message',
defaultMessage: 'Your password must contain at least 8 characters',
description: 'Validation message that appears when password is not formatted correctly with no backend validations available.',
},
'password.label': {
id: 'password.label',
defaultMessage: 'Password (required)',
description: 'Label that appears above password field',
},
'register.page.password.validation.message': {
id: 'register.page.password.validation.message',
defaultMessage: 'Please enter your Password.',
description: 'Validation message that appears when password is non compliant with edX requirement',
},
'fullname.label': {
id: 'fullname.label',
defaultMessage: 'Full Name (required)',
description: 'Label that appears above fullname field',
},
'fullname.validation.message': {
id: 'fullname.validation.message',
defaultMessage: 'Please enter your Full Name.',
description: 'Validation message that appears when fullname is empty',
},
'username.label': {
id: 'username.label',
defaultMessage: 'Public Username (required)',
description: 'Label that appears above username field',
},
'username.validation.message': {
id: 'username.validation.message',
defaultMessage: 'Please enter your Public Username.',
description: 'Validation message that appears when username is invalid',
},
'username.ratelimit.less.chars.message': {
id: 'username.ratelimit.less.chars.message',
defaultMessage: 'Public Username must have atleast 2 characters.',
description: 'Validation message that appears when username is less than 2 characters and with no backend validations available.',
},
'country.validation.message': {
id: 'country.validation.message',
defaultMessage: 'Select your country or region of residence.',
description: 'Validation message that appears when country is not selected',
},
'support.education.research': {
id: 'support.education.research',
defaultMessage: 'Support education research by providing additional information',
description: 'Text for a checkbox to ask user for if they are willing to provide extra information for education research',
},
'register.optional.label': {
id: 'register.optional.label',
defaultMessage: '(optional)',
description: 'Text that appears with optional field labels',
},
});
export default messages;

View File

@@ -7,7 +7,7 @@ import { getConfig } from '@edx/frontend-platform';
import { IntlProvider, injectIntl, configure } from '@edx/frontend-platform/i18n';
import RegistrationPage from '../RegistrationPage';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import { RenderInstitutionButton } from '../../common-components';
import { PENDING_STATE } from '../../data/constants';
import { fetchRegistrationForm, fetchRealtimeValidations, registerNewUser } from '../data/actions';
@@ -16,15 +16,8 @@ const mockStore = configureStore();
describe('./RegistrationPage.js', () => {
const initialState = {
logistration: {
register: {
registrationResult: { success: false, redirectUrl: '' },
thirdPartyAuthContext: {
platformName: 'openedX',
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
registrationError: null,
formData: {
fields: [{
@@ -38,6 +31,16 @@ describe('./RegistrationPage.js', () => {
}],
},
},
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
platformName: 'openedX',
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
},
};
let props = {};
@@ -94,11 +97,11 @@ describe('./RegistrationPage.js', () => {
};
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
register: {
...initialState.register,
formData: {
fields: [
...initialState.logistration.formData.fields,
...initialState.register.formData.fields,
{
label: 'The country or region where you live.',
name: 'country',
@@ -141,8 +144,8 @@ describe('./RegistrationPage.js', () => {
it('should show optional fields section on optional check enabled', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
register: {
...initialState.register,
formData: {
fields: [
{
@@ -299,8 +302,8 @@ describe('./RegistrationPage.js', () => {
it('should match pending button state snapshot', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
register: {
...initialState.register,
submitState: PENDING_STATE,
},
});
@@ -312,9 +315,10 @@ describe('./RegistrationPage.js', () => {
it('should match TPA provider snapshot', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [appleProvider],
},
},
@@ -327,10 +331,10 @@ describe('./RegistrationPage.js', () => {
it('should display no password field when current provider is present', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: 'Google',
},
},
@@ -362,8 +366,8 @@ describe('./RegistrationPage.js', () => {
const dasboardUrl = 'http://test.com/testing-dashboard/';
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
register: {
...initialState.register,
registrationResult: {
success: true,
redirectUrl: dasboardUrl,
@@ -379,10 +383,10 @@ describe('./RegistrationPage.js', () => {
it('should display institution register button', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
@@ -394,10 +398,10 @@ describe('./RegistrationPage.js', () => {
it('should not display institution register button', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
@@ -411,14 +415,17 @@ describe('./RegistrationPage.js', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
register: {
...initialState.register,
registrationResult: {
success: true,
redirectUrl: '',
},
},
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
finishAuthUrl: authCompleteUrl,
},
},
@@ -435,14 +442,18 @@ describe('./RegistrationPage.js', () => {
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [{
...appleProvider,
registerUrl,
}],
},
},
register: {
...initialState.register,
formData: {
fields: [{
label: 'I agree to the Your Platform Name Here <a href="/honor" rel="noopener" target="_blank">Honor Code</a>',
@@ -468,10 +479,10 @@ describe('./RegistrationPage.js', () => {
it('should match third party auth alert', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.logistration.thirdPartyAuthContext,
...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: 'Apple',
},
},
@@ -494,8 +505,8 @@ describe('./RegistrationPage.js', () => {
store = mockStore({
...initialState,
logistration: {
...initialState.logistration,
register: {
...initialState.register,
registrationError: {
email: [
{

View File

@@ -36,28 +36,6 @@ exports[`./RegistrationPage.js should display no password field when current pro
>
Create an Account
</h2>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
<form
className="mb-4 form-group"
>
@@ -499,37 +477,11 @@ exports[`./RegistrationPage.js should match default section snapshot 1`] = `
Sign in.
</a>
</div>
<div
className="d-block mb-4 mt-4"
<h2
className="mt-4"
>
<h4
className="d-block mx-auto mb-4"
>
Create an account using
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
Create an Account
</h2>
<form
className="mb-4 form-group"
>
@@ -745,37 +697,11 @@ exports[`./RegistrationPage.js should match pending button state snapshot 1`] =
Sign in.
</a>
</div>
<div
className="d-block mb-4 mt-4"
<h2
className="mt-4"
>
<h4
className="d-block mx-auto mb-4"
>
Create an account using
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
Create an Account
</h2>
<form
className="mb-4 form-group"
>
@@ -1026,37 +952,11 @@ exports[`./RegistrationPage.js should show error message on 409 1`] = `
Sign in.
</a>
</div>
<div
className="d-block mb-4 mt-4"
<h2
className="mt-4"
>
<h4
className="d-block mx-auto mb-4"
>
Create an account using
</h4>
</div>
<span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
<span
className="react-loading-skeleton css-1vmnjpn-skeletonStyles-Skeleton"
style={
Object {
"height": 36,
}
}
>
</span>
</span>
Create an Account
</h2>
<form
className="mb-4 form-group"
>

View File

@@ -6,9 +6,9 @@ const InvalidTokenMessage = () => {
const loginPasswordLink = (
<Hyperlink destination="/login">
<FormattedMessage
id="logistration.forgot.password.confirmation.support.link"
id="forgot.password.confirmation.support.link"
defaultMessage="sign-in"
description="link text used in message: logistration.reset.password.request.invalid.token.description.message link 'sign-in.'"
description="link text used in message: reset.password.request.invalid.token.description.message link 'sign-in.'"
/>
</Hyperlink>
);
@@ -16,7 +16,7 @@ const InvalidTokenMessage = () => {
const forgotPassword = (
<strong>
<FormattedMessage
id="logistration.reset.password.request.forgot.password.text"
id="reset.password.request.forgot.password.text"
defaultMessage="Forgot Password"
description="Forgot password page help text."
/>
@@ -29,13 +29,13 @@ const InvalidTokenMessage = () => {
<Alert variant="danger">
<Alert.Heading className="text-danger">
<FormattedMessage
id="logistration.reset.password.request.invalid.token.header.message"
id="reset.password.request.invalid.token.header.message"
defaultMessage="Invalid Password Reset Link"
description="Invalid password reset link help text heading."
/>
</Alert.Heading>
<FormattedMessage
id="logistration.reset.password.request.invalid.token.description.message"
id="reset.password.request.invalid.token.description.message"
defaultMessage="This password reset link is invalid. It may have been used already. To reset your password, go to the {loginPasswordLink} page and select {forgotPassword}"
description="Invalid password reset link help text message."
values={{

View File

@@ -11,7 +11,7 @@ const ResetFailureMessage = (props) => {
<div className="text-left mw-500">
<Alert variant="danger">
<FormattedMessage
id="logistration.reset.password.request.failure.header.message"
id="reset.password.request.failure.header.message"
defaultMessage="{errorMessage} "
description="error message when password reset failure."
values={{

View File

@@ -79,12 +79,12 @@ const ResetPasswordPage = (props) => {
<div className="d-flex justify-content-center m-4">
<div className="d-flex flex-column mw-500">
<Form>
<div>
<div className="reset-password-container">
<h3 className="mt-3">
{intl.formatMessage(messages['logistration.reset.password.page.heading'])}
{intl.formatMessage(messages['reset.password.page.heading'])}
</h3>
<p className="mb-4">
{intl.formatMessage(messages['logistration.reset.password.page.instructions'])}
{intl.formatMessage(messages['reset.password.page.instructions'])}
</p>
<div className="d-flex flex-column align-items-start">
<ValidationFormGroup
@@ -94,7 +94,7 @@ const ResetPasswordPage = (props) => {
className="w-100"
>
<Form.Label htmlFor="reset-password-input" className="h6 mr-1">
{intl.formatMessage(messages['logistration.reset.password.page.new.field.label'])}
{intl.formatMessage(messages['reset.password.page.new.field.label'])}
</Form.Label>
<Input
name="new-password1"
@@ -107,11 +107,11 @@ const ResetPasswordPage = (props) => {
<ValidationFormGroup
for="confirm-password-input"
invalid={!passwordMatch}
invalidMessage={intl.formatMessage(messages['logistration.reset.password.page.invalid.match.message'])}
invalidMessage={intl.formatMessage(messages['reset.password.page.invalid.match.message'])}
className="w-100"
>
<Form.Label htmlFor="confirm-password-input" className="h6 mr-1">
{intl.formatMessage(messages['logistration.reset.password.page.confirm.field.label'])}
{intl.formatMessage(messages['reset.password.page.confirm.field.label'])}
</Form.Label>
<Input
name="new-password2"
@@ -129,7 +129,7 @@ const ResetPasswordPage = (props) => {
className="btn-primary"
state={props.status}
labels={{
default: intl.formatMessage(messages['logistration.reset.password.page.submit.button']),
default: intl.formatMessage(messages['reset.password.page.submit.button']),
}}
onClick={e => handleSubmit(e)}
/>

View File

@@ -6,9 +6,9 @@ const ResetSuccessMessage = () => {
const loginPasswordLink = (
<Alert.Link href="/login" className="font-weight-normal text-info">
<FormattedMessage
id="logistration.reset.password.confirmation.support.link"
id="reset.password.confirmation.support.link"
defaultMessage="Sign-in to your account."
description="link text used in message: logistration.reset.password.invalid.token.description.message link 'sign-in.'"
description="link text used in message: reset.password.invalid.token.description.message link 'sign-in.'"
/>
</Alert.Link>
);
@@ -20,13 +20,13 @@ const ResetSuccessMessage = () => {
<Alert variant="success">
<Alert.Heading className="text-success">
<FormattedMessage
id="logistration.reset.password.request.success.header.message"
id="reset.password.request.success.header.message"
defaultMessage="Password Reset Complete."
description="header message when reset is successful."
/>
</Alert.Heading>
<FormattedMessage
id="logistration.reset.password.request.success.header.description.message"
id="reset.password.request.success.header.description.message"
defaultMessage="Your password has been reset. {loginPasswordLink}"
description="message when reset password is successful."
values={{

View File

@@ -1,33 +1,33 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'logistration.reset.password.page.heading': {
id: 'logistration.reset.password.page.heading',
'reset.password.page.heading': {
id: 'reset.password.page.heading',
defaultMessage: 'Reset Your Password',
description: 'The page heading for the Reset password page.',
},
'logistration.reset.password.page.instructions': {
id: 'logistration.reset.password.page.instructions',
'reset.password.page.instructions': {
id: 'reset.password.page.instructions',
defaultMessage: 'Enter and confirm your new password.',
description: 'Instructions message for reset password page.',
},
'logistration.reset.password.page.invalid.match.message': {
id: 'logistration.reset.password.page.invalid.match.message',
'reset.password.page.invalid.match.message': {
id: 'reset.password.page.invalid.match.message',
defaultMessage: 'Passwords do not match.',
description: 'Password format error.',
},
'logistration.reset.password.page.new.field.label': {
id: 'logistration.forgot.password.page.new.field.label',
'reset.password.page.new.field.label': {
id: 'forgot.password.page.new.field.label',
defaultMessage: 'New Password',
description: 'New password field label for the reset password page.',
},
'logistration.reset.password.page.confirm.field.label': {
id: 'logistration.forgot.password.page.confirm.field.label',
'reset.password.page.confirm.field.label': {
id: 'forgot.password.page.confirm.field.label',
defaultMessage: 'Confirm Password',
description: 'Confirm password field label for the reset password page.',
},
'logistration.reset.password.page.submit.button': {
id: 'logistration.reset.password.page.submit.button',
'reset.password.page.submit.button': {
id: 'reset.password.page.submit.button',
defaultMessage: 'Reset my password',
description: 'Submit button text for the reset password page.',
},

View File

@@ -55,7 +55,9 @@ exports[`ResetPasswordPage should match pending reset message section snapshot 1
<form
className=""
>
<div>
<div
className="reset-password-container"
>
<h3
className="mt-3"
>
@@ -154,7 +156,9 @@ exports[`ResetPasswordPage should match reset password default section snapshot
<form
className=""
>
<div>
<div
className="reset-password-container"
>
<h3
className="mt-3"
>
@@ -305,7 +309,9 @@ Array [
<form
className=""
>
<div>
<div
className="reset-password-container"
>
<h3
className="mt-3"
>