From 90129818f250e90246f00c8448d05fff3c7f4659 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 6 May 2022 16:51:36 -0500 Subject: [PATCH] RegistrationForm: convert to tsx --- app/soapbox/components/showable_password.tsx | 9 + .../components/registration_form.js | Bin 13412 -> 0 bytes .../components/registration_form.tsx | 348 ++++++++++++++++++ app/soapbox/features/forms/index.tsx | 15 + 4 files changed, 372 insertions(+) delete mode 100644 app/soapbox/features/auth_login/components/registration_form.js create mode 100644 app/soapbox/features/auth_login/components/registration_form.tsx diff --git a/app/soapbox/components/showable_password.tsx b/app/soapbox/components/showable_password.tsx index 40835decd9..7d7858fdc5 100644 --- a/app/soapbox/components/showable_password.tsx +++ b/app/soapbox/components/showable_password.tsx @@ -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, + onBlur?: React.ChangeEventHandler, + value?: string, } const ShowablePassword: React.FC = (props) => { diff --git a/app/soapbox/features/auth_login/components/registration_form.js b/app/soapbox/features/auth_login/components/registration_form.js deleted file mode 100644 index 898a1b7bd619d343c92130a289830a0db033baeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13412 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 diff --git a/app/soapbox/features/auth_login/components/registration_form.tsx b/app/soapbox/features/auth_login/components/registration_form.tsx new file mode 100644 index 0000000000..ccbd64c048 --- /dev/null +++ b/app/soapbox/features/auth_login/components/registration_form.tsx @@ -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 = ({ 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()); + const [captchaIdempotencyKey, setCaptchaIdempotencyKey] = useState(uuidv4()); + const [usernameUnavailable, setUsernameUnavailable] = useState(false); + const [passwordConfirmation, setPasswordConfirmation] = useState(''); + const [passwordMismatch, setPasswordMismatch] = useState(false); + const [birthday, setBirthday] = useState(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 = e => { + updateParams({ [e.target.name]: e.target.value }); + }; + + const onUsernameChange: React.ChangeEventHandler = e => { + updateParams({ username: e.target.value }); + setUsernameUnavailable(false); + source.current.cancel(); + + usernameAvailable(e.target.value); + }; + + const onCheckboxChange: React.ChangeEventHandler = e => { + updateParams({ [e.target.name]: e.target.checked }); + }; + + const onPasswordChange: React.ChangeEventHandler = e => { + const password = e.target.value; + onInputChange(e); + + if (password === passwordConfirmation) { + setPasswordMismatch(false); + } + }; + + const onPasswordConfirmChange: React.ChangeEventHandler = e => { + const password = params.get('password', ''); + const passwordConfirmation = e.target.value; + setPasswordConfirmation(passwordConfirmation); + + if (password === passwordConfirmation) { + setPasswordMismatch(false); + } + }; + + const onPasswordConfirmBlur: React.ChangeEventHandler = () => { + setPasswordMismatch(!passwordsMatch()); + }; + + const onBirthdayChange = (newBirthday: Date) => { + setBirthday(newBirthday); + }; + + const launchModal = () => { + const message = (<> + {needsConfirmation &&

+ {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), + })); + }; + + 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) => { + 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 ( + +
+
+
+ {usernameUnavailable && ( +
+ +
+ )} + + + {passwordMismatch && ( +
+ +
+ )} + + + {birthdayRequired && + } + {instance.get('approval_required') && + } + hint={} + name='reason' + maxLength={500} + onChange={onInputChange} + value={params.get('reason', '')} + required + />} +
+ +
+ {intl.formatMessage(messages.tos)} })} + name='agreement' + onChange={onCheckboxChange} + checked={params.get('agreement', false)} + required + /> + {supportsEmailList && } +
+
+ +
+
+
+
+ ); +}; + +export default RegistrationForm; diff --git a/app/soapbox/features/forms/index.tsx b/app/soapbox/features/forms/index.tsx index 79495c6cfc..c458cd6b1f 100644 --- a/app/soapbox/features/forms/index.tsx +++ b/app/soapbox/features/forms/index.tsx @@ -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 = (props) => { @@ -104,6 +108,9 @@ interface ISimpleTextarea { value?: string, onChange?: React.ChangeEventHandler, rows?: number, + name?: string, + maxLength?: number, + required?: boolean, } export const SimpleTextarea: React.FC = (props) => { @@ -161,6 +168,7 @@ interface ICheckbox { name?: string, checked?: boolean, onChange?: React.ChangeEventHandler, + required?: boolean, } export const Checkbox: React.FC = (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 = props => (