RegistrationForm: convert to tsx
This commit is contained in:
parent
45a7dc5bcf
commit
90129818f2
4 changed files with 372 additions and 377 deletions
|
@ -14,8 +14,17 @@ interface IShowablePassword {
|
|||
label?: React.ReactNode,
|
||||
className?: string,
|
||||
hint?: React.ReactNode,
|
||||
placeholder?: string,
|
||||
error?: boolean,
|
||||
onToggleVisibility?: () => void,
|
||||
autoComplete?: string,
|
||||
autoCorrect?: string,
|
||||
autoCapitalize?: string,
|
||||
name?: string,
|
||||
required?: boolean,
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>,
|
||||
onBlur?: React.ChangeEventHandler<HTMLInputElement>,
|
||||
value?: string,
|
||||
}
|
||||
|
||||
const ShowablePassword: React.FC<IShowablePassword> = (props) => {
|
||||
|
|
|
@ -1,377 +0,0 @@
|
|||
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} data-testid='registrations-open'>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
348
app/soapbox/features/auth_login/components/registration_form.tsx
Normal file
348
app/soapbox/features/auth_login/components/registration_form.tsx
Normal file
|
@ -0,0 +1,348 @@
|
|||
import axios from 'axios';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { Link, useHistory } 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 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 { useAppSelector, useAppDispatch, useSettings, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
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' },
|
||||
});
|
||||
|
||||
interface IRegistrationForm {
|
||||
inviteToken?: string,
|
||||
}
|
||||
|
||||
/** Allows the user to sign up for the website. */
|
||||
const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const settings = useSettings();
|
||||
const features = useFeatures();
|
||||
const instance = useAppSelector(state => state.instance);
|
||||
|
||||
const locale = settings.get('locale');
|
||||
const needsConfirmation = !!instance.pleroma.getIn(['metadata', 'account_activation_required']);
|
||||
const needsApproval = instance.approval_required;
|
||||
const supportsEmailList = features.emailList;
|
||||
const supportsAccountLookup = features.accountLookup;
|
||||
const birthdayRequired = instance.pleroma.getIn(['metadata', 'birthday_required']);
|
||||
|
||||
const [captchaLoading, setCaptchaLoading] = useState(true);
|
||||
const [submissionLoading, setSubmissionLoading] = useState(false);
|
||||
const [params, setParams] = useState(ImmutableMap<string, any>());
|
||||
const [captchaIdempotencyKey, setCaptchaIdempotencyKey] = useState(uuidv4());
|
||||
const [usernameUnavailable, setUsernameUnavailable] = useState(false);
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('');
|
||||
const [passwordMismatch, setPasswordMismatch] = useState(false);
|
||||
const [birthday, setBirthday] = useState<Date | undefined>(undefined);
|
||||
|
||||
const source = useRef(axios.CancelToken.source());
|
||||
|
||||
const refreshCancelToken = () => {
|
||||
source.current.cancel();
|
||||
source.current = axios.CancelToken.source();
|
||||
return source.current;
|
||||
};
|
||||
|
||||
const updateParams = (map: any) => {
|
||||
setParams(params.merge(ImmutableMap(map)));
|
||||
};
|
||||
|
||||
const onInputChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = e => {
|
||||
updateParams({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const onUsernameChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
updateParams({ username: e.target.value });
|
||||
setUsernameUnavailable(false);
|
||||
source.current.cancel();
|
||||
|
||||
usernameAvailable(e.target.value);
|
||||
};
|
||||
|
||||
const onCheckboxChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
updateParams({ [e.target.name]: e.target.checked });
|
||||
};
|
||||
|
||||
const onPasswordChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const password = e.target.value;
|
||||
onInputChange(e);
|
||||
|
||||
if (password === passwordConfirmation) {
|
||||
setPasswordMismatch(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const password = params.get('password', '');
|
||||
const passwordConfirmation = e.target.value;
|
||||
setPasswordConfirmation(passwordConfirmation);
|
||||
|
||||
if (password === passwordConfirmation) {
|
||||
setPasswordMismatch(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordConfirmBlur: React.ChangeEventHandler<HTMLInputElement> = () => {
|
||||
setPasswordMismatch(!passwordsMatch());
|
||||
};
|
||||
|
||||
const onBirthdayChange = (newBirthday: Date) => {
|
||||
setBirthday(newBirthday);
|
||||
};
|
||||
|
||||
const launchModal = () => {
|
||||
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>{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),
|
||||
}));
|
||||
};
|
||||
|
||||
const postRegisterAction = ({ access_token }: any) => {
|
||||
if (needsConfirmation || needsApproval) {
|
||||
return launchModal();
|
||||
} else {
|
||||
return dispatch(verifyCredentials(access_token)).then(() => {
|
||||
history.push('/');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const passwordsMatch = () => {
|
||||
return params.get('password', '') === passwordConfirmation;
|
||||
};
|
||||
|
||||
const usernameAvailable = debounce(username => {
|
||||
if (!supportsAccountLookup) return;
|
||||
|
||||
const source = refreshCancelToken();
|
||||
|
||||
dispatch(accountLookup(username, source.token))
|
||||
.then(account => {
|
||||
setUsernameUnavailable(!!account);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?.status === 404) {
|
||||
setUsernameUnavailable(false);
|
||||
}
|
||||
});
|
||||
|
||||
}, 1000, { trailing: true });
|
||||
|
||||
const onSubmit: React.FormEventHandler = () => {
|
||||
if (!passwordsMatch()) {
|
||||
setPasswordMismatch(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalParams = params.withMutations(params => {
|
||||
// Locale for confirmation email
|
||||
params.set('locale', 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));
|
||||
}
|
||||
});
|
||||
|
||||
setSubmissionLoading(true);
|
||||
|
||||
dispatch(register(normalParams.toJS()))
|
||||
.then(postRegisterAction)
|
||||
.catch(() => {
|
||||
setSubmissionLoading(false);
|
||||
refreshCaptcha();
|
||||
});
|
||||
};
|
||||
|
||||
const onCaptchaClick: React.MouseEventHandler = () => {
|
||||
refreshCaptcha();
|
||||
};
|
||||
|
||||
const onFetchCaptcha = (captcha: ImmutableMap<string, any>) => {
|
||||
setCaptchaLoading(false);
|
||||
updateParams({
|
||||
captcha_token: captcha.get('token'),
|
||||
captcha_answer_data: captcha.get('answer_data'),
|
||||
});
|
||||
};
|
||||
|
||||
const onFetchCaptchaFail = () => {
|
||||
setCaptchaLoading(false);
|
||||
};
|
||||
|
||||
const refreshCaptcha = () => {
|
||||
setCaptchaIdempotencyKey(uuidv4());
|
||||
updateParams({ captcha_solution: '' });
|
||||
};
|
||||
|
||||
const isLoading = captchaLoading || submissionLoading;
|
||||
|
||||
return (
|
||||
<SimpleForm onSubmit={onSubmit} data-testid='registrations-open'>
|
||||
<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={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={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={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={onPasswordConfirmChange}
|
||||
onBlur={onPasswordConfirmBlur}
|
||||
value={passwordConfirmation}
|
||||
error={passwordMismatch === true}
|
||||
required
|
||||
/>
|
||||
{birthdayRequired &&
|
||||
<BirthdayInput
|
||||
value={birthday}
|
||||
onChange={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={onInputChange}
|
||||
value={params.get('reason', '')}
|
||||
required
|
||||
/>}
|
||||
</div>
|
||||
<CaptchaField
|
||||
onFetch={onFetchCaptcha}
|
||||
onFetchFail={onFetchCaptchaFail}
|
||||
onChange={onInputChange}
|
||||
onClick={onCaptchaClick}
|
||||
idempotencyKey={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={onCheckboxChange}
|
||||
checked={params.get('agreement', false)}
|
||||
required
|
||||
/>
|
||||
{supportsEmailList && <Checkbox
|
||||
label={intl.formatMessage(messages.newsletter)}
|
||||
name='accepts_email_list'
|
||||
onChange={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>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistrationForm;
|
|
@ -85,6 +85,10 @@ interface ISimpleInput {
|
|||
name?: string,
|
||||
placeholder?: string,
|
||||
value?: string | number,
|
||||
autoComplete?: string,
|
||||
autoCorrect?: string,
|
||||
autoCapitalize?: string,
|
||||
required?: boolean,
|
||||
}
|
||||
|
||||
export const SimpleInput: React.FC<ISimpleInput> = (props) => {
|
||||
|
@ -104,6 +108,9 @@ interface ISimpleTextarea {
|
|||
value?: string,
|
||||
onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,
|
||||
rows?: number,
|
||||
name?: string,
|
||||
maxLength?: number,
|
||||
required?: boolean,
|
||||
}
|
||||
|
||||
export const SimpleTextarea: React.FC<ISimpleTextarea> = (props) => {
|
||||
|
@ -161,6 +168,7 @@ interface ICheckbox {
|
|||
name?: string,
|
||||
checked?: boolean,
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>,
|
||||
required?: boolean,
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<ICheckbox> = (props) => (
|
||||
|
@ -240,8 +248,15 @@ interface ITextInput {
|
|||
name?: string,
|
||||
onChange?: React.ChangeEventHandler,
|
||||
label?: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
autoComplete?: string,
|
||||
autoCorrect?: string,
|
||||
autoCapitalize?: string,
|
||||
pattern?: string,
|
||||
error?: boolean,
|
||||
required?: boolean,
|
||||
}
|
||||
|
||||
export const TextInput: React.FC<ITextInput> = props => (
|
||||
|
|
Loading…
Reference in a new issue