scratch-www/src/components/join-flow/join-flow.jsx
2019-10-17 23:34:31 -04:00

222 lines
8.1 KiB
JavaScript

const bindAll = require('lodash.bindall');
const connect = require('react-redux').connect;
const defaults = require('lodash.defaultsdeep');
const PropTypes = require('prop-types');
const React = require('react');
const api = require('../../lib/api');
const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape;
const sessionActions = require('../../redux/session.js');
const Progression = require('../progression/progression.jsx');
const UsernameStep = require('./username-step.jsx');
const BirthDateStep = require('./birthdate-step.jsx');
const GenderStep = require('./gender-step.jsx');
const CountryStep = require('./country-step.jsx');
const EmailStep = require('./email-step.jsx');
const WelcomeStep = require('./welcome-step.jsx');
const RegistrationErrorStep = require('./registration-error-step.jsx');
class JoinFlow extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleAdvanceStep',
'handleErrorNext',
'handleRegistrationError',
'handlePrepareToRegister',
'handleRegistrationResponse',
'handleSubmitRegistration'
]);
this.initialState = {
numAttempts: 0,
formData: {},
registrationError: null,
step: 0,
waiting: false
};
// it's ok to set state by reference, because state is treated as immutable,
// so any changes to its fields will result in a new state which does not
// reference its past fields
this.state = this.initialState;
}
canTryAgain () {
return (this.state.numAttempts <= 1);
}
handleRegistrationError (message) {
if (!message) {
message = this.props.intl.formatMessage({
id: 'registration.generalError'
});
}
this.setState({registrationError: message});
}
handlePrepareToRegister (newFormData) {
newFormData = newFormData || {};
const newState = {
formData: defaults({}, newFormData, this.state.formData)
};
this.setState(newState, () => {
this.handleSubmitRegistration(this.state.formData);
});
}
handleRegistrationResponse (err, body, res) {
// example of failing response:
// [
// {
// "msg": "This field is required.",
// "errors": {
// "username": ["This field is required."],
// "recaptcha": ["Incorrect, please try again."]
// },
// "success": false
// }
// ]
// username: 'username exists'
this.setState({
numAttempts: this.state.numAttempts + 1,
waiting: false
}, () => {
let errStr = '';
if (!err && res.statusCode === 200) {
if (body && body[0]) {
if (body[0].success) {
this.props.refreshSession();
this.setState({
step: this.state.step + 1
});
return;
}
if (body[0].errors) {
// body can include zero or more error objects, each
// with its own key and description. Here we assemble
// all of them into a single string, errStr.
const errorKeys = Object.keys(body[0].errors);
errorKeys.forEach(key => {
const val = body[0].errors[key];
if (val && val[0]) {
if (errStr.length) errStr += '; ';
errStr += `${key}: ${val[0]}`;
}
});
}
if (!errStr.length && body[0].msg) errStr = body[0].msg;
}
}
this.setState({
registrationError: errStr ||
`${this.props.intl.formatMessage({
id: 'registration.generalError'
})} (${res.statusCode})`
});
});
}
handleSubmitRegistration (formData) {
this.setState({
waiting: true
}, () => {
api({
host: '',
uri: '/accounts/register_new_user/',
method: 'post',
useCsrf: true,
formData: {
'username': formData.username,
'email': formData.email,
'password': formData.password,
'birth_month': formData.birth_month,
'birth_year': formData.birth_year,
'g-recaptcha-response': formData['g-recaptcha-response'],
'gender': formData.gender,
'country': formData.country,
'subscribe': true,
'is_robot': formData.yesno
// no need to include csrfmiddlewaretoken; will be provided in
// X-CSRFToken header, which scratchr2 looks for in
// scratchr2/middleware/csrf.py line 237.
}
}, (err, body, res) => {
this.handleRegistrationResponse(err, body, res);
});
});
}
handleAdvanceStep (newFormData) {
newFormData = newFormData || {};
this.setState({
formData: defaults({}, newFormData, this.state.formData),
step: this.state.step + 1
});
}
handleErrorNext () {
if (this.canTryAgain()) {
this.handleSubmitRegistration(this.state.formData);
} else {
this.resetState();
}
}
resetState () {
this.setState(this.initialState);
}
render () {
return (
<React.Fragment>
{this.state.registrationError ? (
<RegistrationErrorStep
canTryAgain={this.canTryAgain()}
errorMsg={this.state.registrationError}
/* eslint-disable react/jsx-no-bind */
onSubmit={this.handleErrorNext}
/* eslint-enable react/jsx-no-bind */
/>
) : (
<Progression step={this.state.step}>
<UsernameStep onNextStep={this.handleAdvanceStep} />
<CountryStep onNextStep={this.handleAdvanceStep} />
<BirthDateStep onNextStep={this.handleAdvanceStep} />
<GenderStep onNextStep={this.handleAdvanceStep} />
<EmailStep
waiting={this.state.waiting}
onNextStep={this.handlePrepareToRegister}
onRegistrationError={this.handleRegistrationError}
/>
<WelcomeStep
createProjectOnComplete={this.props.createProjectOnComplete}
email={this.state.formData.email}
username={this.state.formData.username}
onNextStep={this.props.onCompleteRegistration}
/>
</Progression>
)}
</React.Fragment>
);
}
}
JoinFlow.propTypes = {
createProjectOnComplete: PropTypes.bool,
intl: intlShape,
onCompleteRegistration: PropTypes.func,
refreshSession: PropTypes.func
};
const IntlJoinFlow = injectIntl(JoinFlow);
const mapDispatchToProps = dispatch => ({
refreshSession: () => {
dispatch(sessionActions.refreshSession());
}
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
{}, stateProps, dispatchProps, ownProps
);
const ConnectedJoinFlow = connect(
() => ({}),
mapDispatchToProps,
mergeProps
)(IntlJoinFlow);
module.exports = ConnectedJoinFlow;