From 79f5b52f9779a89dd609e2a91f48626b8ef1cc4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Eduardo=20=C3=81lvarez=20Lerebours?= <45927529+JE1999@users.noreply.github.com> Date: Thu, 22 Jun 2023 09:44:34 -0400 Subject: [PATCH] refactor: improve steps UI/UX, fix Luhn algorithm (#36) * [REF] MUI button style * [ADD] disabled button if is pwned * library update * [REF] footer and themes * [REF] link to home * [ADD] cedula validation if the user exists * [ADD] link to ogtic * [ADD] link to log in * [REF] background textfield * [ADD] color FaceLivenessDetector * [REF] stepper ui * chore: lint all files, removed unused code, fix Luhn * lint: run prettier * fix(api): do not return Surnameto the frontend * imp(ui): remove filled variant from fields * fix(liveness): re-rendering error --------- Co-authored-by: Gustavo Valverde --- package.json | 5 +- .../biometric/face-liveness-detector.tsx | 40 +++++- src/components/elements/alert/index.tsx | 2 +- .../elements/boxContentCenter/index.tsx | 14 +- src/components/elements/button/index.tsx | 40 +++--- src/components/elements/grid/index.tsx | 10 +- .../elements/passwordLevel/index.tsx | 122 ++++++++++++----- src/components/layout/footer.tsx | 3 +- src/components/layout/navBar.tsx | 2 +- src/pages/api/iam/[cedula].ts | 29 ++++ src/pages/api/types/index.ts | 6 + src/pages/register/stepper/index.tsx | 9 +- src/pages/register/stepper/step1.tsx | 129 ++++++++++++++---- src/pages/register/stepper/step2.tsx | 35 ++--- src/pages/register/stepper/step3.tsx | 100 ++++++++++---- src/styles/globals.css | 8 +- src/themes/index.tsx | 11 +- yarn.lock.REMOVED.git-id | 2 +- 18 files changed, 404 insertions(+), 163 deletions(-) create mode 100644 src/pages/api/iam/[cedula].ts diff --git a/package.json b/package.json index c5d3f238..f81c0f04 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "@aws-amplify/ui-react-liveness": "^1.0.1", "@aws-sdk/client-rekognition": "^3.354.0", "@babel/core": "*", - "@emotion/react": "^11.10.6", + "@emotion/react": "^11.11.1", "@emotion/styled": "^11.10.6", "@hookform/resolvers": "2.9.7", "@mui/icons-material": "^5.11.11", - "@mui/material": "^5.11.12", + "@mui/material": "^5.13.5", "aws-amplify": "^5.2.7", "axios": "^1.4.0", "check-password-strength": "^2.0.7", @@ -32,6 +32,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.34.0", + "react-imask": "^7.0.0", "typescript": "4.9.5", "yup": "^1.2.0" }, diff --git a/src/components/biometric/face-liveness-detector.tsx b/src/components/biometric/face-liveness-detector.tsx index 9c6bc8af..8e43ef16 100644 --- a/src/components/biometric/face-liveness-detector.tsx +++ b/src/components/biometric/face-liveness-detector.tsx @@ -1,5 +1,6 @@ import { FaceLivenessDetector } from '@aws-amplify/ui-react-liveness'; -import { Loader, ThemeProvider } from '@aws-amplify/ui-react'; +import { Loader, Theme, ThemeProvider, useTheme } from '@aws-amplify/ui-react'; + import React from 'react'; import { useState, useEffect } from 'react'; @@ -7,6 +8,8 @@ import { useSnackbar } from '@/components/elements/alert'; import { defaultLivenessDisplayText } from './displayText'; export function LivenessQuickStartReact({ handleNextForm, cedula }: any) { + // const { tokens } = useTheme(); + const next = handleNextForm; const id = cedula; const [loading, setLoading] = useState(true); @@ -53,12 +56,43 @@ export function LivenessQuickStartReact({ handleNextForm, cedula }: any) { if (error) { AlertError('No se ha podido validar correctamente la identidad.'); } - }, [error, AlertError]); + // TODO: AlertError is causing re-rendering issues. But not adding it causes eslint error. + }, [error]); + + const theme: Theme = { + name: 'Face Liveness Theme', + tokens: { + colors: { + background: { + primary: { + // value: tokens.colors.neutral['90'].value, + value: '#fff', + }, + secondary: { + // value: tokens.colors.neutral['100'].value, + value: '#000', + }, + }, + font: { + primary: { + // value: tokens.colors.white.value, + value: '#000', + }, + }, + brand: { + primary: { + '80': '#003876', + '90': '#003876', + }, + }, + }, + }, + }; return ( <>
- + {loading ? ( ) : ( diff --git a/src/components/elements/alert/index.tsx b/src/components/elements/alert/index.tsx index 8ce945ee..c790ba37 100644 --- a/src/components/elements/alert/index.tsx +++ b/src/components/elements/alert/index.tsx @@ -36,7 +36,7 @@ export const SnackbarProvider = ({ children }: SnackbarProviderProps) => { - + {children} diff --git a/src/components/elements/button/index.tsx b/src/components/elements/button/index.tsx index e1ec0770..fc77e99e 100644 --- a/src/components/elements/button/index.tsx +++ b/src/components/elements/button/index.tsx @@ -1,4 +1,4 @@ -import Button from "@mui/material/Button"; +import Button from '@mui/material/Button'; interface IButtonTextApp { onClick?: any; @@ -11,7 +11,7 @@ export const ButtonTextApp = ({ onClick, children }: IButtonTextApp) => { onClick={onClick} variant="text" size="small" - sx={{ textTransform: "inherit" }} + sx={{ textTransform: 'inherit' }} > {children} @@ -26,28 +26,30 @@ interface IButtonApp { notFullWidth?: boolean; children: React.ReactNode; startIcon?: any; - size?: "small" | "medium" | "large" | undefined; + endIcon?: any; + size?: 'small' | 'medium' | 'large' | undefined; color?: - | "inherit" - | "primary" - | "secondary" - | "success" - | "error" - | "info" - | "warning"; - variant?: "text" | "outlined" | "contained"; + | 'inherit' + | 'primary' + | 'secondary' + | 'success' + | 'error' + | 'info' + | 'warning'; + variant?: 'text' | 'outlined' | 'contained'; } export const ButtonApp = ({ outlined, disabled, - variant = "contained", + variant = 'contained', submit, onClick, notFullWidth, children, - size = "medium", + size = 'medium', startIcon = null, + endIcon = null, color, }: IButtonApp) => { return ( @@ -55,18 +57,12 @@ export const ButtonApp = ({ size={size} variant={variant} disabled={disabled} - type={submit ? "submit" : "button"} + type={submit ? 'submit' : 'button'} onClick={onClick} - color={color ? color : "primary"} + color={color ? color : 'primary'} fullWidth={notFullWidth ? false : true} - sx={{ - paddingX: `${notFullWidth ? "35px" : "auto"}`, - fontWeight: `${outlined ? "bold" : "normal"}`, - borderRadius: "50px", - padding: "10px 0px", - height: "60px", - }} startIcon={startIcon} + endIcon={endIcon} > {children} diff --git a/src/components/elements/grid/index.tsx b/src/components/elements/grid/index.tsx index 724c94e8..8076a13a 100644 --- a/src/components/elements/grid/index.tsx +++ b/src/components/elements/grid/index.tsx @@ -1,5 +1,5 @@ -import Grid from "@mui/material/Grid"; -import React from "react"; +import Grid from '@mui/material/Grid'; +import React from 'react'; interface IPropsContainer { children: React.ReactNode; @@ -18,10 +18,10 @@ export const GridContainer = ({ }: IPropsContainer) => ( {children} diff --git a/src/components/elements/passwordLevel/index.tsx b/src/components/elements/passwordLevel/index.tsx index 56c08617..6f3f8ece 100644 --- a/src/components/elements/passwordLevel/index.tsx +++ b/src/components/elements/passwordLevel/index.tsx @@ -1,15 +1,77 @@ import * as React from 'react'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; + import { Typography } from '@mui/material'; +// TODO: Refactor this with a simpler approach export default function PasswordLevel({ passwordLevel }: any) { return passwordLevel.length > 0 ? ( - <> +
+
+ + + Una letra minúscula + +
+
+ + + Una letra mayúscula + +
+
+ + + Un número + +
+
+ + + Un carácter especial + +
+
+ = 10 ? 'success' : 'disabled'} + style={{ fontSize: '20px', marginBottom: '-4px', marginRight: '3px' }} + /> + + 10 caracteres como mínimo + +
+
{passwordLevel.id >= 0 && ( @@ -22,36 +84,30 @@ export default function PasswordLevel({ passwordLevel }: any) { }} /> )} - {passwordLevel.id >= 1 && ( -
- )} - {passwordLevel.id >= 2 && ( -
- )} - {passwordLevel.id >= 3 && ( -
- )} +
= 1 ? '#E0D256' : '#f1f1f1'}`, + borderRadius: '10px', + }} + /> +
= 2 ? '#B4E056' : '#f1f1f1'}`, + borderRadius: '10px', + }} + /> +
= 3 ? '#A3E056' : '#f1f1f1'}`, + borderRadius: '10px', + }} + />
- +
) : null; } diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 930c7767..0ca154f5 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -101,11 +101,12 @@ export default function Index() { Desarrollado por logo ogtic window.open("https://ogtic.gob.do/")} />
diff --git a/src/components/layout/navBar.tsx b/src/components/layout/navBar.tsx index 9e8076ed..ee801109 100644 --- a/src/components/layout/navBar.tsx +++ b/src/components/layout/navBar.tsx @@ -23,7 +23,7 @@ export default function Index() {
logo diff --git a/src/pages/api/iam/[cedula].ts b/src/pages/api/iam/[cedula].ts new file mode 100644 index 00000000..2f9063d2 --- /dev/null +++ b/src/pages/api/iam/[cedula].ts @@ -0,0 +1,29 @@ +import { NextApiRequest, NextApiResponse } from 'next/types'; +import axios from 'axios'; + +import { VerifyIamUserNameResponse } from '../types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<{ exists: boolean } | void> +): Promise { + const { token } = req.cookies; + + if (token !== process.env.NEXT_PUBLIC_COOKIE_KEY) { + return res.status(401).send(); + } + + const http = axios.create({ + baseURL: process.env.NEXT_PUBLIC_IAM_API, + }); + + const { cedula } = req.query; + + const { data } = await http.get( + `auth/validations/users/existence?username=${cedula}` + ); + + const { exists } = data.data; + + res.status(200).json({ exists }); +} diff --git a/src/pages/api/types/index.ts b/src/pages/api/types/index.ts index 597f3cb9..0c4b6ae0 100644 --- a/src/pages/api/types/index.ts +++ b/src/pages/api/types/index.ts @@ -30,6 +30,12 @@ export type ReCaptchaResponse = { name: string; }; +export type VerifyIamUserNameResponse = { + data: { + exists: boolean; + }; +}; + export type VerifyIamUserResponse = { id: string; createdTimestamp: number; diff --git a/src/pages/register/stepper/index.tsx b/src/pages/register/stepper/index.tsx index 08423487..1d96badb 100644 --- a/src/pages/register/stepper/index.tsx +++ b/src/pages/register/stepper/index.tsx @@ -13,8 +13,8 @@ import Step1 from './step1'; import Step2 from './step2'; import Step3 from './step3'; -const steps = ['PASO 1', 'PASO 2', 'PASO 3']; -const optionalLabels = ['Identifícate', 'Verifícate', 'Regístrate']; +const steps = ['Identificación', 'Verificación', 'Registro']; +const optionalLabels = ['NDI del usuario', 'Prueba de vida', 'Cuenta de usuario']; export async function getServerSideProps() { await axios.get(`/api/auth`); @@ -70,15 +70,12 @@ export default function StepperRegister() { return ( {steps.map((label, index) => ( void; + name: string; +} + +const TextMaskCustom = forwardRef( + function TextMaskCustom(props, ref: any) { + const { onChange, ...other } = props; + return ( + + onChange({ target: { name: props.name, value } }) + } + overwrite + /> + ); + } +); + export default function Step1({ setInfoCedula, handleNext }: any) { const [loading, setLoading] = useState(false); + const [valueCedula, setValueCedula] = useState(''); + const luhnCheck = (num: string) => { const arr = (num + '') .split('') @@ -28,7 +65,7 @@ export default function Step1({ setInfoCedula, handleNext }: any) { .map((x) => parseInt(x)); const lastDigit = arr.splice(0, 1)[0]; let sum = arr.reduce( - (acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((2 * val) % 9) || 9), + (acc, val, i) => (i % 2 !== 0 ? acc + val : acc + (val * 2 > 9 ? val * 2 - 9 : val * 2)), 0 ); sum += lastDigit; @@ -36,17 +73,23 @@ export default function Step1({ setInfoCedula, handleNext }: any) { }; const { - register, handleSubmit: handleFormSubmit, formState: { errors }, + setValue, } = useForm({ reValidateMode: 'onSubmit', - shouldFocusError: false, + // shouldFocusError: false, + resolver: yupResolver(schema), }); const { executeRecaptcha } = useReCaptcha(); const { AlertError, AlertWarning } = useSnackbar(); + const handleChange = (event: React.ChangeEvent) => { + setValue('cedula', event.target.value.replace(/-/g, '')); + setValueCedula(event.target.value); + }; + const handleSubmit = useCallback( async (data: IFormInputs) => { const cleanCedula = data?.cedula?.replace(/-/g, ''); @@ -71,6 +114,15 @@ export default function Step1({ setInfoCedula, handleNext }: any) { token, }); if (response.data && response.data.isHuman === true) { + const responseCedula = await fetch(`/api/iam/${cleanCedula}`); + if (responseCedula.status !== 200) { + throw new Error('Failed to fetch iam data'); + } + const { exists } = await responseCedula.json(); + if (exists) { + console.log(exists); + return AlertWarning('Su Cédula ya se encuentra registrada.'); + } const response = await fetch(`/api/citizens/${cleanCedula}`); if (response.status !== 200) { throw new Error('Failed to fetch citizen data'); @@ -87,7 +139,8 @@ export default function Step1({ setInfoCedula, handleNext }: any) { } } catch (err) { console.error(err); - AlertError('Esta cédula es correcta, pero no hemos podido validarla.'); + // AlertError('Esta cédula es correcta, pero no hemos podido validarla.'); + AlertError('No hemos podido validar su Cédula'); } finally { setLoading(false); } @@ -96,8 +149,9 @@ export default function Step1({ setInfoCedula, handleNext }: any) { ); return ( + // TODO: Validate this loading approach with Backdrop <> -
+ {/*
theme.zIndex.drawer + 1 }} open={loading} @@ -105,7 +159,9 @@ export default function Step1({ setInfoCedula, handleNext }: any) { Validando cédula... -
+
*/} + {loading && } + Este es el primer paso para poder verificar tu identidad y crear tu @@ -114,27 +170,46 @@ export default function Step1({ setInfoCedula, handleNext }: any) {
- - + + - - - - - + + + + +
+ + + + + ¿Ya tienes una cuenta?{' '} + Inicia sesión aquí. + + + +
); diff --git a/src/pages/register/stepper/step2.tsx b/src/pages/register/stepper/step2.tsx index 795a7628..161d3db0 100644 --- a/src/pages/register/stepper/step2.tsx +++ b/src/pages/register/stepper/step2.tsx @@ -15,6 +15,8 @@ import { import Step2Modal from './step2Modal'; import { useSnackbar } from '@/components/elements/alert'; +import { GridContainer, GridItem } from '@/components/elements/grid'; +import { ButtonApp } from '@/components/elements/button'; interface IFormInputs { acceptTermAndConditions: boolean; @@ -62,8 +64,8 @@ export default function Step2({ infoCedula, handleNext }: IStep2Props) {
- - + +
- Utilizar un dispositivo que posea {' '} + Utilizar un dispositivo que posea{' '} cámara integrada.
-
+ - +
- Permitir que tomemos capturas de {' '} + Permitir que tomemos capturas de{' '} tu rostro.
-
+
- + )} - -
+ + +
- - - + + + INICIAR PROCESO {open && ( )} - - + +
); diff --git a/src/pages/register/stepper/step3.tsx b/src/pages/register/stepper/step3.tsx index a4b3c3e5..03eda969 100644 --- a/src/pages/register/stepper/step3.tsx +++ b/src/pages/register/stepper/step3.tsx @@ -5,25 +5,25 @@ import { useState } from 'react'; import * as yup from 'yup'; import axios from 'axios'; import { Crypto } from '@/helpers'; +import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import PasswordLevel from '@/components/elements/passwordLevel'; import { useSnackbar } from '@/components/elements/alert'; import { labels } from '@/constants/labels'; import { - Backdrop, + Alert, Box, - Button, - CircularProgress, - Grid, IconButton, InputAdornment, - Snackbar, TextField, Tooltip, Typography, } from '@mui/material'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; +import { ButtonApp } from '@/components/elements/button'; +import { GridContainer, GridItem } from '@/components/elements/grid'; +import LoadingBackdrop from '@/components/elements/loadingBackdrop'; interface IFormInputs { email: string; @@ -58,6 +58,7 @@ export default function Step3({ handleNext, infoCedula }: any) { const [isPwned, setIsPwned] = useState(false); const { AlertError, AlertWarning } = useSnackbar(); const [showPassword, setShowPassword] = useState(false); + const [showPasswordConfirm, setShowPasswordConfirm] = useState(false); const { register, @@ -69,7 +70,7 @@ export default function Step3({ handleNext, infoCedula }: any) { resolver: yupResolver(schema), }); - const handleClickShowPassword = () => setShowPassword((show) => !show); + // const handleClickShowPassword = () => setShowPassword((show) => !show); const handleMouseDownPassword = ( event: React.MouseEvent @@ -130,7 +131,7 @@ export default function Step3({ handleNext, infoCedula }: any) { // TODO: Use this Password UI approach https://stackblitz.com/edit/material-password-strength?file=Icons.js return ( <> -
+ {/*
theme.zIndex.drawer + 1 }} open={loadingValidatingPassword} @@ -147,7 +148,12 @@ export default function Step3({ handleNext, infoCedula }: any) { Creando usuario... -
+
*/} + {loadingValidatingPassword && ( + + )} + {loading && } + Para finalizar tu registro completa los siguientes campos: @@ -155,8 +161,8 @@ export default function Step3({ handleNext, infoCedula }: any) {
- - + + { + e.preventDefault(); + return false; + }} + onCopy={(e) => { + e.preventDefault(); + return false; + }} /> - + - + { + e.preventDefault(); + return false; + }} + onCopy={(e) => { + e.preventDefault(); + return false; + }} /> - + - + @@ -209,7 +234,7 @@ export default function Step3({ handleNext, infoCedula }: any) { setShowPassword(!showPassword)} onMouseDown={handleMouseDownPassword} edge="end" > @@ -221,12 +246,12 @@ export default function Step3({ handleNext, infoCedula }: any) { /> - + - + + setShowPasswordConfirm(!showPasswordConfirm) + } onMouseDown={handleMouseDownPassword} edge="end" > - {showPassword ? : } + {showPasswordConfirm ? : } ), }} /> - + - {isPwned && ( - + {/* TODO: validate why not use snackbar */} + {/* {isPwned && ( + Esta contraseña ha estado en filtraciones de datos, por eso no se considera segura. Te recomendamos eligir otra contraseña. - + + )} */} + {isPwned && ( + + + Esta contraseña ha estado en filtraciones de datos, por eso no + se considera segura. Te recomendamos eligir otra contraseña. + + )} - - - - + + +
); diff --git a/src/styles/globals.css b/src/styles/globals.css index a7b34ffa..0633f5ae 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -17,7 +17,7 @@ body { } .text-primary { - color: #003670; + color: #003876; } .text-success { @@ -37,7 +37,7 @@ body { } .bg-primary { - background: #003670; + background: #003876; } .bg-success { @@ -128,10 +128,6 @@ body { /* MUI global */ -.MuiStepConnector-line { - display: none !important; -} - .MuiStepLabel-label { color: #03397775; } diff --git a/src/themes/index.tsx b/src/themes/index.tsx index 48fbfefd..10e31655 100644 --- a/src/themes/index.tsx +++ b/src/themes/index.tsx @@ -4,7 +4,7 @@ import { red } from "@mui/material/colors"; export const theme = createTheme({ palette: { primary: { - main: "#003670", + main: "#003876", }, secondary: { main: "#EE2A24", @@ -48,5 +48,14 @@ export const theme = createTheme({ }, }, }, + MuiTextField: { + defaultProps: { + sx: { + '& .MuiInputBase-root': { + background: '#F8F8F8', + }, + } + } + } }, }); diff --git a/yarn.lock.REMOVED.git-id b/yarn.lock.REMOVED.git-id index 57faf24f..489b1649 100644 --- a/yarn.lock.REMOVED.git-id +++ b/yarn.lock.REMOVED.git-id @@ -1 +1 @@ -cfcb4c68032de6c744c4ca6b08467a4fc65eeea2 \ No newline at end of file +99747535bfec6a708b4cecbb901acfd9f553dc0a \ No newline at end of file