Skip to content
Open
265 changes: 60 additions & 205 deletions packages/web-ui-registration/src/RegisterForm.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
/* eslint-disable complexity */
import {
FieldGroup,
TextInput,
Field,
FieldLabel,
FieldRow,
FieldError,
PasswordInput,
ButtonGroup,
Button,
TextAreaInput,
Callout,
} from '@rocket.chat/fuselage';
import { FieldGroup, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage';
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 { CustomFieldsForm } from '@rocket.chat/ui-client';
import { useAccountsCustomFields, useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useEffect, useId, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
Expand All @@ -23,6 +10,10 @@ import { Trans, useTranslation } from 'react-i18next';
import EmailConfirmationForm from './EmailConfirmationForm';
import type { DispatchLoginRouter } from './hooks/useLoginRouter';
import { useRegisterMethod } from './hooks/useRegisterMethod';
import { useRegisterFormValidation } from './hooks/useRegisterFormValidation';
import { useRegisterErrorHandler } from './hooks/useRegisterErrorHandler';
import { FormFieldInput } from './components/FormFieldInput';
import { PasswordFieldWithVerifier } from './components/PasswordFieldWithVerifier';

type LoginRegisterPayload = {
name: string;
Expand All @@ -36,10 +27,6 @@ type LoginRegisterPayload = {
export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRouter }): ReactElement => {
const { t } = useTranslation();

const requireNameForRegister = useSetting('Accounts_RequireNameForSignUp', true);
const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation', true);
const manuallyApproveNewUsersRequired = useSetting('Accounts_ManuallyApproveNewUsers', false);

const usernameOrEmailPlaceholder = useSetting('Accounts_EmailOrUsernamePlaceholder', '');
const passwordPlaceholder = useSetting('Accounts_PasswordPlaceholder', '');
const passwordConfirmationPlaceholder = useSetting('Accounts_ConfirmPasswordPlaceholder', '');
Expand All @@ -58,8 +45,6 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo

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

const dispatchToastMessage = useToastMessageDispatch();

const {
register,
handleSubmit,
Expand All @@ -71,8 +56,10 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo
formState: { errors },
} = useForm<LoginRegisterPayload>({ mode: 'onBlur' });

const { password } = watch();
const passwordIsValid = useValidatePassword(password);
const { validationRules, requireNameForRegister, requiresPasswordConfirmation, manuallyApproveNewUsersRequired, passwordIsValid } =
useRegisterFormValidation(watch);

const { handleRegisterError } = useRegisterErrorHandler(setError, setServerError, setLoginRoute);

const registerFormRef = useRef<HTMLElement>(null);

Expand All @@ -86,39 +73,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo
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 (/The username provided is not valid/.test(error.error)) {
setError('username', {
type: 'username-contains-invalid-chars',
message: t('registration.component.form.usernameContainsInvalidChars'),
});
}
if (/Name contains invalid characters/.test(error.error)) {
setError('name', { type: 'name-contains-invalid-chars', message: t('registration.component.form.nameContainsInvalidChars') });
}
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);
}
},
onError: handleRegisterError,
},
);
};
Expand All @@ -140,155 +95,55 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo
</Form.Header>
<Form.Container>
<FieldGroup>
<Field>
<FieldLabel required={requireNameForRegister} htmlFor={nameId}>
{t('registration.component.form.name')}
</FieldLabel>
<FieldRow>
<TextInput
{...register('name', {
required: requireNameForRegister ? t('Required_field', { field: t('registration.component.form.name') }) : 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 role='alert' 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('Required_field', { field: t('registration.component.form.email') }),
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 role='alert' 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('Required_field', { field: t('registration.component.form.username') }),
})}
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 role='alert' 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('Required_field', { field: t('registration.component.form.password') }),
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 role='alert' 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('Required_field', { field: t('registration.component.form.confirmPassword') }),
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 role='alert' id={`${passwordConfirmationId}-error`}>
{errors.passwordConfirmation.message}
</FieldError>
)}
</Field>
)}
<FormFieldInput
label={t('registration.component.form.name')}
fieldId={nameId}
required={requireNameForRegister}
error={errors.name}
placeholder={t('onboarding.form.adminInfoForm.fields.fullName.placeholder')}
register={register('name', validationRules.name)}
/>
<FormFieldInput
label={t('registration.component.form.email')}
fieldId={emailId}
required
error={errors.email}
placeholder={usernameOrEmailPlaceholder || t('registration.component.form.emailPlaceholder')}
register={register('email', validationRules.email)}
/>
<FormFieldInput
label={t('registration.component.form.username')}
fieldId={usernameId}
required
error={errors.username}
placeholder='jon.doe'
register={register('username', validationRules.username)}
/>
<PasswordFieldWithVerifier
passwordId={passwordId}
passwordVerifierId={passwordVerifierId}
passwordConfirmationId={passwordConfirmationId}
passwordError={errors.password}
passwordConfirmationError={errors.passwordConfirmation}
passwordRegister={register('password', validationRules.password)}
passwordConfirmationRegister={
requiresPasswordConfirmation ? register('passwordConfirmation', validationRules.passwordConfirmation) : undefined
}
password={watch('password')}
passwordIsValid={passwordIsValid}
requiresPasswordConfirmation={requiresPasswordConfirmation}
passwordPlaceholder={passwordPlaceholder}
passwordConfirmationPlaceholder={passwordConfirmationPlaceholder}
/>
{manuallyApproveNewUsersRequired && (
<Field>
<FieldLabel required htmlFor={reasonId}>
{t('registration.component.form.reasonToJoin')}
</FieldLabel>
<FieldRow>
<TextAreaInput
{...register('reason', {
required: t('Required_field', { field: t('registration.component.form.reasonToJoin') }),
})}
error={errors?.reason?.message}
aria-required='true'
aria-invalid={errors.reason ? 'true' : 'false'}
aria-describedby={`${reasonId}-error`}
id={reasonId}
/>
</FieldRow>
{errors.reason && (
<FieldError role='alert' id={`${reasonId}-error`}>
{errors.reason.message}
</FieldError>
)}
</Field>
<FormFieldInput
label={t('registration.component.form.reasonToJoin')}
fieldId={reasonId}
required
error={errors.reason}
register={register('reason', validationRules.reason)}
type='textarea'
/>
)}
<CustomFieldsForm formName='customFields' formControl={control} metadata={customFields} />
{serverError && <Callout type='danger'>{serverError}</Callout>}
Expand Down
49 changes: 49 additions & 0 deletions packages/web-ui-registration/src/components/FormFieldInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Field, FieldLabel, FieldRow, FieldError, TextInput, TextAreaInput } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
import type { FieldError as FieldErrorType, UseFormRegisterReturn } from 'react-hook-form';

type FormFieldInputProps = {
label: string;
fieldId: string;
required?: boolean;
error?: FieldErrorType;
placeholder?: string;
register: UseFormRegisterReturn;
type?: 'text' | 'textarea';
};

export const FormFieldInput = ({
label,
fieldId,
required = false,
error,
placeholder,
register,
type = 'text',
}: FormFieldInputProps): ReactElement => {
const InputComponent = type === 'textarea' ? TextAreaInput : TextInput;

return (
<Field>
<FieldLabel required={required} htmlFor={fieldId}>
{label}
</FieldLabel>
<FieldRow>
<InputComponent
{...register}
error={error?.message}
aria-required={required ? 'true' : 'false'}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={`${fieldId}-error`}
id={fieldId}
placeholder={placeholder}
/>
</FieldRow>
{error && (
<FieldError role='alert' id={`${fieldId}-error`}>
{error.message}
</FieldError>
)}
</Field>
);
};
Loading