Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HUM-93 - refactor email verification pages #3009

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { PageCard, PageCardLoader } from '@/shared/components/ui/page-card';
import { getErrorMessageForError } from '@/shared/errors';
import { useAuth } from '@/modules/auth/hooks/use-auth';
import { routerPaths } from '@/router/router-paths';
import {
TopNotificationType,
useNotification,
} from '@/shared/hooks/use-notification';
import { useResendEmailRouterParams, useResendEmail } from '../hooks';
import { ResendVerificationEmailForm } from './resend-verification-email-form';

export function EmailVerificationFormContainer() {
const { user, signOut } = useAuth();
const navigate = useNavigate();
const routerState = useResendEmailRouterParams();
const { methods, handleResend, isError, error, isSuccess } = useResendEmail(
routerState?.email ?? ''
);
const { showNotification } = useNotification();
const { t } = useTranslation();
const isAuthenticated = Boolean(user);

useEffect(() => {
if (isError) {
showNotification({
message: getErrorMessageForError(error),
type: TopNotificationType.WARNING,
});
}
}, [isError, error, showNotification]);

useEffect(() => {
if (isSuccess && methods.formState.isSubmitSuccessful) {
showNotification({
message: t('worker.sendResetLinkSuccess.successResent'),
type: TopNotificationType.SUCCESS,
});
}
}, [isSuccess, methods.formState.isSubmitSuccessful, showNotification, t]);

const handleCancel = () => {
signOut();
navigate(routerPaths.homePage);
};

if (!routerState?.email) {
return <PageCardLoader />;
}

return (
<PageCard cancelNavigation={handleCancel} title="Verify Email">
<ResendVerificationEmailForm
methods={methods}
handleResend={handleResend}
email={routerState.email}
isAuthenticated={isAuthenticated}
/>
</PageCard>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
PageCardError,
PageCardLoader,
} from '@/shared/components/ui/page-card';
import { getErrorMessageForError } from '@/shared/errors';
import { useVerifyEmailQuery } from '../hooks';
import { EmailVerificationSuccessMessage } from './email-verification-success-message';

interface EmailVerificationProcessProps {
token: string;
}

function useEmailVerification(token: string) {
const { error, isError, isPending } = useVerifyEmailQuery({ token });

return {
error,
isError,
isPending,
};
}

export function EmailVerificationProcess({
token,
}: Readonly<EmailVerificationProcessProps>) {
const { error, isError, isPending } = useEmailVerification(token);

if (isError) {
return <PageCardError errorMessage={getErrorMessageForError(error)} />;
}

if (isPending) {
return <PageCardLoader />;
}

return <EmailVerificationSuccessMessage />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { t } from 'i18next';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Link } from 'react-router-dom';
import { Button } from '@/shared/components/ui/button';
import { PageCard } from '@/shared/components/ui/page-card';
import { SuccessLabel } from '@/shared/components/ui/success-label';
import { routerPaths } from '@/router/router-paths';

export function EmailVerificationSuccessMessage() {
return (
<PageCard
showBackButton={false}
showCancelButton={false}
title={<SuccessLabel>{t('worker.emailVerification.title')}</SuccessLabel>}
>
<Grid container gap="2rem">
<Typography variant="body1">
{t('worker.emailVerification.description')}
</Typography>
<Button
component={Link}
fullWidth
to={routerPaths.worker.signIn}
type="submit"
variant="contained"
>
{t('worker.emailVerification.btn')}
</Button>
</Grid>
</PageCard>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './email-verification-form-container';
export * from './email-verification-process';
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { FormProvider } from 'react-hook-form';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { Trans, useTranslation } from 'react-i18next';
import type { UseFormReturn } from 'react-hook-form';
import { Button } from '@/shared/components/ui/button';
import type { ResendEmailVerificationDto } from '@/modules/worker/services/resend-email-verification';
import { HCaptchaForm } from '@/shared/components/hcaptcha/h-captcha-form';
import { MailTo } from '@/shared/components/ui/mail-to';
import { env } from '@/shared/env';

interface ResendVerificationEmailFormProps {
methods: UseFormReturn<Pick<ResendEmailVerificationDto, 'h_captcha_token'>>;
handleResend: (
data: Pick<ResendEmailVerificationDto, 'h_captcha_token'>
) => void;
email: string;
isAuthenticated: boolean;
}

export function ResendVerificationEmailForm({
methods,
handleResend,
email,
isAuthenticated,
}: Readonly<ResendVerificationEmailFormProps>) {
const { t } = useTranslation();

return (
<FormProvider {...methods}>
<form
onSubmit={(event) => {
void methods.handleSubmit(handleResend)(event);
}}
>
<Grid container gap="2rem" sx={{ paddingTop: '1rem' }}>
<Typography>
<Trans
components={{
1: <Typography component="span" fontWeight={600} />,
}}
i18nKey="worker.verifyEmail.paragraph1"
values={{ email }}
/>
</Typography>
<Typography variant="body1">
{t('worker.verifyEmail.paragraph2')}
</Typography>
<Typography variant="body1">
<Trans
components={{
1: <Typography component="span" fontWeight={600} />,
}}
i18nKey="worker.verifyEmail.paragraph3"
/>
</Typography>
<Typography variant="body1">
<Trans
components={{
1: (
<Typography
component="span"
fontWeight={600}
variant="body1"
/>
),
2: <MailTo mail={env.VITE_HUMAN_SUPPORT_EMAIL} />,
}}
i18nKey="worker.verifyEmail.paragraph4"
/>
</Typography>
{isAuthenticated && (
<>
<HCaptchaForm name="h_captcha_token" />
<Button fullWidth type="submit" variant="outlined">
{t('worker.verifyEmail.btn')}
</Button>
</>
)}
</Grid>
</form>
</FormProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './use-email-verification-query';
export * from './use-email-verification-token';
export * from './use-resend-email';
export * from './use-resend-email-router-params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from 'zod';
import { useLocationState } from '@/modules/worker/hooks/use-location-state';

export const tokenSchema = z.string().transform((value, ctx) => {
const token = value.split('=')[1];
if (!token) {
ctx.addIssue({
fatal: true,
code: z.ZodIssueCode.custom,
message: 'error',
});
}
return token;
});

export function useEmailVerificationToken() {
const { field: token } = useLocationState({
schema: tokenSchema,
locationStorage: 'search',
});

return {
token,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod';
import { useLocationState } from '@/modules/worker/hooks/use-location-state';

export const routerStateSchema = z.object({
email: z.string().email(),
resendOnMount: z.boolean().optional(),
});

export function useResendEmailRouterParams() {
const { field: routerState } = useLocationState({
keyInStorage: 'routerState',
schema: routerStateSchema,
});

return routerState;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable camelcase -- ...*/
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
useResendEmailVerificationWorkerMutation,
resendEmailVerificationHcaptchaSchema,
} from '@/modules/worker/services/resend-email-verification';
import { useResetMutationErrors } from '@/shared/hooks/use-reset-mutation-errors';
import type { ResendEmailVerificationDto } from '@/modules/worker/services/resend-email-verification';

export function useResendEmail(email: string) {
const {
isError,
error,
isSuccess,
mutate: resendEmailVerificationMutation,
reset: resendEmailVerificationMutationReset,
} = useResendEmailVerificationWorkerMutation();
const methods = useForm<Pick<ResendEmailVerificationDto, 'h_captcha_token'>>({
defaultValues: {
h_captcha_token: '',
},
resolver: zodResolver(resendEmailVerificationHcaptchaSchema),
});

useResetMutationErrors(methods.watch, resendEmailVerificationMutationReset);

const handleResend = (
data: Pick<ResendEmailVerificationDto, 'h_captcha_token'>
) => {
if (!email) {
return;
}

resendEmailVerificationMutation({
email,
h_captcha_token: data.h_captcha_token,
});
};

return {
isError,
isSuccess,
error,
methods,
handleResend,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './pages/worker-email-verification-process.page';
export * from './pages/worker-verify-email.page';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './worker-email-verification-process.page';
export * from './worker-verify-email.page';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useTranslation } from 'react-i18next';
import { PageCardError } from '@/shared/components/ui/page-card';
import { EmailVerificationProcess } from '../components';
import { useEmailVerificationToken } from '../hooks';

export function WorkerEmailVerificationProcessPage() {
const { t } = useTranslation();
const { token } = useEmailVerificationToken();

if (!token) {
return (
<PageCardError
errorMessage={t('worker.emailVerification.errors.noToken')}
/>
);
}

return <EmailVerificationProcess token={token} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { EmailVerificationFormContainer } from '../components';

export function WorkerVerifyEmailPage() {
return <EmailVerificationFormContainer />;
}
Loading