RocketChat/Rocket.Chat

View on GitHub
packages/web-ui-registration/src/RegisterForm.tsx

Summary

Maintainability
D
2 days
Test Coverage
/* eslint-disable complexity */
import {
    FieldGroup,
    TextInput,
    Field,
    FieldLabel,
    FieldRow,
    FieldError,
    PasswordInput,
    ButtonGroup,
    Button,
    TextAreaInput,
    Callout,
} from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { Form, ActionLink } from '@rocket.chat/layout';
import { CustomFieldsForm, PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client';
import { useAccountsCustomFields, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';

import EmailConfirmationForm from './EmailConfirmationForm';
import type { DispatchLoginRouter } from './hooks/useLoginRouter';
import { useRegisterMethod } from './hooks/useRegisterMethod';

type LoginRegisterPayload = {
    name: string;
    passwordConfirmation: string;
    username: string;
    password: string;
    email: string;
    reason: string;
};

export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRouter }): ReactElement => {
    const { t } = useTranslation();

    const requireNameForRegister = Boolean(useSetting('Accounts_RequireNameForSignUp'));
    const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation');
    const manuallyApproveNewUsersRequired = useSetting('Accounts_ManuallyApproveNewUsers');

    const usernameOrEmailPlaceholder = String(useSetting('Accounts_EmailOrUsernamePlaceholder'));
    const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder'));
    const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder'));

    const formLabelId = useUniqueId();
    const passwordVerifierId = useUniqueId();
    const nameId = useUniqueId();
    const emailId = useUniqueId();
    const usernameId = useUniqueId();
    const passwordId = useUniqueId();
    const passwordConfirmationId = useUniqueId();
    const reasonId = useUniqueId();

    const registerUser = useRegisterMethod();
    const customFields = useAccountsCustomFields();

    const [serverError, setServerError] = useState<string | undefined>(undefined);

    const dispatchToastMessage = useToastMessageDispatch();

    const {
        register,
        handleSubmit,
        setError,
        watch,
        getValues,
        clearErrors,
        control,
        formState: { errors },
    } = useForm<LoginRegisterPayload>({ mode: 'onBlur' });

    const { password } = watch();
    const passwordIsValid = useValidatePassword(password);

    const registerFormRef = useRef<HTMLElement>(null);

    useEffect(() => {
        if (registerFormRef.current) {
            registerFormRef.current.focus();
        }
    }, []);

    const handleRegister = async ({ password, passwordConfirmation: _, ...formData }: LoginRegisterPayload) => {
        registerUser.mutate(
            { pass: password, ...formData },
            {
                onError: (error: any) => {
                    if ([error.error, error.errorType].includes('error-invalid-email')) {
                        setError('email', { type: 'invalid-email', message: t('registration.component.form.invalidEmail') });
                    }
                    if (error.errorType === 'error-user-already-exists') {
                        setError('username', { type: 'user-already-exists', message: t('registration.component.form.usernameAlreadyExists') });
                    }

                    if (/Email already exists/.test(error.error)) {
                        setError('email', { type: 'email-already-exists', message: t('registration.component.form.emailAlreadyExists') });
                    }

                    if (/Username is already in use/.test(error.error)) {
                        setError('username', { type: 'username-already-exists', message: t('registration.component.form.userAlreadyExist') });
                    }
                    if (/error-too-many-requests/.test(error.error)) {
                        dispatchToastMessage({ type: 'error', message: error.error });
                    }
                    if (/error-user-is-not-activated/.test(error.error)) {
                        dispatchToastMessage({ type: 'info', message: t('registration.page.registration.waitActivationWarning') });
                        setLoginRoute('login');
                    }
                    if (error.error === 'error-user-registration-custom-field') {
                        setServerError(error.message);
                    }
                },
            },
        );
    };

    if (errors.email?.type === 'invalid-email') {
        return <EmailConfirmationForm onBackToLogin={() => clearErrors('email')} email={getValues('email')} />;
    }

    return (
        <Form
            tabIndex={-1}
            ref={registerFormRef}
            aria-labelledby={formLabelId}
            aria-describedby='welcomeTitle'
            onSubmit={handleSubmit(handleRegister)}
        >
            <Form.Header>
                <Form.Title id={formLabelId}>{t('registration.component.form.createAnAccount')}</Form.Title>
            </Form.Header>
            <Form.Container>
                <FieldGroup>
                    <Field>
                        <FieldLabel required={requireNameForRegister} htmlFor={nameId}>
                            {t('registration.component.form.name')}
                        </FieldLabel>
                        <FieldRow>
                            <TextInput
                                {...register('name', {
                                    required: requireNameForRegister ? t('registration.component.form.requiredField') : false,
                                })}
                                error={errors?.name?.message}
                                aria-required={requireNameForRegister}
                                aria-invalid={errors.name ? 'true' : 'false'}
                                placeholder={t('onboarding.form.adminInfoForm.fields.fullName.placeholder')}
                                aria-describedby={`${nameId}-error`}
                                id={nameId}
                            />
                        </FieldRow>
                        {errors.name && (
                            <FieldError aria-live='assertive' id={`${nameId}-error`}>
                                {errors.name.message}
                            </FieldError>
                        )}
                    </Field>
                    <Field>
                        <FieldLabel required htmlFor={emailId}>
                            {t('registration.component.form.email')}
                        </FieldLabel>
                        <FieldRow>
                            <TextInput
                                {...register('email', {
                                    required: t('registration.component.form.requiredField'),
                                    pattern: {
                                        value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                                        message: t('registration.component.form.invalidEmail'),
                                    },
                                })}
                                placeholder={usernameOrEmailPlaceholder || t('registration.component.form.emailPlaceholder')}
                                error={errors?.email?.message}
                                aria-required='true'
                                aria-invalid={errors.email ? 'true' : 'false'}
                                aria-describedby={`${emailId}-error`}
                                id={emailId}
                            />
                        </FieldRow>
                        {errors.email && (
                            <FieldError aria-live='assertive' id={`${emailId}-error`}>
                                {errors.email.message}
                            </FieldError>
                        )}
                    </Field>
                    <Field>
                        <FieldLabel required htmlFor={usernameId}>
                            {t('registration.component.form.username')}
                        </FieldLabel>
                        <FieldRow>
                            <TextInput
                                {...register('username', {
                                    required: t('registration.component.form.requiredField'),
                                })}
                                error={errors?.username?.message}
                                aria-required='true'
                                aria-invalid={errors.username ? 'true' : 'false'}
                                aria-describedby={`${usernameId}-error`}
                                id={usernameId}
                                placeholder='jon.doe'
                            />
                        </FieldRow>
                        {errors.username && (
                            <FieldError aria-live='assertive' id={`${usernameId}-error`}>
                                {errors.username.message}
                            </FieldError>
                        )}
                    </Field>
                    <Field>
                        <FieldLabel required htmlFor={passwordId}>
                            {t('registration.component.form.password')}
                        </FieldLabel>
                        <FieldRow>
                            <PasswordInput
                                {...register('password', {
                                    required: t('registration.component.form.requiredField'),
                                    validate: () => (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true),
                                })}
                                error={errors.password?.message}
                                aria-required='true'
                                aria-invalid={errors.password ? 'true' : undefined}
                                id={passwordId}
                                placeholder={passwordPlaceholder || t('Create_a_password')}
                                aria-describedby={`${passwordVerifierId} ${passwordId}-error`}
                            />
                        </FieldRow>
                        {errors?.password && (
                            <FieldError aria-live='assertive' id={`${passwordId}-error`}>
                                {errors.password.message}
                            </FieldError>
                        )}
                        <PasswordVerifier password={password} id={passwordVerifierId} />
                    </Field>
                    {requiresPasswordConfirmation && (
                        <Field>
                            <FieldLabel required htmlFor={passwordConfirmationId}>
                                {t('registration.component.form.confirmPassword')}
                            </FieldLabel>
                            <FieldRow>
                                <PasswordInput
                                    {...register('passwordConfirmation', {
                                        required: t('registration.component.form.requiredField'),
                                        deps: ['password'],
                                        validate: (val: string) => (watch('password') === val ? true : t('registration.component.form.invalidConfirmPass')),
                                    })}
                                    error={errors.passwordConfirmation?.message}
                                    aria-required='true'
                                    aria-invalid={errors.passwordConfirmation ? 'true' : 'false'}
                                    id={passwordConfirmationId}
                                    aria-describedby={`${passwordConfirmationId}-error`}
                                    placeholder={passwordConfirmationPlaceholder || t('Confirm_password')}
                                    disabled={!passwordIsValid}
                                />
                            </FieldRow>
                            {errors.passwordConfirmation && (
                                <FieldError aria-live='assertive' id={`${passwordConfirmationId}-error`}>
                                    {errors.passwordConfirmation.message}
                                </FieldError>
                            )}
                        </Field>
                    )}
                    {manuallyApproveNewUsersRequired && (
                        <Field>
                            <FieldLabel required htmlFor={reasonId}>
                                {t('registration.component.form.reasonToJoin')}
                            </FieldLabel>
                            <FieldRow>
                                <TextAreaInput
                                    {...register('reason', {
                                        required: t('registration.component.form.requiredField'),
                                    })}
                                    error={errors?.reason?.message}
                                    aria-required='true'
                                    aria-invalid={errors.reason ? 'true' : 'false'}
                                    aria-describedby={`${reasonId}-error`}
                                    id={reasonId}
                                />
                            </FieldRow>
                            {errors.reason && (
                                <FieldError aria-live='assertive' id={`${reasonId}-error`}>
                                    {errors.reason.message}
                                </FieldError>
                            )}
                        </Field>
                    )}
                    <CustomFieldsForm formName='customFields' formControl={control} metadata={customFields} />
                    {serverError && <Callout type='danger'>{serverError}</Callout>}
                </FieldGroup>
            </Form.Container>
            <Form.Footer>
                <ButtonGroup>
                    <Button type='submit' loading={registerUser.isLoading} primary>
                        {t('registration.component.form.joinYourTeam')}
                    </Button>
                </ButtonGroup>
                <ActionLink
                    onClick={(): void => {
                        setLoginRoute('login');
                    }}
                >
                    <Trans i18nKey='registration.page.register.back'>Back to Login</Trans>
                </ActionLink>
            </Form.Footer>
        </Form>
    );
};

export default RegisterForm;