Files
ncd-fe/app/soapbox/features/auth_login/components/registration_form.js
2022-03-17 20:17:28 -05:00

378 lines
13 KiB
JavaScript

import { CancelToken } from 'axios';
import { Map as ImmutableMap } from 'immutable';
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';
import { accountLookup } from 'soapbox/actions/accounts';
import { register, verifyCredentials } from 'soapbox/actions/auth';
import { openModal } from 'soapbox/actions/modals';
import { getSettings } from 'soapbox/actions/settings';
import BirthdayInput from 'soapbox/components/birthday_input';
import ShowablePassword from 'soapbox/components/showable_password';
import CaptchaField from 'soapbox/features/auth_login/components/captcha';
import {
SimpleForm,
SimpleInput,
TextInput,
SimpleTextarea,
Checkbox,
} from 'soapbox/features/forms';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' },
username_hint: { id: 'registration.fields.username_hint', defaultMessage: 'Only letters, numbers, and underscores are allowed.' },
email: { id: 'registration.fields.email_placeholder', defaultMessage: 'E-Mail address' },
password: { id: 'registration.fields.password_placeholder', defaultMessage: 'Password' },
confirm: { id: 'registration.fields.confirm_placeholder', defaultMessage: 'Password (again)' },
agreement: { id: 'registration.agreement', defaultMessage: 'I agree to the {tos}.' },
tos: { id: 'registration.tos', defaultMessage: 'Terms of Service' },
close: { id: 'registration.confirmation_modal.close', defaultMessage: 'Close' },
newsletter: { id: 'registration.newsletter', defaultMessage: 'Subscribe to newsletter.' },
needsConfirmationHeader: { id: 'confirmations.register.needs_confirmation.header', defaultMessage: 'Confirmation needed' },
needsApprovalHeader: { id: 'confirmations.register.needs_approval.header', defaultMessage: 'Approval needed' },
});
const mapStateToProps = (state, props) => ({
instance: state.get('instance'),
locale: getSettings(state).get('locale'),
needsConfirmation: state.getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']),
needsApproval: state.getIn(['instance', 'approval_required']),
supportsEmailList: getFeatures(state.get('instance')).emailList,
supportsAccountLookup: getFeatures(state.get('instance')).accountLookup,
birthdayRequired: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_required']),
});
export default @connect(mapStateToProps)
@injectIntl
@withRouter
class RegistrationForm extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
instance: ImmutablePropTypes.map,
locale: PropTypes.string,
needsConfirmation: PropTypes.bool,
needsApproval: PropTypes.bool,
supportsEmailList: PropTypes.bool,
supportsAccountLookup: PropTypes.bool,
inviteToken: PropTypes.string,
birthdayRequired: PropTypes.bool,
history: PropTypes.object,
}
state = {
captchaLoading: true,
submissionLoading: false,
params: ImmutableMap(),
captchaIdempotencyKey: uuidv4(),
usernameUnavailable: false,
passwordConfirmation: '',
passwordMismatch: false,
}
source = CancelToken.source();
refreshCancelToken = () => {
this.source.cancel();
this.source = CancelToken.source();
return this.source;
}
setParams = map => {
this.setState({ params: this.state.params.merge(ImmutableMap(map)) });
}
onInputChange = e => {
this.setParams({ [e.target.name]: e.target.value });
}
onUsernameChange = e => {
this.setParams({ username: e.target.value });
this.setState({ usernameUnavailable: false });
this.source.cancel();
this.usernameAvailable(e.target.value);
}
onCheckboxChange = e => {
this.setParams({ [e.target.name]: e.target.checked });
}
onPasswordChange = e => {
const password = e.target.value;
const { passwordConfirmation } = this.state;
this.onInputChange(e);
if (password === passwordConfirmation) {
this.setState({ passwordMismatch: false });
}
}
onPasswordConfirmChange = e => {
const password = this.state.params.get('password', '');
const passwordConfirmation = e.target.value;
this.setState({ passwordConfirmation });
if (password === passwordConfirmation) {
this.setState({ passwordMismatch: false });
}
}
onPasswordConfirmBlur = e => {
this.setState({ passwordMismatch: !this.passwordsMatch() });
}
onBirthdayChange = birthday => {
this.setState({
birthday,
});
}
launchModal = () => {
const { dispatch, intl, needsConfirmation, needsApproval } = this.props;
const message = (<>
{needsConfirmation && <p>
<FormattedMessage
id='confirmations.register.needs_confirmation'
defaultMessage='Please check your inbox at {email} for confirmation instructions. You will need to verify your email address to continue.'
values={{ email: <strong>{this.state.params.get('email')}</strong> }}
/></p>}
{needsApproval && <p>
<FormattedMessage
id='confirmations.register.needs_approval'
defaultMessage='Your account will be manually approved by an admin. Please be patient while we review your details.'
/></p>}
</>);
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/icons/check.svg'),
heading: needsConfirmation
? intl.formatMessage(messages.needsConfirmationHeader)
: needsApproval
? intl.formatMessage(messages.needsApprovalHeader)
: undefined,
message,
confirm: intl.formatMessage(messages.close),
}));
}
postRegisterAction = ({ access_token }) => {
const { dispatch, needsConfirmation, needsApproval, history } = this.props;
if (needsConfirmation || needsApproval) {
return this.launchModal();
} else {
return dispatch(verifyCredentials(access_token)).then(() => {
history.push('/');
});
}
}
passwordsMatch = () => {
const { params, passwordConfirmation } = this.state;
return params.get('password', '') === passwordConfirmation;
}
usernameAvailable = debounce(username => {
const { dispatch, supportsAccountLookup } = this.props;
if (!supportsAccountLookup) return;
const source = this.refreshCancelToken();
dispatch(accountLookup(username, source.token))
.then(account => {
this.setState({ usernameUnavailable: !!account });
})
.catch((error) => {
if (error.response?.status === 404) {
this.setState({ usernameUnavailable: false });
}
});
}, 1000, { trailing: true });
onSubmit = e => {
const { dispatch, inviteToken } = this.props;
const { birthday } = this.state;
if (!this.passwordsMatch()) {
this.setState({ passwordMismatch: true });
return;
}
const params = this.state.params.withMutations(params => {
// Locale for confirmation email
params.set('locale', this.props.locale);
// Pleroma invites
if (inviteToken) {
params.set('token', inviteToken);
}
if (birthday) {
params.set('birthday', new Date(birthday.getTime() - (birthday.getTimezoneOffset() * 60000)).toISOString().slice(0, 10));
}
});
this.setState({ submissionLoading: true });
dispatch(register(params.toJS()))
.then(this.postRegisterAction)
.catch(error => {
this.setState({ submissionLoading: false });
this.refreshCaptcha();
});
}
onCaptchaClick = e => {
this.refreshCaptcha();
}
onFetchCaptcha = captcha => {
this.setState({ captchaLoading: false });
this.setParams({
captcha_token: captcha.get('token'),
captcha_answer_data: captcha.get('answer_data'),
});
}
onFetchCaptchaFail = error => {
this.setState({ captchaLoading: false });
}
refreshCaptcha = () => {
this.setState({ captchaIdempotencyKey: uuidv4() });
this.setParams({ captcha_solution: '' });
}
render() {
const { instance, intl, supportsEmailList, birthdayRequired } = this.props;
const { params, usernameUnavailable, passwordConfirmation, passwordMismatch, birthday } = this.state;
const isLoading = this.state.captchaLoading || this.state.submissionLoading;
return (
<SimpleForm onSubmit={this.onSubmit}>
<fieldset disabled={isLoading}>
<div className='simple_form__overlay-area'>
<div className='fields-group'>
{usernameUnavailable && (
<div className='error'>
<FormattedMessage id='registration.username_unavailable' defaultMessage='Username is already taken.' />
</div>
)}
<TextInput
placeholder={intl.formatMessage(messages.username)}
name='username'
hint={intl.formatMessage(messages.username_hint)}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
pattern='^[a-zA-Z\d_-]+'
onChange={this.onUsernameChange}
value={params.get('username', '')}
error={usernameUnavailable}
required
/>
<SimpleInput
placeholder={intl.formatMessage(messages.email)}
name='email'
type='email'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={this.onInputChange}
value={params.get('email', '')}
required
/>
{passwordMismatch && (
<div className='error'>
<FormattedMessage id='registration.password_mismatch' defaultMessage="Passwords don't match." />
</div>
)}
<ShowablePassword
placeholder={intl.formatMessage(messages.password)}
name='password'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={this.onPasswordChange}
value={params.get('password', '')}
error={passwordMismatch === true}
required
/>
<ShowablePassword
placeholder={intl.formatMessage(messages.confirm)}
name='password_confirmation'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
onChange={this.onPasswordConfirmChange}
onBlur={this.onPasswordConfirmBlur}
value={passwordConfirmation}
error={passwordMismatch === true}
required
/>
{birthdayRequired &&
<BirthdayInput
value={birthday}
onChange={this.onBirthdayChange}
required
/>}
{instance.get('approval_required') &&
<SimpleTextarea
label={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
hint={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
name='reason'
maxLength={500}
onChange={this.onInputChange}
value={params.get('reason', '')}
required
/>}
</div>
<CaptchaField
onFetch={this.onFetchCaptcha}
onFetchFail={this.onFetchCaptchaFail}
onChange={this.onInputChange}
onClick={this.onCaptchaClick}
idempotencyKey={this.state.captchaIdempotencyKey}
name='captcha_solution'
value={params.get('captcha_solution', '')}
/>
<div className='fields-group'>
<Checkbox
label={intl.formatMessage(messages.agreement, { tos: <Link to='/about/tos' target='_blank' key={0}>{intl.formatMessage(messages.tos)}</Link> })}
name='agreement'
onChange={this.onCheckboxChange}
checked={params.get('agreement', false)}
required
/>
{supportsEmailList && <Checkbox
label={intl.formatMessage(messages.newsletter)}
name='accepts_email_list'
onChange={this.onCheckboxChange}
checked={params.get('accepts_email_list', false)}
/>}
</div>
<div className='actions'>
<button name='button' type='submit' className='btn button button-primary'>
<FormattedMessage id='registration.sign_up' defaultMessage='Sign up' />
</button>
</div>
</div>
</fieldset>
</SimpleForm>
);
}
}