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 } 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/modal'; 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 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, } static contextTypes = { router: 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 &&

{this.state.params.get('email')} }} />

} {needsApproval &&

} ); 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 } = this.props; const { router } = this.context; if (needsConfirmation || needsApproval) { return this.launchModal(); } else { return dispatch(verifyCredentials(access_token)).then(() => { router.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 && 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 (
{usernameUnavailable && (
)} {passwordMismatch && (
)} {birthdayRequired && } {instance.get('approval_required') && } hint={} name='reason' maxLength={500} onChange={this.onInputChange} value={params.get('reason', '')} required />}
{intl.formatMessage(messages.tos)} })} name='agreement' onChange={this.onCheckboxChange} checked={params.get('agreement', false)} required /> {supportsEmailList && }
); } }