mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-01-08 05:22:16 -05:00
264 lines
14 KiB
JavaScript
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;
|