Files
frontend-app-authn/src/register/RegistrationPage.jsx
Waheed Ahmed 37c5344066 Set cookie domain. (#267)
VAN-291
2021-05-04 14:33:03 +05:00

749 lines
26 KiB
JavaScript

import React from 'react';
import camelCase from 'lodash.camelcase';
import { connect } from 'react-redux';
import Skeleton from 'react-loading-skeleton';
import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import Cookies from 'universal-cookie';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
injectIntl, intlShape, getCountryList, getLocale, FormattedMessage,
} from '@edx/frontend-platform/i18n';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { Form, Hyperlink, StatefulButton } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { registerNewUser, fetchRealtimeValidations } from './data/actions';
import { registrationRequestSelector } from './data/selectors';
import messages from './messages';
import OptionalFields from './OptionalFields';
import RegistrationFailure from './RegistrationFailure';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
InstitutionLogistration, AuthnValidationFormGroup,
} from '../common-components';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import {
DEFAULT_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
} from '../data/constants';
import {
getTpaProvider, getTpaHint, updatePathWithQueryParams, getAllPossibleQueryParam,
} from '../data/utils';
class RegistrationPage extends React.Component {
constructor(props, context) {
super(props, context);
sendPageEvent('login_and_registration', 'register');
this.intl = props.intl;
this.queryParams = getAllPossibleQueryParam();
this.tpaHint = getTpaHint();
this.state = {
email: '',
name: '',
username: '',
password: '',
country: '',
gender: '',
yearOfBirth: '',
goals: '',
levelOfEducation: '',
enableOptionalField: false,
validationAlertMessages: {
name: [{ user_message: '' }],
username: [{ user_message: '' }],
email: [{ user_message: '' }],
password: [{ user_message: '' }],
country: [{ user_message: '' }],
},
errors: {
email: '',
name: '',
username: '',
password: '',
country: '',
},
institutionLogin: false,
formValid: false,
startTime: Date.now(),
updateFieldErrors: false,
updateAlertErrors: false,
registrationErrorsUpdated: false,
};
}
componentDidMount() {
const payload = { ...this.queryParams };
if (this.tpaHint) {
payload.tpa_hint = this.tpaHint;
}
this.props.getThirdPartyAuthContext(payload);
}
shouldComponentUpdate(nextProps) {
if (nextProps.statusCode !== 403 && this.props.validations !== nextProps.validations) {
const { errors } = this.state;
const { fieldName } = this.state;
const errorMsg = nextProps.validations.validation_decisions[fieldName];
errors[fieldName] = errorMsg;
this.setState({
errors,
});
return false;
}
if (this.props.thirdPartyAuthContext.pipelineUserDetails !== nextProps.thirdPartyAuthContext.pipelineUserDetails) {
this.setState({
...nextProps.thirdPartyAuthContext.pipelineUserDetails,
});
return false;
}
if (this.props.registrationError !== nextProps.registrationError) {
this.setState({
formValid: false,
registrationErrorsUpdated: true,
});
return false;
}
if (this.state.registrationErrorsUpdated && this.props.registrationError === nextProps.registrationError) {
this.setState({
formValid: false,
registrationErrorsUpdated: false,
});
return false;
}
return true;
}
getCountryOptions = () => {
const { intl } = this.props;
return [{
value: '',
label: intl.formatMessage(messages['registration.country.label']),
}].concat(getCountryList(getLocale()).map(({ code, name }) => ({ value: code, label: name })));
}
getOptionalFields() {
const values = {};
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',');
optionalFields.forEach((key) => {
values[camelCase(key)] = this.state[camelCase(key)];
});
return (
<OptionalFields
values={values}
onChangeHandler={(fieldName, value) => { this.setState({ [fieldName]: value }); }}
/>
);
}
handleInstitutionLogin = () => {
this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin }));
}
handleSubmit = (e) => {
e.preventDefault();
const totalRegistrationTime = (Date.now() - this.state.startTime) / 1000;
let payload = {
name: this.state.name,
username: this.state.username,
email: this.state.email,
country: this.state.country,
honor_code: true,
};
if (this.props.thirdPartyAuthContext.currentProvider) {
payload.social_auth_provider = this.props.thirdPartyAuthContext.currentProvider;
} else {
payload.password = this.state.password;
}
const postParams = getAllPossibleQueryParam();
payload = { ...payload, ...postParams };
let finalValidation = this.state.formValid;
if (!this.state.formValid) {
Object.keys(payload).forEach(key => {
finalValidation = this.validateInput(key, payload[key], payload);
});
}
// Since optional fields are not validated we can add it to payload after required fields
// have been validated. This will save us unwanted calls to validateInput()
const optionalFields = getConfig().REGISTRATION_OPTIONAL_FIELDS.split(',');
optionalFields.forEach((key) => {
const stateKey = camelCase(key);
if (this.state[stateKey]) {
payload[key] = this.state[stateKey];
}
});
if (finalValidation) {
payload.totalRegistrationTime = totalRegistrationTime;
this.props.registerNewUser(payload);
}
}
checkNoFieldErrors(validations) {
const keyValidList = Object.entries(validations).map(([key]) => !validations[key]);
return keyValidList.every((current) => current === true);
}
checkNoAlertErrors(validations) {
const keyValidList = Object.entries(validations).map(([key]) => {
const validation = validations[key][0];
return !validation.user_message;
});
return keyValidList.every((current) => current === true);
}
handleOnBlur(e) {
const payload = {
email: this.state.email,
username: this.state.username,
password: this.state.password,
name: this.state.name,
honor_code: true,
country: this.state.country,
};
const { name, value } = e.target;
this.setState({
updateFieldErrors: false,
updateAlertErrors: false,
fieldName: e.target.name,
}, () => {
this.validateInput(name, value, payload, false);
});
}
handleOnChange(e) {
if (!(e.target.name === 'username' && e.target.value.length > 30)) {
this.setState({
[e.target.name]: e.target.value,
updateFieldErrors: false,
updateAlertErrors: false,
});
}
}
handleOnOptional(e) {
const optionalEnable = this.state.enableOptionalField;
const targetValue = e.target.id === 'additionalFields' ? !optionalEnable : e.target.checked;
this.setState({
enableOptionalField: targetValue,
updateAlertErrors: false,
updateFieldErrors: false,
});
sendTrackEvent('edx.bi.user.register.optional_fields_selected', {});
}
handleLoginLinkClickEvent() {
sendTrackEvent('edx.bi.login_form.toggled', { category: 'user-engagement' });
}
validateInput(inputName, value, payload, updateAlertMessage = true) {
const { errors } = this.state;
const { intl, statusCode } = this.props;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
let {
formValid,
updateFieldErrors,
updateAlertErrors,
} = this.state;
switch (inputName) {
case 'email':
if (value.length < 1) {
errors.email = intl.formatMessage(messages['email.validation.message']);
} else if (value.length <= 2) {
errors.email = intl.formatMessage(messages['email.ratelimit.less.chars.validation.message']);
} else if (!emailRegex.test(value)) {
errors.email = intl.formatMessage(messages['email.ratelimit.incorrect.format.validation.message']);
} else if (payload && statusCode !== 403) {
this.props.fetchRealtimeValidations(payload);
} else {
errors.email = '';
}
break;
case 'name':
if (value.length < 1) {
errors.name = intl.formatMessage(messages['fullname.validation.message']);
} else {
errors.name = '';
}
break;
case 'username':
if (value.length < 1) {
errors.username = intl.formatMessage(messages['username.validation.message']);
} else if (value.length <= 1 || value.length > 30) {
errors.username = intl.formatMessage(messages['username.ratelimit.less.chars.message']);
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
errors.username = intl.formatMessage(messages['username.format.validation.message']);
} else if (payload && statusCode !== 403) {
this.props.fetchRealtimeValidations(payload);
} else {
errors.username = '';
}
break;
case 'password':
if (value.length < 1) {
errors.password = intl.formatMessage(messages['register.page.password.validation.message']);
} else if (value.length < 8) {
errors.password = intl.formatMessage(messages['email.ratelimit.password.validation.message']);
} else if (!value.match(/.*[0-9].*/i)) {
errors.password = intl.formatMessage(messages['username.number.validation.message']);
} else if (!value.match(/.*[a-zA-Z].*/i)) {
errors.password = intl.formatMessage(messages['username.character.validation.message']);
} else if (payload && statusCode !== 403) {
this.props.fetchRealtimeValidations(payload);
} else {
errors.password = '';
}
break;
case 'country':
if (!value) {
errors.country = intl.formatMessage(messages['country.validation.message']);
} else {
errors.country = '';
}
break;
default:
break;
}
if (updateAlertMessage) {
updateFieldErrors = true;
updateAlertErrors = true;
formValid = this.checkNoFieldErrors(errors);
}
this.setState({
formValid,
updateFieldErrors,
updateAlertErrors,
errors,
});
return formValid;
}
updateFieldErrors(registrationError) {
const {
errors,
} = this.state;
Object.entries(registrationError).map(([key]) => {
if (registrationError[key]) {
errors[key] = registrationError[key][0].user_message;
}
return errors;
});
}
updateValidationAlertMessages() {
const {
errors,
validationAlertMessages,
} = this.state;
Object.entries(errors).map(([key, value]) => {
if (validationAlertMessages[key]) {
validationAlertMessages[key][0].user_message = value;
}
return validationAlertMessages;
});
}
renderErrors() {
let errorsObject = null;
let { registrationErrorsUpdated } = this.state;
const {
updateAlertErrors,
updateFieldErrors,
validationAlertMessages,
} = this.state;
const { registrationError, submitState } = this.props;
if (registrationError && registrationErrorsUpdated) {
if (updateFieldErrors && submitState !== PENDING_STATE) {
this.updateFieldErrors(registrationError);
}
registrationErrorsUpdated = false;
errorsObject = registrationError;
} else {
if (updateAlertErrors && submitState !== PENDING_STATE) {
this.updateValidationAlertMessages();
}
errorsObject = !this.checkNoAlertErrors(validationAlertMessages) ? validationAlertMessages : {};
}
return (
<RegistrationFailure
errors={errorsObject}
isSubmitted={updateAlertErrors}
submitButtonState={submitState}
/>
);
}
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
let thirdPartyComponent = null;
if ((providers.length || secondaryProviders.length) && !currentProvider) {
thirdPartyComponent = (
<>
<RenderInstitutionButton
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
/>
<div className="row tpa-container">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
</div>
</>
);
} else if (thirdPartyAuthApiStatus === PENDING_STATE) {
thirdPartyComponent = <Skeleton height={36} count={2} />;
}
return thirdPartyComponent;
}
renderForm(currentProvider,
providers,
secondaryProviders,
thirdPartyAuthApiStatus,
finishAuthUrl,
submitState,
intl) {
if (this.state.institutionLogin) {
return (
<InstitutionLogistration
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
buttonTitle={intl.formatMessage(messages['create.an.account'])}
/>
);
}
if (this.props.registrationResult.success) {
const cookieName = getConfig().USER_SIGNUP_SURVEY_COOKIE_NAME;
if (cookieName) {
const cookies = new Cookies();
const signupTimestamp = (new Date()).getTime();
// set expiry to exactly 24 hours from now
const cookieExpiry = new Date(signupTimestamp + 1 * 864e5);
const options = { domain: getConfig().COOKIE_DOMAIN, expires: cookieExpiry, path: '/' };
cookies.set(cookieName, signupTimestamp, options);
}
}
return (
<>
<Helmet>
<title>{intl.formatMessage(messages['register.page.title'],
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<RedirectLogistration
success={this.props.registrationResult.success}
redirectUrl={this.props.registrationResult.redirectUrl}
finishAuthUrl={finishAuthUrl}
/>
<div className="d-flex justify-content-center m-4">
<div className="d-flex flex-column">
<div className="mw-500">
{this.renderErrors()}
{currentProvider && (
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={this.props.thirdPartyAuthContext.platformName}
referrer={REGISTER_PAGE}
/>
)}
<p>
{intl.formatMessage(messages['already.have.an.edx.account'])}
<Hyperlink
className="ml-1"
destination={updatePathWithQueryParams(LOGIN_PAGE)}
onClick={this.handleLoginLinkClickEvent}
>
{intl.formatMessage(messages['sign.in.hyperlink'])}
</Hyperlink>
</p>
<hr className="mb-3 border-gray-200" />
<h1 className="mb-3 h3">{intl.formatMessage(messages['create.a.new.account'])}</h1>
<Form className="form-group">
<AuthnValidationFormGroup
label={intl.formatMessage(messages['fullname.label'])}
for="name"
name="name"
type="text"
invalid={this.state.errors.name !== ''}
ariaInvalid={this.state.errors.name !== ''}
invalidMessage={this.state.errors.name}
value={this.state.name}
onBlur={(e) => this.handleOnBlur(e)}
onChange={(e) => this.handleOnChange(e)}
helpText={intl.formatMessage(messages['helptext.name'])}
inputFieldStyle="border-gray-600"
/>
<AuthnValidationFormGroup
label={intl.formatMessage(messages['username.label'])}
for="username"
name="username"
type="text"
className="data-hj-suppress"
invalid={this.state.errors.username !== ''}
ariaInvalid={this.state.errors.username !== ''}
invalidMessage={this.state.errors.username}
value={this.state.username}
onBlur={(e) => this.handleOnBlur(e)}
onChange={(e) => this.handleOnChange(e)}
helpText={intl.formatMessage(messages['helptext.username'])}
inputFieldStyle="border-gray-600"
/>
<AuthnValidationFormGroup
label={intl.formatMessage(messages['register.page.email.label'])}
for="email"
name="email"
type="text"
className="data-hj-suppress"
invalid={this.state.errors.email !== ''}
ariaInvalid={this.state.errors.email !== ''}
invalidMessage={this.state.errors.email}
value={this.state.email}
onBlur={(e) => this.handleOnBlur(e)}
onChange={(e) => this.handleOnChange(e)}
helpText={intl.formatMessage(messages['helptext.email'])}
inputFieldStyle="border-gray-600"
/>
{!currentProvider && (
<AuthnValidationFormGroup
label={intl.formatMessage(messages['password.label'])}
for="password"
name="password"
type="password"
invalid={this.state.errors.password !== ''}
ariaInvalid={this.state.errors.password !== ''}
invalidMessage={this.state.errors.password}
value={this.state.password}
onBlur={(e) => this.handleOnBlur(e)}
onChange={(e) => this.handleOnChange(e)}
helpText={intl.formatMessage(messages['helptext.password'])}
inputFieldStyle="border-gray-600"
/>
)}
<AuthnValidationFormGroup
label={intl.formatMessage(messages['registration.country.label'])}
for="country"
name="country"
type="select"
key="country"
invalid={this.state.errors.country !== ''}
ariaInvalid={this.state.errors.country !== ''}
invalidMessage={intl.formatMessage(messages['country.validation.message'])}
className="mb-0 data-hj-suppress"
value={this.state.country}
onBlur={(e) => this.handleOnBlur(e)}
onChange={(e) => this.handleOnChange(e)}
selectOptions={this.getCountryOptions()}
inputFieldStyle="border-gray-600 custom-select-size"
/>
<div id="honor-code" className="pt-10 small">
<FormattedMessage
id="register.page.terms.of.service.and.honor.code"
tagName="p"
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
Member process your personal data in accordance with the {privacyPolicy}."
description="Text that appears on registration form stating honor code and privacy policy"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink destination={getConfig().TOS_AND_HONOR_CODE} target="_blank">
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
</Hyperlink>
),
privacyPolicy: (
<Hyperlink destination={getConfig().PRIVACY_POLICY} target="_blank">
{intl.formatMessage(messages['privacy.policy'])}
</Hyperlink>
),
}}
/>
</div>
{getConfig().REGISTRATION_OPTIONAL_FIELDS ? (
<AuthnValidationFormGroup
label={intl.formatMessage(messages['support.education.research'])}
for="optional"
name="optional"
type="checkbox"
value={this.state.enableOptionalField}
onClick={(e) => this.handleOnOptional(e)}
onBlur={null}
onChange={(e) => this.handleOnOptional(e)}
optionalFieldCheckbox
isChecked={this.state.enableOptionalField}
checkboxMessage={intl.formatMessage(messages['support.education.research'])}
/>
) : null}
{ this.state.enableOptionalField ? this.getOptionalFields() : null }
<StatefulButton
type="submit"
variant="brand"
state={submitState}
className="mt-3"
labels={{
default: intl.formatMessage(messages['create.account.button']),
}}
icons={{ pending: <FontAwesomeIcon icon={faSpinner} spin /> }}
onClick={this.handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{(providers.length || secondaryProviders.length || thirdPartyAuthApiStatus === PENDING_STATE)
&& !currentProvider ? (
<div className="d-block mb-4 mt-4">
<hr className="mt-0 border-gray-200" />
<span className="d-block mb-4 text-left">
{intl.formatMessage(messages['create.an.account.using'])}
</span>
</div>
) : null}
{this.renderThirdPartyAuth(providers,
secondaryProviders,
currentProvider,
thirdPartyAuthApiStatus,
intl)}
</Form>
</div>
</div>
</div>
</>
);
}
render() {
const { intl, submitState, thirdPartyAuthApiStatus } = this.props;
const {
currentProvider, finishAuthUrl, providers, secondaryProviders,
} = this.props.thirdPartyAuthContext;
if (this.tpaHint) {
if (thirdPartyAuthApiStatus === PENDING_STATE) {
return <Skeleton height={36} />;
}
const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders);
if (skipHintedLogin) {
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
return null;
}
return provider ? (<EnterpriseSSO provider={provider} intl={intl} />)
: this.renderForm(
currentProvider,
providers,
secondaryProviders,
thirdPartyAuthApiStatus,
finishAuthUrl,
submitState,
intl,
);
}
return this.renderForm(
currentProvider,
providers,
secondaryProviders,
thirdPartyAuthApiStatus,
finishAuthUrl,
submitState,
intl,
);
}
}
RegistrationPage.defaultProps = {
registrationResult: null,
registerNewUser: null,
registrationError: null,
submitState: DEFAULT_STATE,
thirdPartyAuthApiStatus: 'pending',
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
},
validations: null,
statusCode: null,
};
RegistrationPage.propTypes = {
intl: intlShape.isRequired,
getThirdPartyAuthContext: PropTypes.func.isRequired,
registerNewUser: PropTypes.func,
registrationResult: PropTypes.shape({
redirectUrl: PropTypes.string,
success: PropTypes.bool,
}),
registrationError: PropTypes.shape({
email: PropTypes.array,
username: PropTypes.array,
country: PropTypes.array,
password: PropTypes.array,
name: PropTypes.array,
}),
submitState: PropTypes.string,
thirdPartyAuthApiStatus: PropTypes.string,
thirdPartyAuthContext: PropTypes.shape({
currentProvider: PropTypes.string,
platformName: PropTypes.string,
providers: PropTypes.array,
secondaryProviders: PropTypes.array,
finishAuthUrl: PropTypes.string,
pipelineUserDetails: PropTypes.shape({
email: PropTypes.string,
fullname: PropTypes.string,
firstName: PropTypes.string,
lastName: PropTypes.string,
username: PropTypes.string,
}),
}),
fetchRealtimeValidations: PropTypes.func.isRequired,
validations: PropTypes.shape({
validation_decisions: PropTypes.shape({
country: PropTypes.string,
email: PropTypes.string,
name: PropTypes.string,
password: PropTypes.string,
username: PropTypes.string,
}),
}),
statusCode: PropTypes.number,
};
const mapStateToProps = state => {
const registrationResult = registrationRequestSelector(state);
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
return {
registrationError: state.register.registrationError,
submitState: state.register.submitState,
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
registrationResult,
thirdPartyAuthContext,
validations: state.register.validations,
statusCode: state.register.statusCode,
};
};
export default connect(
mapStateToProps,
{
getThirdPartyAuthContext,
fetchRealtimeValidations,
registerNewUser,
},
)(injectIntl(RegistrationPage));