Skip to content

Commit

Permalink
refactor(identification): enhance error handling (wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
JeffreyArt1 committed Aug 6, 2024
1 parent 35c201e commit 96f1dc2
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 95 deletions.
14 changes: 14 additions & 0 deletions src/app/[lang]/identification/adornment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CircularProgress } from '@mui/material';
import { useFormStatus } from 'react-dom';

export function LoadingAdornment() {
const status = useFormStatus();

if (!status.pending) return null;

return (
<div style={{ display: 'flex' }}>
<CircularProgress size={28} />
</div>
);
}
156 changes: 61 additions & 95 deletions src/app/[lang]/identification/form.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
'use client';

import ArrowCircleRightOutlinedIcon from '@mui/icons-material/ArrowCircleRightOutlined';
import { CircularProgress, TextField, Tooltip } from '@mui/material';
import { zodResolver } from '@hookform/resolvers/zod';
import { TextField, Tooltip } from '@mui/material';
import { useReCaptcha } from 'next-recaptcha-v3';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import * as Sentry from '@sentry/nextjs';
import { useState } from 'react';
import { useFormState } from 'react-dom';
import Link from 'next/link';
import React from 'react';
import { z } from 'zod';

import {
findCitizen,
findIamCitizen,
setCookie,
validateRecaptcha,
} from '@/actions';
import { GridContainer, GridItem } from '@/components/elements/grid';
import { createCedulaSchema } from '@/common/validation-schemas';
import { TextBodyTiny } from '@/components/elements/typography';
import { CustomTextMask } from '@/components/CustomTextMask';
import { useSnackAlert } from '@/components/elements/alert';
import { ButtonApp } from '@/components/elements/button';
import { Validations } from '@/common/helpers';
import { identifyAccount } from './identify.action';
import { SubmitButton } from './submit.button';
import { LoadingAdornment } from './adornment';
import theme from '@/components/themes/theme';
import { useLanguage } from '../provider';
import { LOGIN_URL } from '@/common';
Expand All @@ -32,127 +25,100 @@ type CedulaForm = z.infer<ReturnType<typeof createCedulaSchema>>;

export function Form() {
const { AlertError, AlertWarning } = useSnackAlert();
const [loading, setLoading] = useState(false);
const { executeRecaptcha } = useReCaptcha();
const router = useRouter();

const { executeRecaptcha, loaded } = useReCaptcha();
const { intl } = useLanguage();

const {
handleSubmit,
formState: { errors },
setValue,
watch,
} = useForm<CedulaForm>({
reValidateMode: 'onSubmit',
resolver: zodResolver(createCedulaSchema(intl)),
});

const cedulaFormValue = watch('cedula', '');
const { formState, setValue, register, trigger, clearErrors, setError } =
useForm<CedulaForm>({
reValidateMode: 'onChange',
resolver: zodResolver(createCedulaSchema(intl)),
});

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const valueWithoutHyphens = event.target.value.replace(/-/g, '');
setValue('cedula', valueWithoutHyphens);
};

const onSubmit = handleSubmit(async (data) => {
setLoading(true);

const cedula = data.cedula.replace(/-/g, '');
const isValidByLuhn = Validations.luhnCheck(cedula);
const [state, action] = useFormState(identifyAccount, {
message: '',
});

if (!isValidByLuhn) {
AlertError(intl.errors.cedula.invalid);
setLoading(false);
const onChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
const cedula = target.value.replace(/-/g, '');
setValue('cedula', cedula);

return;
if (formState.errors.cedula) {
clearErrors('cedula');
}

const reCaptchaToken = await executeRecaptcha('form_submit');

if (!reCaptchaToken) {
AlertWarning(intl.errors.recaptcha.issues);
setLoading(false);
if (cedula.length === 11) {
trigger('cedula');
}
};

return;
const [token, setToken] = React.useState('');
React.useEffect(() => {
if (loaded && !token) {
executeRecaptcha('form_submit')
.then(setToken)
.catch(() => '');
}
}, [token, loaded]);

try {
const { isHuman } = await validateRecaptcha(reCaptchaToken);

if (!isHuman) {
setLoading(false);
return AlertError(intl.errors.recaptcha.validation);
}

const { exists } = await findIamCitizen(cedula);

if (exists) {
setLoading(false);
return AlertError(intl.errors.cedula.exists);
}

const citizen = await findCitizen(cedula);
await setCookie('citizen', citizen);
router.push('liveness');
setLoading(false);
} catch (err: any) {
Sentry.captureMessage(err.message || err, 'error');
setLoading(false);
return AlertError(intl.errors.cedula.invalid);
React.useEffect(() => {
if (state.message) {
AlertError(state.message);
}
});
}, [state]);

return (
<form onSubmit={onSubmit}>
<form
action={action}
onSubmit={async (e) => {
await executeRecaptcha('form_submit').then(setToken);

if (!formState.isValid) {
e.preventDefault();
trigger();
return false;
}

e.currentTarget?.requestSubmit();
}}
>
<input type="hidden" name="token" value={token} />
<input type="hidden" {...register('cedula')} />

<GridContainer>
<GridItem lg={12} md={12}>
<Tooltip title={intl.step1.cedulaTooltip}>
<TextField
required
value={cedulaFormValue}
onChange={onChange}
label={intl.step1.cedula}
placeholder="***-**00000-0"
autoComplete="off"
error={Boolean(errors.cedula)}
helperText={errors?.cedula?.message}
error={Boolean(formState.errors.cedula)}
helperText={formState.errors?.cedula?.message}
inputProps={{
inputMode: 'numeric',
}}
InputProps={{
inputComponent: CustomTextMask,
endAdornment: loading ? (
<div style={{ display: 'flex' }}>
<CircularProgress size={28} />
</div>
) : null,
endAdornment: <LoadingAdornment />,
}}
fullWidth
/>
</Tooltip>
</GridItem>

<GridItem lg={12} md={12}>
<br />
<ButtonApp
submit
endIcon={<ArrowCircleRightOutlinedIcon />}
disabled={loading}
>
{intl.actions.confirm}
</ButtonApp>
<GridItem lg={12} md={12} sx={{ my: 3 }}>
<SubmitButton />
</GridItem>
</GridContainer>

<br />
<GridContainer>
<GridItem md={12} lg={12}>
<TextBodyTiny textCenter>
<Link href={LOGIN_URL} style={{ textDecoration: 'none' }}>
<span style={{ color: theme.palette.primary.main }}>
{intl.alreadyRegistered}
</span>{' '}
<span style={{ color: theme.palette.primary.main }}>
{intl.alreadyRegistered}
</span>{' '}
<Link href={LOGIN_URL}>
<span
style={{
color: theme.palette.info.main,
Expand Down
42 changes: 42 additions & 0 deletions src/app/[lang]/identification/identify.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use server';

import { redirect } from 'next/navigation';

import {
findCitizen,
findIamCitizen,
setCookie,
validateRecaptcha,
} from '@/actions';

type State = { message: string };

export async function identifyAccount(prev: State, form: FormData) {
const cedula = form.get('cedula') as string;
const token = form.get('token') as string;

if (!token) {
return { message: 'intl.errors.recaptcha.issues' };
}

if (!cedula) {
return { message: 'No cédula' };
}

const { isHuman } = await validateRecaptcha(token);

if (!isHuman) {
return { message: 'intl.errors.recaptcha.validation' };
}

const { exists } = await findIamCitizen(cedula);

if (exists) {
return { message: 'intl.errors.cedula.exists' };
}

const citizen = await findCitizen(cedula);
await setCookie('citizen', citizen);

redirect('liveness');
}
20 changes: 20 additions & 0 deletions src/app/[lang]/identification/submit.button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ArrowCircleRightOutlined from '@mui/icons-material/ArrowCircleRightOutlined';
import { useFormStatus } from 'react-dom';

import { ButtonApp } from '@/components/elements/button';
import { useLanguage } from '../provider';

export function SubmitButton() {
const status = useFormStatus();
const { intl } = useLanguage();

return (
<ButtonApp
submit
endIcon={<ArrowCircleRightOutlined />}
disabled={status.pending}
>
{intl.actions.confirm}
</ButtonApp>
);
}

0 comments on commit 96f1dc2

Please sign in to comment.