Skip to content

Commit

Permalink
[DEV-1465] Reset login form errors on user changes (#692)
Browse files Browse the repository at this point in the history
* refactor login form

* handle reset errors on change input value

* add LoginForm tests

* add test

* Create sour-penguins-smile.md

* Moved test file

* fix error prop value

* improve test

* pr changes

* minor fixes

* changes after review

* Update .changeset/sour-penguins-smile.md

Co-authored-by: marcobottaro <[email protected]>

---------

Co-authored-by: marcobottaro <[email protected]>
  • Loading branch information
jeremygordillo and marcobottaro authored Mar 11, 2024
1 parent f250a26 commit ffde2ad
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-penguins-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nextjs-website": patch
---

Reset login form errors on user changes
63 changes: 31 additions & 32 deletions apps/nextjs-website/src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,34 @@ import ConfirmSignUp from '@/components/organisms/Auth/ConfirmSignUp';
import { useRouter, useSearchParams } from 'next/navigation';
import PageBackgroundWrapper from '@/components/atoms/PageBackgroundWrapper/PageBackgroundWrapper';
import { SignInOpts } from '@aws-amplify/auth/lib/types';
import AuthStatus from '@/components/organisms/Auth/AuthStatus';

const Login = () => {
const router = useRouter();
const [logInStep, setLogInStep] = useState(LoginSteps.LOG_IN);
const [user, setUser] = useState(null);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');

const [loginData, setLoginData] = useState({ username: '', password: '' });
const [submitting, setSubmitting] = useState(false);
const [noAccountError, setNoAccountError] = useState<boolean>(false);

const onLogin: LoginFunction = useCallback(async ({ username, password }) => {
setSubmitting(true);
setNoAccountError(false);
setUsername(username);
setPassword(password);

const user = await Auth.signIn({
username,
password,
} as SignInOpts).catch((error) => {
if (error.code === 'UserNotConfirmedException') {
setUsername(username);
setLogInStep(LoginSteps.CONFIRM_ACCOUNT);
} else {
setNoAccountError(true);
}
return false;
});
setLoginData({ username, password });

setUsername(username);
const opts: SignInOpts = { username, password };
const user = await Auth.signIn(opts)
.catch((error) => {
if (error.code === 'UserNotConfirmedException') {
setLogInStep(LoginSteps.CONFIRM_ACCOUNT);
} else {
setNoAccountError(true);
}
return false;
})
.finally(() => {
setSubmitting(false);
});

if (user) {
setUser(user);
Expand All @@ -47,12 +46,12 @@ const Login = () => {
}, []);

const resendCode = useCallback(async () => {
const result = await onLogin({ username, password })
const result = await onLogin(loginData)
.then(() => true)
.catch(() => false);

return result;
}, [onLogin, password, username]);
}, [onLogin, loginData]);

const searchParams = useSearchParams();

Expand All @@ -66,15 +65,9 @@ const Login = () => {
[router, searchParams, user]
);

const onBackStep = useCallback(() => {
router.replace(
`/auth/login?email=${encodeURIComponent(username || '')}&step=${
LoginSteps.LOG_IN
}`
);
const onBackStep = () => {
setLogInStep(LoginSteps.LOG_IN);
return null;
}, [router, username]);
};

return (
<PageBackgroundWrapper>
Expand All @@ -86,17 +79,23 @@ const Login = () => {
spacing={6}
>
{logInStep === LoginSteps.LOG_IN && (
<LoginForm noAccount={noAccountError} onLogin={onLogin} />
<AuthStatus>
<LoginForm
submitting={submitting}
noAccount={noAccountError}
onLogin={onLogin}
/>
</AuthStatus>
)}
{logInStep === LoginSteps.MFA_CHALLENGE && (
<ConfirmLogIn
email={username}
email={loginData.username}
onConfirmLogin={confirmLogin}
resendCode={resendCode}
/>
)}
{logInStep === LoginSteps.CONFIRM_ACCOUNT && (
<ConfirmSignUp email={username || ''} onBack={onBackStep} />
<ConfirmSignUp email={loginData.username} onBack={onBackStep} />
)}
</Grid>
</PageBackgroundWrapper>
Expand Down
14 changes: 14 additions & 0 deletions apps/nextjs-website/src/components/organisms/Auth/AuthStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';
import { useAuthenticator } from '@aws-amplify/ui-react';
import { redirect } from 'next/navigation';
import { PropsWithChildren } from 'react';

export default function AuthStatus({ children }: PropsWithChildren) {
const { authStatus } = useAuthenticator((context) => [context.authStatus]);

if (authStatus === 'authenticated') {
redirect('/');
}

return children;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { useTranslations } from 'next-intl';

interface ConfirmSignUpProps {
email: string;
onBack: () => null;
// eslint-disable-next-line functional/no-return-void
onBack: () => void;
}

const ConfirmSignUp = ({ email, onBack }: ConfirmSignUpProps) => {
Expand Down
109 changes: 65 additions & 44 deletions apps/nextjs-website/src/components/organisms/Auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
'use client';
import { LoginFunction } from '@/lib/types/loginFunction';
import { useAuthenticator } from '@aws-amplify/ui-react';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import {
Box,
Expand All @@ -22,43 +20,47 @@ import {
import { IllusLogin } from '@pagopa/mui-italia';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { MouseEvent, useCallback, useEffect, useState } from 'react';
import {
ChangeEvent,
MouseEvent,
useCallback,
useEffect,
useState,
} from 'react';
import { validateEmail, validateField } from '@/helpers/auth.helpers';

interface LoginFormProps {
onLogin: LoginFunction;
noAccount: boolean;
noAccount?: boolean;
submitting?: boolean;
}

interface LoginFieldsError {
email: string | null;
password: string | null;
}

const LoginForm = ({ onLogin, noAccount = false }: LoginFormProps) => {
const LoginForm = ({
onLogin,
noAccount = false,
submitting = false,
}: LoginFormProps) => {
const signUp = useTranslations('auth.signUp');
const login = useTranslations('auth.login');
const shared = useTranslations('shared');
const errors = useTranslations('errors');

const { palette } = useTheme();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [submitting, setSubmitting] = useState(false);

const { authStatus } = useAuthenticator((context) => [context.authStatus]);
const [formData, setFormData] = useState({
username: '',
password: '',
});

const [showPassword, setShowPassword] = useState(false);
const [fieldErrors, setFieldErrors] = useState<LoginFieldsError>({
email: null,
password: null,
});

if (authStatus === 'authenticated') {
redirect('/');
}

const handleClickShowPassword = useCallback(
() => setShowPassword((show) => !show),
[]
Expand All @@ -71,17 +73,31 @@ const LoginForm = ({ onLogin, noAccount = false }: LoginFormProps) => {
[]
);

const handleChangeInput = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (fieldErrors.email || fieldErrors.password) {
setFieldErrors({
email: null,
password: null,
});
}

setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
},
[fieldErrors]
);

const validateForm = useCallback(() => {
const emailError = validateEmail(username);
const emailError = validateEmail(formData.username);
const passwordError = validateField(formData.password);

const passwordError = validateField(password);
setFieldErrors({
email: emailError ? shared(emailError) : null,
password: passwordError ? shared(passwordError) : null,
});

return !emailError && !passwordError;
}, [username, shared, password]);
}, [shared, formData]);

const setNotloggedOnError = useCallback(() => {
if (noAccount) {
Expand All @@ -99,14 +115,11 @@ const LoginForm = ({ onLogin, noAccount = false }: LoginFormProps) => {

const onLoginHandler = useCallback(() => {
const valid = validateForm();
if (!valid) {
return;
}
setSubmitting(true);
onLogin({ username, password }).finally(() => {
setSubmitting(false);
});
}, [validateForm, onLogin, username, password, errors]);

if (!valid) return;

onLogin(formData);
}, [validateForm, onLogin, formData]);

return (
<Box
Expand All @@ -130,32 +143,35 @@ const LoginForm = ({ onLogin, noAccount = false }: LoginFormProps) => {
</Typography>
<Stack spacing={2} mb={4}>
<TextField
label={shared('emailAddress')}
variant='outlined'
size='small'
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete={'username'}
error={!!fieldErrors.email}
helperText={fieldErrors.email}
error={!!fieldErrors.email || noAccount}
inputProps={{
'aria-label': 'email',
name: 'username',
}}
label={shared('emailAddress')}
required
size='small'
sx={{
width: '100%',
backgroundColor: palette.background.paper,
}}
autoComplete={'username'}
value={formData.username}
variant='outlined'
onChange={handleChangeInput}
/>
</Stack>
<Stack spacing={2} mb={2}>
<FormControl variant='outlined' size='small'>
<TextField
autoComplete={'current-password'}
error={!!fieldErrors.password}
id='password-input'
type={showPassword ? 'text' : 'password'}
onChange={(e) => setPassword(e.target.value)}
label={`${shared('password')}`}
variant='outlined'
size='small'
error={!!fieldErrors.password || noAccount}
required
inputProps={{
'aria-label': 'password',
name: 'password',
}}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
Expand All @@ -170,9 +186,14 @@ const LoginForm = ({ onLogin, noAccount = false }: LoginFormProps) => {
</InputAdornment>
),
}}
autoComplete={'current-password'}
label={`${shared('password')}`}
required
size='small'
type={showPassword ? 'text' : 'password'}
variant='outlined'
onChange={handleChangeInput}
/>
{(fieldErrors.password || noAccount) && (
{fieldErrors.password && (
<FormHelperText error>
{fieldErrors.password}
</FormHelperText>
Expand Down
Loading

0 comments on commit ffde2ad

Please sign in to comment.