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( {beforeLink} {linkText} , ); } while (afterLink.includes('a href')); nodes.push({afterLink}); return ( { nodes } ); } if (field.type === 'checkbox') { const matches = processLink(field.label); [beforeLink, link, linkText, afterLink] = matches; props.checked = this.state[stateVar]; return ( {beforeLink} {linkText} {afterLink} ); } if (field.type === 'select') { options = field.options.map((item) => ({ value: item.value, label: item.name, })); props.options = options; } return ( ); } } 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 ( ); } } return null; }); return fields; } render() { const { intl, submitState } = this.props; const { currentProvider, finishAuthUrl, providers, secondaryProviders, } = this.props.thirdPartyAuthContext; if (!this.props.formData) { return
; } if (this.state.institutionLogin) { return ( ); } return ( <>
{this.props.registrationError ? : null} {currentProvider && ( )}
{intl.formatMessage(messages['logistration.already.have.an.edx.account'])} {intl.formatMessage(messages['logistration.sign.in.hyperlink'])}
{(providers.length || secondaryProviders.length) && !currentProvider ? (
{intl.formatMessage(messages['logistration.create.an.account.using'])}
{intl.formatMessage(messages['logistration.create.a.new.one.here'])}
) : null}
this.handleOnChange(e)} onBlur={e => this.handleOnBlur(e)} required /> this.handleOnChange(e)} onBlur={e => this.handleOnBlur(e)} required /> this.handleOnChange(e)} onBlur={e => this.handleOnBlur(e)} required /> this.handleOnChange(e)} onBlur={e => this.handleOnBlur(e)} required /> { this.addExtraRequiredFields() } this.handleOnOptional(e)} required /> { this.state.enableOptionalField ? this.addExtraOptionalFields() : null}
); } } 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));