From 96f1dc26eab0c26672cb9ca7dddbe5713ca6e87e Mon Sep 17 00:00:00 2001 From: Jeffrey Mesa Date: Tue, 6 Aug 2024 18:27:19 -0400 Subject: [PATCH] refactor(identification): enhance error handling (wip) --- src/app/[lang]/identification/adornment.tsx | 14 ++ src/app/[lang]/identification/form.tsx | 156 +++++++----------- .../[lang]/identification/identify.action.ts | 42 +++++ .../[lang]/identification/submit.button.tsx | 20 +++ 4 files changed, 137 insertions(+), 95 deletions(-) create mode 100644 src/app/[lang]/identification/adornment.tsx create mode 100644 src/app/[lang]/identification/identify.action.ts create mode 100644 src/app/[lang]/identification/submit.button.tsx diff --git a/src/app/[lang]/identification/adornment.tsx b/src/app/[lang]/identification/adornment.tsx new file mode 100644 index 0000000..067e88c --- /dev/null +++ b/src/app/[lang]/identification/adornment.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/src/app/[lang]/identification/form.tsx b/src/app/[lang]/identification/form.tsx index 5fd6775..a746cea 100644 --- a/src/app/[lang]/identification/form.tsx +++ b/src/app/[lang]/identification/form.tsx @@ -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'; @@ -32,127 +25,100 @@ type CedulaForm = z.infer>; 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({ - reValidateMode: 'onSubmit', - resolver: zodResolver(createCedulaSchema(intl)), - }); - - const cedulaFormValue = watch('cedula', ''); + const { formState, setValue, register, trigger, clearErrors, setError } = + useForm({ + reValidateMode: 'onChange', + resolver: zodResolver(createCedulaSchema(intl)), + }); - const onChange = (event: React.ChangeEvent) => { - 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) => { + 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 ( -
+ { + await executeRecaptcha('form_submit').then(setToken); + + if (!formState.isValid) { + e.preventDefault(); + trigger(); + return false; + } + + e.currentTarget?.requestSubmit(); + }} + > + + + - - - ) : null, + endAdornment: , }} fullWidth /> - -
- } - disabled={loading} - > - {intl.actions.confirm} - + +
-
- - - {intl.alreadyRegistered} - {' '} + + {intl.alreadyRegistered} + {' '} + } + disabled={status.pending} + > + {intl.actions.confirm} + + ); +}