Files
frontend-app-authn/src/logistration/RegistrationPage.jsx
Waheed Ahmed fcdce291bb Fix form submission issues. (#50)
* Fix form submission issues.

Fixed form submission issues on first click and
with optional fields check. Also fixed some warnings
and did some refactoring as well.

VAN-225, VAN-206

* added more tests
2020-12-23 18:07:37 +05:00

637 lines
20 KiB
JavaScript

import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Input,
StatefulButton,
Hyperlink,
ValidationFormGroup,
} from '@edx/paragon';
import {
injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import camelCase from 'lodash.camelcase';
import {
getThirdPartyAuthContext,
registerNewUser,
fetchRegistrationForm,
fetchRealtimeValidations,
} from './data/actions';
import { registrationRequestSelector, thirdPartyAuthContextSelector } from './data/selectors';
import { RedirectLogistration } from '../common-components';
import RegistrationFailure from './RegistrationFailure';
import {
DEFAULT_REDIRECT_URL,
DEFAULT_STATE,
LOGIN_PAGE,
REGISTER_PAGE,
REGISTRATION_VALIDITY_MAP,
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';
class RegistrationPage extends React.Component {
constructor(props, context) {
super(props, context);
this.intl = props.intl;
this.state = {
email: '',
name: '',
username: '',
password: '',
country: '',
city: '',
gender: '',
yearOfBirth: '',
mailingAddress: '',
goals: '',
honorCode: true,
termsOfService: true,
levelOfEducation: '',
confirmEmail: '',
enableOptionalField: false,
validationFieldName: '',
errors: {
email: '',
name: '',
username: '',
password: '',
country: '',
honorCode: '',
termsOfService: '',
},
emailValid: false,
nameValid: false,
usernameValid: false,
passwordValid: false,
countryValid: false,
honorCodeValid: true,
termsOfServiceValid: false,
formValid: false,
institutionLogin: false,
};
}
componentDidMount() {
const params = (new URL(document.location)).searchParams;
const payload = {
redirect_to: params.get('next') || DEFAULT_REDIRECT_URL,
};
this.props.getThirdPartyAuthContext(payload);
this.props.fetchRegistrationForm();
}
shouldComponentUpdate(nextProps) {
if (this.props.validations !== nextProps.validations) {
const { errors } = this.state;
const errorMsg = nextProps.validations.validation_decisions[this.state.validationFieldName];
errors[this.state.validationFieldName] = errorMsg;
this.setState({
errors,
});
return false;
}
return true;
}
handleInstitutionLogin = () => {
this.setState(prevState => ({ institutionLogin: !prevState.institutionLogin }));
}
handleSubmit = (e) => {
e.preventDefault();
const params = (new URL(document.location)).searchParams;
const payload = {
email: this.state.email,
username: this.state.username,
password: this.state.password,
name: this.state.name,
};
const fieldMap = { ...REGISTRATION_VALIDITY_MAP, ...REGISTRATION_OPTIONAL_MAP };
Object.entries(fieldMap).forEach(([key, value]) => {
if (value) {
payload[key] = this.state[camelCase(key)];
}
});
const next = params.get('next');
const courseId = params.get('course_id');
if (next) {
payload.next = params.next;
}
if (courseId) {
payload.course_id = params.course_id;
}
if (!this.state.formValid) {
// Special case where honor code and tos is a single field, true by default. We don't need
// to validate this field
Object.entries(payload).filter(([key]) => (key !== 'honor_code' || !('terms_of_service' in REGISTRATION_EXTRA_FIELDS)))
.forEach(([key, value]) => {
this.validateInput(key, value);
});
return;
}
this.props.registerNewUser(payload);
}
handleOnBlur(e) {
this.setState({
validationFieldName: e.target.name,
});
const payload = {
email: this.state.email,
username: this.state.username,
password: this.state.password,
name: this.state.name,
honor_code: this.state.honorCode,
country: this.state.country,
};
this.props.fetchRealtimeValidations(payload);
}
handleOnChange(e) {
const targetValue = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
this.setState({
[camelCase(e.target.name)]: targetValue,
});
this.validateInput(e.target.name, targetValue);
}
handleOnOptional(e) {
const optionalEnable = this.state.enableOptionalField;
const targetValue = e.target.id === 'additionalFields' ? !optionalEnable : e.target.checked;
this.setState({
enableOptionalField: targetValue,
});
}
validateInput(inputName, value) {
const { errors } = this.state;
let {
emailValid,
nameValid,
usernameValid,
passwordValid,
countryValid,
honorCodeValid,
termsOfServiceValid,
} = this.state;
switch (inputName) {
case 'email':
emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i);
errors.email = emailValid ? '' : this.intl.formatMessage(messages['logistration.email.validation.message']);
break;
case 'name':
nameValid = value.length >= 1;
errors.name = nameValid ? '' : this.intl.formatMessage(messages['logistration.fullname.validation.message']);
break;
case 'username':
usernameValid = value.length >= 2 && value.length <= 30;
errors.username = usernameValid ? '' : this.intl.formatMessage(messages['logistration.username.validation.message']);
break;
case 'password':
passwordValid = !!(value.length >= 8 && value.match(/\d+/g));
errors.password = passwordValid ? '' : this.intl.formatMessage(messages['logistration.register.page.password.validation.message']);
break;
case 'country':
countryValid = value !== '';
errors.country = countryValid ? '' : this.intl.formatMessage(messages['logistration.country.validation.message']);
break;
case 'honor_code':
honorCodeValid = value !== false;
errors.honorCode = honorCodeValid ? '' : null;
break;
case 'terms_of_service':
termsOfServiceValid = value !== false;
errors.termsOfService = termsOfServiceValid ? '' : null;
break;
default:
break;
}
this.setState({
errors,
emailValid,
nameValid,
usernameValid,
passwordValid,
countryValid,
honorCodeValid,
termsOfServiceValid,
}, this.validateForm);
}
validateForm() {
const {
emailValid,
nameValid,
usernameValid,
passwordValid,
} = this.state;
const validityMap = REGISTRATION_VALIDITY_MAP;
let extraFieldsValid = true;
Object.entries(validityMap).forEach(([key, value]) => {
if (value) {
const stateValid = `${camelCase(key)}Valid`;
extraFieldsValid = extraFieldsValid && this.state[stateValid];
}
});
this.setState({
formValid: emailValid && nameValid && usernameValid && passwordValid && extraFieldsValid,
});
}
addExtraRequiredFields() {
const fields = this.props.formData.fields.map((field) => {
let options = null;
if (REGISTRATION_EXTRA_FIELDS.includes(field.name)) {
if (field.required) {
const stateVar = camelCase(field.name);
let beforeLink;
let link;
let linkText;
let afterLink;
const props = {
id: field.name,
name: field.name,
type: field.type,
value: this.state[stateVar],
required: true,
onChange: e => this.handleOnChange(e),
};
REGISTRATION_VALIDITY_MAP[field.name] = true;
if (field.type === 'plaintext' && field.name === 'honor_code') { // special case where honor code and tos are combined
afterLink = field.label;
props.type = 'hidden';
const nodes = [];
do {
const matches = processLink(afterLink);
[beforeLink, link, linkText, afterLink] = matches;
nodes.push(
<React.Fragment key={link}>
{beforeLink}
<Hyperlink destination={link}>{linkText}</Hyperlink>
</React.Fragment>,
);
} while (afterLink.includes('a href'));
nodes.push(<React.Fragment key={afterLink}>{afterLink}</React.Fragment>);
return (
<React.Fragment key={field.type}>
<input {...props} />
{ nodes }
</React.Fragment>
);
}
if (field.type === 'checkbox') {
const matches = processLink(field.label);
[beforeLink, link, linkText, afterLink] = matches;
props.checked = this.state[stateVar];
return (
<ValidationFormGroup
for={field.name}
key={field.name}
invalid={this.state.errors[stateVar] !== ''}
invalidMessage={field.errorMessages.required}
className="custom-control"
>
<Input {...props} />
{beforeLink}
<Hyperlink destination={link}>{linkText}</Hyperlink>
{afterLink}
</ValidationFormGroup>
);
}
if (field.type === 'select') {
options = field.options.map((item) => ({
value: item.value,
label: item.name,
}));
props.options = options;
}
return (
<ValidationFormGroup
for={field.name}
key={field.name}
invalid={this.state.errors[stateVar] !== ''}
invalidMessage={field.errorMessages.required}
>
<label htmlFor={field.name} className="h6 pt-3">{field.label} (required)</label>
<Input {...props} />
</ValidationFormGroup>
);
}
}
return null;
});
return fields;
}
addExtraOptionalFields() {
const fields = this.props.formData.fields.map((field) => {
let options = null;
if (REGISTRATION_EXTRA_FIELDS.includes(field.name)) {
if (!field.required && field.name !== 'honor_code' && field.name !== 'country') {
REGISTRATION_OPTIONAL_MAP[field.name] = true;
const props = {
id: field.name,
name: field.name,
type: field.type,
onChange: e => this.handleOnChange(e),
};
if (field.type === 'select') {
options = field.options.map((item) => ({
value: item.value,
label: item.name,
}));
props.options = options;
}
return (
<ValidationFormGroup
for={field.name}
key={field.name}
>
<label htmlFor={field.name} className="h6 pt-3">
{field.label} {this.props.intl.formatMessage(messages['logistration.register.potional.label'])}
</label>
<Input {...props} />
</ValidationFormGroup>
);
}
}
return null;
});
return fields;
}
render() {
const { intl, submitState } = this.props;
const {
currentProvider, finishAuthUrl, providers, secondaryProviders,
} = this.props.thirdPartyAuthContext;
if (!this.props.formData) {
return <div />;
}
if (this.state.institutionLogin) {
return (
<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'])}
/>
);
}
return (
<>
<RedirectLogistration
success={this.props.registrationResult.success}
redirectUrl={this.props.registrationResult.redirectUrl}
finishAuthUrl={finishAuthUrl}
/>
<div className="register-container mx-auto">
{this.props.registrationError ? <RegistrationFailure errors={this.props.registrationError} /> : null}
{currentProvider && (
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={this.props.thirdPartyAuthContext.platformName}
referrer="register"
/>
)}
<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>
</div>
{(providers.length || secondaryProviders.length) && !currentProvider ? (
<div className="d-block mb-4 mt-4">
<span className="d-block mx-auto mb-4 section-heading-line">
{intl.formatMessage(messages['logistration.create.an.account.using'])}
</span>
<div className="row tpa-container">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
</div>
<RenderInstitutionButton
onSubmitHandler={this.handleInstitutionLogin}
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
buttonTitle={intl.formatMessage(messages['logistration.register.institution.login.button'])}
/>
<span className="d-block mx-auto text-center mt-4 section-heading-line">
{intl.formatMessage(messages['logistration.create.a.new.one.here'])}
</span>
</div>
) : null}
<form className="mb-4 mx-auto form-group">
<ValidationFormGroup
for="name"
invalid={this.state.errors.name !== ''}
invalidMessage={this.state.errors.name}
>
<label htmlFor="name" className="h6 pt-3">
{intl.formatMessage(messages['logistration.fullname.label'])}
</label>
<Input
name="name"
id="name"
type="text"
placeholder=""
value={this.state.name}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
required
/>
</ValidationFormGroup>
<ValidationFormGroup
for="username"
invalid={this.state.errors.username !== ''}
invalidMessage={this.state.errors.username}
>
<label htmlFor="username" className="h6 pt-3">
{intl.formatMessage(messages['logistration.username.label'])}
</label>
<Input
name="username"
id="username"
type="text"
placeholder=""
value={this.state.username}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
required
/>
</ValidationFormGroup>
<ValidationFormGroup
for="email"
invalid={this.state.errors.email !== ''}
invalidMessage={this.state.errors.email}
>
<label htmlFor="email" className="h6 pt-3">
{intl.formatMessage(messages['logistration.register.page.email.label'])}
</label>
<Input
name="email"
id="email"
type="email"
placeholder=""
value={this.state.email}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
required
/>
</ValidationFormGroup>
<ValidationFormGroup
for="password"
invalid={this.state.errors.password !== ''}
invalidMessage={this.state.errors.password}
>
<label htmlFor="password" className="h6 pt-3">
{intl.formatMessage(messages['logistration.password.label'])}
</label>
<Input
name="password"
id="password"
type="password"
placeholder=""
value={this.state.password}
onChange={e => this.handleOnChange(e)}
onBlur={e => this.handleOnBlur(e)}
required
/>
</ValidationFormGroup>
{ this.addExtraRequiredFields() }
<ValidationFormGroup
for="optional"
className="custom-control"
>
<Input
name="optional"
id="optional"
type="checkbox"
value={this.state.enableOptionalField}
checked={this.state.enableOptionalField}
onChange={e => this.handleOnOptional(e)}
required
/>
<p role="presentation" id="additionalFields" onClick={e => this.handleOnOptional(e)}>
{intl.formatMessage(messages['logistration.support.education.research'])}
</p>
</ValidationFormGroup>
{ this.state.enableOptionalField ? this.addExtraOptionalFields() : null}
<StatefulButton
type="button"
className="btn-primary submit mt-4"
state={submitState}
labels={{
default: intl.formatMessage(messages['logistration.create.account.button']),
}}
onClick={this.handleSubmit}
/>
</form>
</div>
</>
);
}
}
RegistrationPage.defaultProps = {
registrationResult: null,
registerNewUser: null,
registrationError: null,
submitState: DEFAULT_STATE,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
formData: null,
validations: 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,
}),
submitState: 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,
}),
}),
fetchRegistrationForm: PropTypes.func.isRequired,
formData: PropTypes.shape({
fields: PropTypes.array,
}),
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,
}),
}),
};
const mapStateToProps = state => {
const registrationResult = registrationRequestSelector(state);
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
return {
registrationError: state.logistration.registrationError,
submitState: state.logistration.submitState,
registrationResult,
thirdPartyAuthContext,
formData: state.logistration.formData,
validations: state.logistration.validations,
};
};
export default connect(
mapStateToProps,
{
getThirdPartyAuthContext,
fetchRegistrationForm,
fetchRealtimeValidations,
registerNewUser,
},
)(injectIntl(RegistrationPage));