diff --git a/app/soapbox/components/showable_password.tsx b/app/soapbox/components/showable_password.tsx deleted file mode 100644 index 40835decd..000000000 --- a/app/soapbox/components/showable_password.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import classNames from 'classnames'; -import React, { useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; - -import IconButton from 'soapbox/components/icon_button'; -import { InputContainer, LabelInputContainer } from 'soapbox/features/forms'; - -const messages = defineMessages({ - showPassword: { id: 'forms.show_password', defaultMessage: 'Show password' }, - hidePassword: { id: 'forms.hide_password', defaultMessage: 'Hide password' }, -}); - -interface IShowablePassword { - label?: React.ReactNode, - className?: string, - hint?: React.ReactNode, - error?: boolean, - onToggleVisibility?: () => void, -} - -const ShowablePassword: React.FC = (props) => { - const intl = useIntl(); - const [revealed, setRevealed] = useState(false); - - const { hint, error, label, className, ...rest } = props; - - const toggleReveal = () => { - if (props.onToggleVisibility) { - props.onToggleVisibility(); - } else { - setRevealed(!revealed); - } - }; - - const revealButton = ( - - ); - - return ( - - {label ? ( - - - {revealButton} - - ) : (<> - - {revealButton} - )} - - ); -}; - -export default ShowablePassword; diff --git a/app/soapbox/components/ui/checkbox/checkbox.tsx b/app/soapbox/components/ui/checkbox/checkbox.tsx new file mode 100644 index 000000000..dc7a4e370 --- /dev/null +++ b/app/soapbox/components/ui/checkbox/checkbox.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +interface ICheckbox extends Pick, 'disabled' | 'id' | 'name' | 'onChange' | 'checked' | 'required'> { } + +/** A pretty checkbox input. */ +const Checkbox = React.forwardRef((props, ref) => { + return ( + + ); +}); + +export default Checkbox; diff --git a/app/soapbox/components/ui/form-group/form-group.tsx b/app/soapbox/components/ui/form-group/form-group.tsx index e90448e8c..75517fc79 100644 --- a/app/soapbox/components/ui/form-group/form-group.tsx +++ b/app/soapbox/components/ui/form-group/form-group.tsx @@ -1,6 +1,10 @@ import React, { useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; +import Checkbox from '../checkbox/checkbox'; +import HStack from '../hstack/hstack'; +import Stack from '../stack/stack'; + interface IFormGroup { /** Input label message. */ labelText?: React.ReactNode, @@ -10,17 +14,56 @@ interface IFormGroup { errors?: string[] } -/** Input element with label and hint. */ +/** Input container with label. Renders the child. */ const FormGroup: React.FC = (props) => { const { children, errors = [], labelText, hintText } = props; const formFieldId: string = useMemo(() => `field-${uuidv4()}`, []); const inputChildren = React.Children.toArray(children); + const hasError = errors?.length > 0; let firstChild; if (React.isValidElement(inputChildren[0])) { firstChild = React.cloneElement( inputChildren[0], - { id: formFieldId }, + { id: formFieldId, hasError }, + ); + } + const isCheckboxFormGroup = firstChild?.type === Checkbox; + + if (isCheckboxFormGroup) { + return ( + + {firstChild} + + + {labelText && ( + + )} + + {hasError && ( +
+

+ {errors.join(', ')} +

+
+ )} + + {hintText && ( +

+ {hintText} +

+ )} +
+
); } @@ -40,7 +83,7 @@ const FormGroup: React.FC = (props) => { {firstChild} {inputChildren.filter((_, i) => i !== 0)} - {errors?.length > 0 && ( + {hasError && (

, 'maxLength' | 'onChange' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern'> { +interface IInput extends Pick, 'maxLength' | 'onChange' | 'onBlur' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled' | 'onClick' | 'readOnly' | 'min' | 'pattern'> { /** Put the cursor into the input on mount. */ autoFocus?: boolean, /** The initial text in the input. */ @@ -32,6 +32,8 @@ interface IInput extends Pick, 'maxL onChange?: (event: React.ChangeEvent) => void, /** HTML input type. */ type: 'text' | 'number' | 'email' | 'tel' | 'password', + /** Whether to display the input in red. */ + hasError?: boolean, } /** Form input element. */ @@ -39,7 +41,7 @@ const Input = React.forwardRef( (props, ref) => { const intl = useIntl(); - const { type = 'text', icon, className, outerClassName, ...filteredProps } = props; + const { type = 'text', icon, className, outerClassName, hasError, ...filteredProps } = props; const [revealed, setRevealed] = React.useState(false); @@ -65,6 +67,7 @@ const Input = React.forwardRef( 'dark:bg-slate-800 block w-full sm:text-sm border-gray-300 dark:border-gray-600 rounded-md focus:ring-indigo-500 focus:border-indigo-500': true, 'pr-7': isPassword, + 'text-red-600 border-red-600': hasError, 'pl-8': typeof icon !== 'undefined', }, className)} /> diff --git a/app/soapbox/features/auth_login/components/captcha.js b/app/soapbox/features/auth_login/components/captcha.js deleted file mode 100644 index bcdaf74a4..000000000 --- a/app/soapbox/features/auth_login/components/captcha.js +++ /dev/null @@ -1,125 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchCaptcha } from 'soapbox/actions/auth'; -import { TextInput } from 'soapbox/features/forms'; - -const noOp = () => {}; - -export default @connect() -class CaptchaField extends React.Component { - - static propTypes = { - onChange: PropTypes.func, - onFetch: PropTypes.func, - onFetchFail: PropTypes.func, - onClick: PropTypes.func, - dispatch: PropTypes.func, - refreshInterval: PropTypes.number, - idempotencyKey: PropTypes.string, - } - - static defaultProps = { - onChange: noOp, - onFetch: noOp, - onFetchFail: noOp, - onClick: noOp, - refreshInterval: 5*60*1000, // 5 minutes, Pleroma default - } - - state = { - captcha: ImmutableMap(), - refresh: undefined, - } - - startRefresh = () => { - const { refreshInterval } = this.props; - if (refreshInterval) { - const refresh = setInterval(this.fetchCaptcha, refreshInterval); - this.setState({ refresh }); - } - } - - endRefresh = () => { - clearInterval(this.state.refresh); - } - - forceRefresh = () => { - this.fetchCaptcha(); - this.endRefresh(); - this.startRefresh(); - } - - fetchCaptcha = () => { - const { dispatch, onFetch, onFetchFail } = this.props; - dispatch(fetchCaptcha()).then(response => { - const captcha = ImmutableMap(response.data); - this.setState({ captcha }); - onFetch(captcha); - }).catch(error => { - onFetchFail(error); - }); - } - - componentDidMount() { - this.fetchCaptcha(); - this.startRefresh(); // Refresh periodically - } - - componentWillUnmount() { - this.endRefresh(); - } - - componentDidUpdate(prevProps) { - if (this.props.idempotencyKey !== prevProps.idempotencyKey) { - this.forceRefresh(); - } - } - - render() { - const { captcha } = this.state; - const { onChange, onClick, ...props } = this.props; - - switch(captcha.get('type')) { - case 'native': - return ( -

-

{}

- -
- ); - case 'none': - default: - return null; - } - } - -} - -export const NativeCaptchaField = ({ captcha, onChange, onClick, name, value }) => ( -
- captcha - -
-); - -NativeCaptchaField.propTypes = { - captcha: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func, - onClick: PropTypes.func, - name: PropTypes.string, - value: PropTypes.string, -}; diff --git a/app/soapbox/features/auth_login/components/captcha.tsx b/app/soapbox/features/auth_login/components/captcha.tsx new file mode 100644 index 000000000..dcd51209c --- /dev/null +++ b/app/soapbox/features/auth_login/components/captcha.tsx @@ -0,0 +1,132 @@ +import { Map as ImmutableMap } from 'immutable'; +import React, { useState, useEffect } from 'react'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { fetchCaptcha } from 'soapbox/actions/auth'; +import { Stack, Text, Input } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; + +const noOp = () => {}; + +const messages = defineMessages({ + placeholder: { id: 'registration.captcha.placeholder', defaultMessage: 'Enter the pictured text' }, +}); + +interface ICaptchaField { + name?: string, + value: string, + onChange?: React.ChangeEventHandler, + onFetch?: (captcha: ImmutableMap) => void, + onFetchFail?: (error: Error) => void, + onClick?: React.MouseEventHandler, + refreshInterval?: number, + idempotencyKey: string, +} + +const CaptchaField: React.FC = ({ + name, + value, + onChange = noOp, + onFetch = noOp, + onFetchFail = noOp, + onClick = noOp, + refreshInterval = 5*60*1000, // 5 minutes, Pleroma default + idempotencyKey, +}) => { + const dispatch = useAppDispatch(); + + const [captcha, setCaptcha] = useState(ImmutableMap()); + const [refresh, setRefresh] = useState(undefined); + + const getCaptcha = () => { + dispatch(fetchCaptcha()).then(response => { + const captcha = ImmutableMap(response.data); + setCaptcha(captcha); + onFetch(captcha); + }).catch((error: Error) => { + onFetchFail(error); + }); + }; + + const startRefresh = () => { + if (refreshInterval) { + const newRefresh = setInterval(getCaptcha, refreshInterval); + setRefresh(newRefresh); + } + }; + + const endRefresh = () => { + if (refresh) { + clearInterval(refresh); + } + }; + + useEffect(() => { + getCaptcha(); + endRefresh(); + startRefresh(); // Refresh periodically + + return () => { + endRefresh(); + }; + }, [idempotencyKey]); + + switch(captcha.get('type')) { + case 'native': + return ( +
+ + + + + +
+ ); + case 'none': + default: + return null; + } +}; + +interface INativeCaptchaField { + captcha: ImmutableMap, + onChange: React.ChangeEventHandler, + onClick: React.MouseEventHandler, + name?: string, + value: string, +} + +const NativeCaptchaField: React.FC = ({ captcha, onChange, onClick, name, value }) => { + const intl = useIntl(); + + return ( + +
+ captcha +
+ + +
+ ); +}; + +export { + CaptchaField as default, + NativeCaptchaField, +}; 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 898a1b7bd..000000000 --- a/app/soapbox/features/auth_login/components/registration_form.js +++ /dev/null @@ -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 &&

- {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, 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 ( - -
-
-
- {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 && } -
-
- -
-
-
-
- ); - } - -} 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 000000000..22a7bbca9 --- /dev/null +++ b/app/soapbox/features/auth_login/components/registration_form.tsx @@ -0,0 +1,356 @@ +import axios from 'axios'; +import { Map as ImmutableMap } from 'immutable'; +import { debounce } from 'lodash'; +import React, { useState, useRef, useCallback } 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 { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea } from 'soapbox/components/ui'; +import CaptchaField from 'soapbox/features/auth_login/components/captcha'; +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.' }, + usernameUnavailable: { id: 'registration.username_unavailable', defaultMessage: 'Username is already taken.' }, + email: { id: 'registration.fields.email_placeholder', defaultMessage: 'E-Mail address' }, + password: { id: 'registration.fields.password_placeholder', defaultMessage: 'Password' }, + passwordMismatch: { id: 'registration.password_mismatch', defaultMessage: 'Passwords don\'t match.' }, + 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 = useCallback(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 ( +
+
+ + + + + + + + + + + + + {birthdayRequired && ( + + )} + + {needsApproval && ( + } + hintText={} + > +