Skip to content

Commit

Permalink
[DEV-1333] Fix sign-in and confirm sign-in UI and UX (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcobottaro authored Feb 14, 2024
1 parent 37d15c3 commit c14d7e5
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 148 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-drinks-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nextjs-website": patch
---

Fix sign-in and confirm sign-in UI and UX
45 changes: 26 additions & 19 deletions apps/nextjs-website/src/__tests__/helpers/auth.helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { passwordMatcher } from '../../helpers/auth.helpers';
import {
validateEmail,
validateField,
validatePassword,
} from '../../helpers/auth.helpers';

describe('passwordMatch', () => {
it('returns true if the passwords match', () => {
const validPasswords = ['Password1!', 'P4ssw0rd@!'];
const invalidPasswords = [
'password',
'password1',
'password!',
'password1!',
'Password',
'Password1',
'Password!',
];
describe('validateField', () => {
it('returns error if the value is empty', () => {
expect(validateField('')).toBe('requiredFieldError');
});

it('returns null if the value is not empty', () => {
expect(validateField('test')).toBe(null);
});
});

validPasswords.forEach((password) => {
expect(passwordMatcher.test(password)).toBe(true);
});
describe('validateEmail', () => {
it('returns error if the email is invalid', () => {
expect(validateEmail('')).toBe('requiredFieldError');
expect(validateEmail('test')).toBe('emailFieldError');
expect(validateEmail('[email protected]')).toBe(null);
});
});

invalidPasswords.forEach((password) => {
expect(passwordMatcher.test(password)).toBe(false);
});
describe('validatePassword', () => {
it('returns error if the password is invalid', () => {
expect(validatePassword('')).toBe('requiredFieldError');
expect(validatePassword('password')).toBe('passwordError');
expect(validatePassword('Password1!')).toBe(null);
});
});
81 changes: 59 additions & 22 deletions apps/nextjs-website/src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,59 @@
'use client';
import LoginForm from '@/components/organisms/Auth/LoginForm';
import ConfirmLogIn from '@/components/organisms/Auth/ConfirmLogin';
import { Box, Grid } from '@mui/material';
import { Grid } from '@mui/material';
import { useCallback, useState } from 'react';
import { Auth } from 'aws-amplify';
import { LoginSteps } from '@/lib/types/loginSteps';
import { LoginFunction } from '@/lib/types/loginFunction';
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';

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

const [noAccountError, setNoAccountError] = useState<boolean>(false);

const onLogin: LoginFunction = useCallback(async ({ username, password }) => {
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;
});

setUserName(username);
setUser(user);
setLogInStep(LoginSteps.MFA_CHALLENGE);
setUsername(username);

if (user) {
setUser(user);
setLogInStep(LoginSteps.MFA_CHALLENGE);
}
}, []);

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

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

const searchParams = useSearchParams();

const confirmLogin = useCallback(
Expand All @@ -33,36 +63,43 @@ const Login = () => {
const redirect = searchParams.get('redirect');
router.replace(redirect ? redirect : '/');
},
[router, user]
[router, searchParams, user]
);

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

return (
<Box
sx={{
display: 'flex',
minHeight: '100vh',
alignItems: 'center',
justifyContent: 'center',
width: '100vw',
backgroundImage: 'url(/images/hero.jpg)',
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundPosition: 'bottom right',
}}
>
<PageBackgroundWrapper>
<Grid
container
justifyContent='center'
sx={{ mx: 'auto' }}
my={6}
spacing={6}
>
{logInStep === LoginSteps.LOG_IN && <LoginForm onLogin={onLogin} />}
{logInStep === LoginSteps.LOG_IN && (
<LoginForm noAccount={noAccountError} onLogin={onLogin} />
)}
{logInStep === LoginSteps.MFA_CHALLENGE && (
<ConfirmLogIn email={userName} onConfirmLogin={confirmLogin} />
<ConfirmLogIn
email={username}
onConfirmLogin={confirmLogin}
resendCode={resendCode}
/>
)}
{logInStep === LoginSteps.CONFIRM_ACCOUNT && (
<ConfirmSignUp email={username || ''} onBack={onBackStep} />
)}
</Grid>
</Box>
</PageBackgroundWrapper>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import { useTranslations } from 'next-intl';
import { LoaderPhase } from '@/lib/types/loader';
import DoneIcon from '@mui/icons-material/Done';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import { useCallback, useState } from 'react';
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { Auth } from 'aws-amplify';
import { resetResendEmailAfterMs } from '@/config';

type ResendEmailProps = {
text: string;
email: string;
isLoginCTA?: boolean;
setSubmitting?: Dispatch<SetStateAction<boolean>>;
resendCode?: () => Promise<boolean>;
};

const ResendEmail = ({ text, email }: ResendEmailProps) => {
const ResendEmail = ({
text,
email,
setSubmitting,
isLoginCTA = false,
resendCode,
}: ResendEmailProps) => {
const t = useTranslations('auth.resendEmail');
const { palette } = useTheme();

Expand All @@ -23,19 +32,25 @@ const ResendEmail = ({ text, email }: ResendEmailProps) => {
const handleResendEmail = useCallback(async () => {
setLoader(LoaderPhase.LOADING);

const result = await Auth.resendSignUp(email).catch(() => {
setLoader(LoaderPhase.ERROR);
return false;
});
const result =
isLoginCTA && resendCode
? await resendCode()
: await Auth.resendSignUp(email).catch(() => {
setLoader(LoaderPhase.ERROR);
return false;
});

if (result) {
setLoader(LoaderPhase.SUCCESS);
if (setSubmitting) {
setSubmitting(false);
}
}

setTimeout(() => {
setLoader(undefined);
}, resetResendEmailAfterMs);
}, [email]);
}, [email, isLoginCTA, resendCode, setSubmitting]);

const buildLoader = () => {
switch (loader) {
Expand Down
77 changes: 48 additions & 29 deletions apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,49 @@
'use client';
import { translations } from '@/_contents/translations';
import {
Box,
Grid,
Button,
Card,
Snackbar,
Alert,
Grid,
Stack,
Typography,
TextField,
Button,
Typography,
} from '@mui/material';
import { IllusEmailValidation } from '@pagopa/mui-italia';
import { useCallback, useState } from 'react';
import ResendEmail from '@/components/molecules/ResendEmail/ResendEmail';
import { snackbarAutoHideDurationMs } from '@/config';
import { useTranslations } from 'next-intl';
import { useTheme } from '@mui/material';

interface confirmLoginProps {
email: string | null;
onConfirmLogin: (code: string) => Promise<void>;
resendCode: () => Promise<boolean>;
}

const ConfirmLogin = ({ email, onConfirmLogin }: confirmLoginProps) => {
const {
auth: { confirmLogin },
} = translations;
const ConfirmLogin = ({
email,
onConfirmLogin,
resendCode,
}: confirmLoginProps) => {
const confirmLogin = useTranslations('auth.confirmLogin');

const { palette } = useTheme();
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [code, setCode] = useState<string>('');
const [codeError, setCodeError] = useState<boolean>(false);
const [emptyCode, setEmptyCode] = useState<boolean>(false);

const onConfirmLoginHandler = useCallback(() => {
setCodeError(false);
setEmptyCode(false);
setSubmitting(true);

onConfirmLogin(code).catch((e) => {
setError(e.message);
setSubmitting(false);
if (e.name === 'AuthError') {
setEmptyCode(true);
} else if (e.name === 'NotAuthorizedException') {
setCodeError(true);
}
});
}, [onConfirmLogin, code]);

Expand All @@ -57,53 +64,65 @@ const ConfirmLogin = ({ email, onConfirmLogin }: confirmLoginProps) => {
<IllusEmailValidation />
</Stack>
<Typography variant='h4' pt={8} mb={5} textAlign='center'>
{confirmLogin.title}
{confirmLogin('title')}
</Typography>
{email && (
<Typography variant='body2' mb={6}>
{confirmLogin.body(email)}
{confirmLogin('confirmationCodeSent')}
<Box component='span' fontWeight='fontWeightMedium'>
{email}
</Box>
<br />
{confirmLogin('confirmationCodeExpires')}
</Typography>
)}
<Typography
variant='body1'
sx={{ marginBottom: 1.5, fontWeight: 600 }}
>
{confirmLogin.code}
{confirmLogin('code')}
</Typography>
<Stack spacing={2} mb={4}>
<TextField
variant='outlined'
size='small'
onChange={(e) => setCode(e.target.value)}
helperText={
codeError
? confirmLogin('invalidCode')
: emptyCode
? confirmLogin('emptyCode')
: ''
}
error={codeError || emptyCode}
sx={{
backgroundColor: palette.background.paper,
}}
/>
</Stack>
{email && (
<ResendEmail email={email} text={confirmLogin.checkJunkMail} />
)}
<Stack spacing={4} pt={4} pb={2}>
<Stack spacing={4} pt={4} pb={4}>
<Stack direction='row' justifyContent='center'>
<Button
variant='contained'
disabled={submitting}
onClick={onConfirmLoginHandler}
>
{confirmLogin.continue}
{confirmLogin('continue')}
</Button>
</Stack>
</Stack>
{email && (
<ResendEmail
isLoginCTA={true}
resendCode={resendCode}
setSubmitting={setSubmitting}
email={email}
text={confirmLogin('checkJunkMail')}
/>
)}
</Grid>
</Grid>
</Card>
<Snackbar
open={!!error}
autoHideDuration={snackbarAutoHideDurationMs}
onClose={() => setError(null)}
>
<Alert severity='error'>{error}</Alert>
</Snackbar>
</Box>
);
};
Expand Down
Loading

0 comments on commit c14d7e5

Please sign in to comment.