Skip to content

Commit

Permalink
wip: Change email flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-dalmet committed Oct 10, 2023
1 parent 6e78ccf commit 34b2bbe
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 135 deletions.
7 changes: 7 additions & 0 deletions src/app/(authenticated)/account/email/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import PageEmail from '@/features/account/PageEmail';

export default function Page() {
return <PageEmail />;
}
10 changes: 9 additions & 1 deletion src/features/account/AccountNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { LuUser } from 'react-icons/lu';
import { LuMail, LuUser } from 'react-icons/lu';

import { Nav, NavGroup, NavItem } from '@/components/Nav';

Expand All @@ -22,6 +22,14 @@ export const AccountNav = () => {
>
{t('account:nav.profile')}
</NavItem>
<NavItem
as={Link}
href="/account/email"
isActive={isActive('/account/email')}
icon={LuMail}
>
{t('account:nav.email')}
</NavItem>
</NavGroup>
</Nav>
);
Expand Down
99 changes: 99 additions & 0 deletions src/features/account/PageEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';

import { Button, Card, CardBody, Flex, Heading, Stack } from '@chakra-ui/react';
import { Formiz, useForm } from '@formiz/core';
import { isEmail } from '@formiz/validations';
import { useTranslation } from 'react-i18next';

import { ErrorPage } from '@/components/ErrorPage';
import { FieldInput } from '@/components/FieldInput';
import { Page, PageContent } from '@/components/Page';
import { useToastError, useToastSuccess } from '@/components/Toast';
import { AccountNav } from '@/features/account/AccountNav';
import { Loader } from '@/layout/Loader';
import { trpc } from '@/lib/trpc/client';

export default function PageEmail() {
const { t } = useTranslation(['common', 'account']);
const trpcContext = trpc.useContext();
const account = trpc.account.get.useQuery(undefined, {
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});

const toastSuccess = useToastSuccess();
const toastError = useToastError();

const updateEmail = trpc.account.updateEmail.useMutation({
onSuccess: async () => {
await trpcContext.account.invalidate();
toastSuccess({
title: t('account:email.feedbacks.updateSuccess.title'),
});
},
onError: () => {
toastError({
title: t('account:email.feedbacks.updateError.title'),
});
},
});

const profileForm = useForm<{
email: string;
}>({
initialValues: {
email: account.data?.email ?? undefined,
},
onValidSubmit: (values) => {
updateEmail.mutate(values);
},
});

return (
<Page nav={<AccountNav />}>
<PageContent>
<Heading size="md" mb="4">
{t('account:email.title')}
</Heading>

<Card minH="11rem">
{account.isLoading && <Loader />}
{account.isError && <ErrorPage />}
{account.isSuccess && (
<CardBody>
<Stack spacing={4}>
<Formiz connect={profileForm}>
<form noValidate onSubmit={profileForm.submit}>
<Stack spacing="6">
<FieldInput
name="email"
label={t('account:data.email.label')}
required={t('account:data.email.required')}
validations={[
{
handler: isEmail(),
message: t('account:data.email.invalid'),
},
]}
/>
<Flex>
<Button
type="submit"
variant="@primary"
ms="auto"
isLoading={updateEmail.isLoading}
>
{t('account:email.actions.update')}
</Button>
</Flex>
</Stack>
</form>
</Formiz>
</Stack>
</CardBody>
)}
</Card>
</PageContent>
</Page>
);
}
27 changes: 4 additions & 23 deletions src/features/account/PageProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';

import { Button, Card, CardBody, Flex, Heading, Stack } from '@chakra-ui/react';
import { Formiz, useForm } from '@formiz/core';
import { isEmail } from '@formiz/validations';
import { useTranslation } from 'react-i18next';

import { ErrorPage } from '@/components/ErrorPage';
Expand Down Expand Up @@ -37,13 +36,7 @@ export default function PageProfile() {
title: t('account:profile.feedbacks.updateSuccess.title'),
});
},
onError: (error) => {
if (isErrorDatabaseConflict(error, 'email')) {
profileForm.setErrors({
email: t('account:data.email.alreadyUsed'),
});
return;
}
onError: () => {
toastError({
title: t('account:profile.feedbacks.updateError.title'),
});
Expand All @@ -52,12 +45,11 @@ export default function PageProfile() {

const profileForm = useForm<{
name: string;
email: string;
language: string;
}>({
initialValues: {
name: account.data?.name ?? undefined,
email: account.data?.email ?? undefined,

language: account.data?.language ?? undefined,
},
onValidSubmit: (values) => {
Expand All @@ -72,7 +64,7 @@ export default function PageProfile() {
{t('account:profile.title')}
</Heading>

<Card minH="22rem">
<Card minH="16rem">
{account.isLoading && <Loader />}
{account.isError && <ErrorPage />}
{account.isSuccess && (
Expand All @@ -86,17 +78,6 @@ export default function PageProfile() {
label={t('account:data.name.label')}
required={t('account:data.name.required')}
/>
<FieldInput
name="email"
label={t('account:data.email.label')}
required={t('account:data.email.required')}
validations={[
{
handler: isEmail(),
message: t('account:data.email.invalid'),
},
]}
/>
<FieldSelect
name="language"
label={t('account:data.language.label')}
Expand All @@ -113,7 +94,7 @@ export default function PageProfile() {
ms="auto"
isLoading={updateAccount.isLoading}
>
{t('account:profile.actions.save')}
{t('account:profile.actions.update')}
</Button>
</Flex>
</Stack>
Expand Down
102 changes: 11 additions & 91 deletions src/features/auth/PageLoginValidate.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,25 @@
import React from 'react';

import {
Box,
Button,
Card,
CardBody,
CardHeader,
HStack,
Heading,
Stack,
Text,
chakra,
} from '@chakra-ui/react';
import { Box, Button, Card, CardBody, CardHeader } from '@chakra-ui/react';
import { Formiz, useForm } from '@formiz/core';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { LuArrowLeft, LuArrowRight } from 'react-icons/lu';

import { FieldPinInput } from '@/components/FieldPinInput';
import { Logo } from '@/components/Logo';
import { SlideIn } from '@/components/SlideIn';
import {
VALIDATION_TOKEN_EXPIRATION_IN_MINUTES,
getRetryDelayInSeconds,
} from '@/features/auth/utils';
import { DevCodeHint } from '@/features/dev/DevCodeHint';
VerificationCodeForm,
useOnVerificationCodeError,
} from '@/features/auth/VerificationCodeForm';
import { useRtl } from '@/hooks/useRtl';
import { useSearchParamsUpdater } from '@/hooks/useSearchParamsUpdater';
import { trpc } from '@/lib/trpc/client';

export default function PageLoginValidate() {
const { rtlValue } = useRtl();
const { t } = useTranslation(['auth']);
const router = useRouter();
const params = useParams();
const trpcContext = trpc.useContext();
const searchParams = useSearchParams();
const searchParamsUpdater = useSearchParamsUpdater();

const token = params?.token?.toString() ?? '';
const email = searchParams.get('email');
Expand All @@ -45,6 +28,8 @@ export default function PageLoginValidate() {
onValidSubmit: (values) => validate.mutate({ ...values, token }),
});

const OnVerificationCodeError = useOnVerificationCodeError({ form });

const validate = trpc.auth.loginValidate.useMutation({
onSuccess: () => {
// Optimistic Update
Expand All @@ -53,36 +38,7 @@ export default function PageLoginValidate() {
// TODO setup redirect logic (redirect url params)
router.push('/dashboard');
},
onError: async (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
const retries = parseInt(searchParams.get('retries') ?? '0', 10);
const seconds = getRetryDelayInSeconds(retries);

searchParamsUpdater(
{
retries: (retries + 1).toString(),
},
{ replace: true }
);

await new Promise((r) => {
setTimeout(r, seconds * 1_000);
});

form.setErrors({
code: `Code is invalid`,
});

return;
}

if (error.data?.code === 'BAD_REQUEST') {
form.setErrors({ code: 'Code should be 6 digits' });
return;
}

form.setErrors({ code: 'Unkown error' });
},
onError: OnVerificationCodeError,
});

return (
Expand All @@ -102,46 +58,10 @@ export default function PageLoginValidate() {
</CardHeader>
<CardBody>
<Formiz connect={form} autoForm>
<Stack spacing="4">
<Stack>
<Heading size="md" data-test="login-page-heading">
{t('auth:login.code.title')}
</Heading>
<Text fontSize="sm">
We&apos;ve sent a 6-character code to{' '}
<chakra.strong>{email}</chakra.strong>. The code expires
shortly ({VALIDATION_TOKEN_EXPIRATION_IN_MINUTES} minutes),
so please enter it soon.
</Text>
</Stack>
<FieldPinInput
name="code"
label="Verification code"
helper="Can't find the code? Check your spams."
autoFocus
isDisabled={validate.isLoading}
onComplete={() => {
// Only auto submit on first try
if (!form.isSubmitted) {
form.submit();
}
}}
/>
<HStack spacing={8}>
<Button
size="lg"
isLoading={validate.isLoading}
isDisabled={form.isSubmitted && !form.isValid}
type="submit"
variant="@primary"
flex={1}
>
Confirm
</Button>
</HStack>

<DevCodeHint />
</Stack>
<VerificationCodeForm
email={email ?? ''}
isLoading={validate.isLoading}
/>
</Formiz>
</CardBody>
</Card>
Expand Down
Loading

0 comments on commit 34b2bbe

Please sign in to comment.