scratch-www/src/components/join-flow/username-step.jsx

264 lines
14 KiB
JavaScript

const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const React = require('react');
const PropTypes = require('prop-types');
import {Formik} from 'formik';
const {injectIntl, intlShape} = require('react-intl');
const validate = require('../../lib/validate');
const FormikInput = require('../../components/formik-forms/formik-input.jsx');
const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx');
const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');
/*
* Username step
*/
/* eslint-disable react/prefer-stateless-function, no-useless-constructor */
class UsernameStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleChangeShowPassword',
'handleFocused',
'handleSetUsernameRef',
'handleValidSubmit',
'validatePasswordIfPresent',
'validatePasswordConfirmIfPresent',
'validateUsernameIfPresent',
'validateForm'
]);
this.state = {
focused: null,
showPassword: false
};
}
componentDidMount () {
// automatically start with focus on username field
if (this.usernameInput) this.usernameInput.focus();
}
handleChangeShowPassword () {
this.setState({showPassword: !this.state.showPassword});
}
// track the currently focused input field, to determine whether each field should
// display a tooltip. (We only display it if a field is focused and has never been touched.)
handleFocused (fieldName) {
this.setState({focused: fieldName});
}
handleSetUsernameRef (usernameInputRef) {
this.usernameInput = usernameInputRef;
}
// we allow username to be empty on blur, since you might not have typed anything yet
validateUsernameIfPresent (username) {
if (!username) return null; // skip validation if username is blank; null indicates valid
// if username is not blank, run both local and remote validations
const localResult = validate.validateUsernameLocally(username);
return validate.validateUsernameRemotely(username).then(
remoteResult => {
// there may be multiple validation errors. Prioritize vulgarity, then
// length, then having invalid chars, then all other remote reports
if (remoteResult.valid === false && remoteResult.errMsgId === 'registration.validationUsernameVulgar') {
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
} else if (localResult.valid === false) {
return this.props.intl.formatMessage({id: localResult.errMsgId});
} else if (remoteResult.valid === false) {
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
}
return null;
}
);
}
validatePasswordIfPresent (password, username) {
if (!password) return null; // skip validation if password is blank; null indicates valid
const localResult = validate.validatePassword(password, username);
if (localResult.valid) return null;
return this.props.intl.formatMessage({id: localResult.errMsgId});
}
validatePasswordConfirmIfPresent (password, passwordConfirm) {
if (!passwordConfirm) return null; // allow blank password if not submitting yet
const localResult = validate.validatePasswordConfirm(password, passwordConfirm);
if (localResult.valid) return null;
return this.props.intl.formatMessage({id: localResult.errMsgId});
}
// called asynchonously when form submit is initially requested,
// along with all of the individual field validation functions
validateForm (values) {
// in addition to field-level username/password validations, we need to additionally
// check that these values aren't blank.
const errors = {};
const usernameResult = validate.validateUsernameLocally(values.username);
if (!usernameResult.valid) {
errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId});
}
const passwordResult = validate.validatePassword(values.password, values.username);
if (!passwordResult.valid) {
errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId});
}
const passwordConfirmResult = validate.validatePasswordConfirm(values.password, values.passwordConfirm);
if (!passwordConfirmResult.valid) {
errors.passwordConfirm = this.props.intl.formatMessage({id: passwordConfirmResult.errMsgId});
}
return errors;
}
// called after all validations pass with no errors
handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false); // formik makes us do this ourselves
delete formData.showPassword;
this.props.onNextStep(formData);
}
render () {
return (
<Formik
initialValues={{
username: '',
password: '',
passwordConfirm: '',
showPassword: false
}}
validate={this.validateForm}
validateOnBlur={false}
validateOnChange={false}
onSubmit={this.handleValidSubmit}
>
{props => {
const {
errors,
handleSubmit,
isSubmitting,
setFieldError,
setFieldTouched,
setFieldValue,
touched,
validateField,
values
} = props;
return (
<JoinFlowStep
description={this.props.intl.formatMessage({
id: 'registration.usernameStepDescriptionNonEducator'
})}
innerClassName="join-flow-inner-username-step"
title={this.props.intl.formatMessage({id: 'general.joinScratch'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
>
<div>
<div className="join-flow-input-title">
{this.props.intl.formatMessage({id: 'registration.createUsername'})}
</div>
<FormikInput
className={classNames(
'join-flow-input'
)}
error={errors.username}
id="username"
name="username"
placeholder={this.props.intl.formatMessage({id: 'general.username'})}
toolTip={this.state.focused === 'username' && !touched.username &&
this.props.intl.formatMessage({id: 'registration.usernameAdviceShort'})}
validate={this.validateUsernameIfPresent}
validationClassName="validation-full-width-input"
/* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('username')}
onChange={e => {
setFieldValue('username', e.target.value);
setFieldTouched('username');
setFieldError('username', null);
}}
onFocus={() => this.handleFocused('username')}
/* eslint-enable react/jsx-no-bind */
onSetRef={this.handleSetUsernameRef}
/>
<div className="join-flow-password-section">
<div className="join-flow-input-title">
{this.props.intl.formatMessage({id: 'registration.choosePasswordStepTitle'})}
</div>
<FormikInput
className={classNames(
'join-flow-input',
{'join-flow-input-password':
!values.showPassword && values.password.length > 0}
)}
error={errors.password}
id="password"
name="password"
placeholder={this.props.intl.formatMessage({id: 'general.password'})}
toolTip={this.state.focused === 'password' && !touched.password &&
this.props.intl.formatMessage({id: 'registration.passwordAdviceShort'})}
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={password => this.validatePasswordIfPresent(password, values.username)}
validationClassName="validation-full-width-input"
onBlur={() => validateField('password')}
onChange={e => {
setFieldValue('password', e.target.value);
setFieldTouched('password');
setFieldError('password', null);
}}
onFocus={() => this.handleFocused('password')}
/* eslint-enable react/jsx-no-bind */
/>
<FormikInput
className={classNames(
'join-flow-input',
'join-flow-password-confirm',
{
'join-flow-input-password':
!values.showPassword && values.passwordConfirm.length > 0,
'fail': errors.passwordConfirm
}
)}
error={errors.passwordConfirm}
id="passwordConfirm"
name="passwordConfirm"
placeholder={this.props.intl.formatMessage({
id: 'registration.confirmPasswordInstruction'
})}
toolTip={
this.state.focused === 'passwordConfirm' && !touched.passwordConfirm &&
this.props.intl.formatMessage({
id: 'registration.confirmPasswordInstruction'
})
}
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={() =>
this.validatePasswordConfirmIfPresent(values.password,
values.passwordConfirm)
}
validationClassName="validation-full-width-input"
onBlur={() => validateField('passwordConfirm')}
onChange={e => {
setFieldValue('passwordConfirm', e.target.value);
setFieldTouched('passwordConfirm');
setFieldError('passwordConfirm', null);
}}
onFocus={() => this.handleFocused('passwordConfirm')}
/* eslint-enable react/jsx-no-bind */
/>
<div className="join-flow-input-title">
<FormikCheckbox
id="showPassword"
label={this.props.intl.formatMessage({id: 'registration.showPassword'})}
name="showPassword"
/>
</div>
</div>
</div>
</JoinFlowStep>
);
}}
</Formik>
);
}
}
UsernameStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
};
const IntlUsernameStep = injectIntl(UsernameStep);
module.exports = IntlUsernameStep;