Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 379 additions & 0 deletions packages/web-ui-registration/src/RegisterForm.spec.tsx

Large diffs are not rendered by default.

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
Loading