2022-05-06 21:51:36 +00:00
|
|
|
import axios from 'axios';
|
|
|
|
import { Map as ImmutableMap } from 'immutable';
|
2022-06-16 19:32:17 +00:00
|
|
|
import debounce from 'lodash/debounce';
|
2022-05-07 01:14:04 +00:00
|
|
|
import React, { useState, useRef, useCallback } from 'react';
|
2022-05-06 21:51:36 +00:00
|
|
|
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';
|
2022-11-15 14:10:14 +00:00
|
|
|
import BirthdayInput from 'soapbox/components/birthday-input';
|
2022-05-09 17:17:58 +00:00
|
|
|
import { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea } from 'soapbox/components/ui';
|
2022-11-15 17:23:36 +00:00
|
|
|
import CaptchaField from 'soapbox/features/auth-login/components/captcha';
|
2022-11-26 16:38:16 +00:00
|
|
|
import { useAppDispatch, useSettings, useFeatures, useInstance } from 'soapbox/hooks';
|
2022-05-06 21:51:36 +00:00
|
|
|
|
|
|
|
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.' },
|
2022-05-09 17:00:27 +00:00
|
|
|
usernameUnavailable: { id: 'registration.username_unavailable', defaultMessage: 'Username is already taken.' },
|
2022-05-06 21:51:36 +00:00
|
|
|
email: { id: 'registration.fields.email_placeholder', defaultMessage: 'E-Mail address' },
|
|
|
|
password: { id: 'registration.fields.password_placeholder', defaultMessage: 'Password' },
|
2022-05-09 17:17:58 +00:00
|
|
|
passwordMismatch: { id: 'registration.password_mismatch', defaultMessage: 'Passwords don\'t match.' },
|
2022-05-06 21:51:36 +00:00
|
|
|
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' },
|
2023-03-21 20:36:53 +00:00
|
|
|
reasonHint: { id: 'registration.reason_hint', defaultMessage: 'This will help us review your application' },
|
2022-05-06 21:51:36 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
interface IRegistrationForm {
|
2023-02-15 21:26:27 +00:00
|
|
|
inviteToken?: string
|
2022-05-06 21:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** 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();
|
2022-11-26 16:38:16 +00:00
|
|
|
const instance = useInstance();
|
2022-05-06 21:51:36 +00:00
|
|
|
|
|
|
|
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 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());
|
|
|
|
};
|
|
|
|
|
2022-05-27 17:13:44 +00:00
|
|
|
const onBirthdayChange = (birthday: string) => {
|
|
|
|
updateParams({ birthday });
|
2022-05-06 21:51:36 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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', {
|
2022-07-09 16:20:02 +00:00
|
|
|
icon: require('@tabler/icons/check.svg'),
|
2022-05-06 21:51:36 +00:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2022-05-07 01:14:04 +00:00
|
|
|
const usernameAvailable = useCallback(debounce(username => {
|
2022-05-06 21:51:36 +00:00
|
|
|
if (!supportsAccountLookup) return;
|
|
|
|
|
|
|
|
const source = refreshCancelToken();
|
|
|
|
|
|
|
|
dispatch(accountLookup(username, source.token))
|
|
|
|
.then(account => {
|
|
|
|
setUsernameUnavailable(!!account);
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
if (error.response?.status === 404) {
|
|
|
|
setUsernameUnavailable(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-05-07 01:14:04 +00:00
|
|
|
}, 1000, { trailing: true }), []);
|
2022-05-06 21:51:36 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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 (
|
2022-05-06 22:13:00 +00:00
|
|
|
<Form onSubmit={onSubmit} data-testid='registrations-open'>
|
|
|
|
<fieldset disabled={isLoading} className='space-y-3'>
|
2023-01-10 23:03:15 +00:00
|
|
|
<>
|
|
|
|
<FormGroup
|
|
|
|
hintText={intl.formatMessage(messages.username_hint)}
|
|
|
|
errors={usernameUnavailable ? [intl.formatMessage(messages.usernameUnavailable)] : undefined}
|
|
|
|
>
|
|
|
|
<Input
|
|
|
|
type='text'
|
|
|
|
name='username'
|
|
|
|
placeholder={intl.formatMessage(messages.username)}
|
|
|
|
autoComplete='off'
|
|
|
|
autoCorrect='off'
|
|
|
|
autoCapitalize='off'
|
|
|
|
pattern='^[a-zA-Z\d_-]+'
|
2023-01-15 19:25:42 +00:00
|
|
|
icon={require('@tabler/icons/at.svg')}
|
2023-01-10 23:03:15 +00:00
|
|
|
onChange={onUsernameChange}
|
|
|
|
value={params.get('username', '')}
|
|
|
|
required
|
|
|
|
/>
|
|
|
|
</FormGroup>
|
|
|
|
|
2022-05-06 22:18:36 +00:00
|
|
|
<Input
|
2023-01-10 23:03:15 +00:00
|
|
|
type='email'
|
|
|
|
name='email'
|
|
|
|
placeholder={intl.formatMessage(messages.email)}
|
2022-05-06 22:18:36 +00:00
|
|
|
autoComplete='off'
|
|
|
|
autoCorrect='off'
|
|
|
|
autoCapitalize='off'
|
2023-01-10 23:03:15 +00:00
|
|
|
onChange={onInputChange}
|
|
|
|
value={params.get('email', '')}
|
2022-05-06 22:18:36 +00:00
|
|
|
required
|
|
|
|
/>
|
2023-01-10 23:03:15 +00:00
|
|
|
|
2022-05-09 17:00:27 +00:00
|
|
|
<Input
|
|
|
|
type='password'
|
2023-01-10 23:03:15 +00:00
|
|
|
name='password'
|
|
|
|
placeholder={intl.formatMessage(messages.password)}
|
2022-05-09 17:00:27 +00:00
|
|
|
autoComplete='off'
|
|
|
|
autoCorrect='off'
|
|
|
|
autoCapitalize='off'
|
2023-01-10 23:03:15 +00:00
|
|
|
onChange={onPasswordChange}
|
|
|
|
value={params.get('password', '')}
|
2022-05-09 17:00:27 +00:00
|
|
|
required
|
|
|
|
/>
|
2022-05-06 22:18:36 +00:00
|
|
|
|
|
|
|
<FormGroup
|
2023-01-10 23:03:15 +00:00
|
|
|
errors={passwordMismatch ? [intl.formatMessage(messages.passwordMismatch)] : undefined}
|
2022-05-06 22:18:36 +00:00
|
|
|
>
|
2023-01-10 23:03:15 +00:00
|
|
|
<Input
|
|
|
|
type='password'
|
|
|
|
name='password_confirmation'
|
|
|
|
placeholder={intl.formatMessage(messages.confirm)}
|
|
|
|
autoComplete='off'
|
|
|
|
autoCorrect='off'
|
|
|
|
autoCapitalize='off'
|
|
|
|
onChange={onPasswordConfirmChange}
|
|
|
|
onBlur={onPasswordConfirmBlur}
|
|
|
|
value={passwordConfirmation}
|
2022-05-06 21:51:36 +00:00
|
|
|
required
|
|
|
|
/>
|
2022-05-06 22:18:36 +00:00
|
|
|
</FormGroup>
|
2023-01-10 23:03:15 +00:00
|
|
|
|
|
|
|
{birthdayRequired && (
|
|
|
|
<BirthdayInput
|
|
|
|
value={params.get('birthday')}
|
|
|
|
onChange={onBirthdayChange}
|
|
|
|
required
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{needsApproval && (
|
|
|
|
<FormGroup
|
|
|
|
labelText={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
|
|
|
|
>
|
|
|
|
<Textarea
|
|
|
|
name='reason'
|
2023-03-21 20:36:53 +00:00
|
|
|
placeholder={intl.formatMessage(messages.reasonHint)}
|
2023-01-10 23:03:15 +00:00
|
|
|
maxLength={500}
|
|
|
|
onChange={onInputChange}
|
|
|
|
value={params.get('reason', '')}
|
2023-03-21 20:36:53 +00:00
|
|
|
autoGrow
|
2023-01-10 23:03:15 +00:00
|
|
|
required
|
|
|
|
/>
|
|
|
|
</FormGroup>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<CaptchaField
|
|
|
|
onFetch={onFetchCaptcha}
|
|
|
|
onFetchFail={onFetchCaptchaFail}
|
|
|
|
onChange={onInputChange}
|
|
|
|
onClick={onCaptchaClick}
|
|
|
|
idempotencyKey={captchaIdempotencyKey}
|
|
|
|
name='captcha_solution'
|
|
|
|
value={params.get('captcha_solution', '')}
|
2022-05-06 21:51:36 +00:00
|
|
|
/>
|
2022-05-06 22:18:36 +00:00
|
|
|
|
2023-01-10 23:03:15 +00:00
|
|
|
<FormGroup
|
|
|
|
labelText={intl.formatMessage(messages.agreement, { tos: <Link to='/about/tos' target='_blank' key={0}>{intl.formatMessage(messages.tos)}</Link> })}
|
|
|
|
>
|
2022-05-06 21:51:36 +00:00
|
|
|
<Checkbox
|
2023-01-10 23:03:15 +00:00
|
|
|
name='agreement'
|
2022-05-06 21:51:36 +00:00
|
|
|
onChange={onCheckboxChange}
|
2023-01-10 23:03:15 +00:00
|
|
|
checked={params.get('agreement', false)}
|
|
|
|
required
|
2022-05-06 22:18:36 +00:00
|
|
|
/>
|
2022-05-09 13:52:41 +00:00
|
|
|
</FormGroup>
|
2022-05-06 22:18:36 +00:00
|
|
|
|
2023-01-10 23:03:15 +00:00
|
|
|
{supportsEmailList && (
|
|
|
|
<FormGroup labelText={intl.formatMessage(messages.newsletter)}>
|
|
|
|
<Checkbox
|
|
|
|
name='accepts_email_list'
|
|
|
|
onChange={onCheckboxChange}
|
|
|
|
checked={params.get('accepts_email_list', false)}
|
|
|
|
/>
|
|
|
|
</FormGroup>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<FormActions>
|
|
|
|
<Button type='submit'>
|
|
|
|
<FormattedMessage id='registration.sign_up' defaultMessage='Sign up' />
|
|
|
|
</Button>
|
|
|
|
</FormActions>
|
|
|
|
</>
|
2022-05-06 21:51:36 +00:00
|
|
|
</fieldset>
|
2022-05-06 22:13:00 +00:00
|
|
|
</Form>
|
2022-05-06 21:51:36 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default RegistrationForm;
|