Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEV-1465] Reset login form errors on user changes #692

Merged
merged 21 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
---

[DEV-1465] Reset login form errors on user changes
jeremygordillo marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';
jeremygordillo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading